Commit Graph

1635 Commits

Author SHA1 Message Date
cucadmuh 398adaf214 fix(cover): playlist and radio custom covers (fetch-only getCoverArt ids) (#1295) 2026-07-14 08:47:08 +03:00
cucadmuh fd19419ac2 fix(ui): no cover thumbs on album detail tracklist (#1291) 2026-07-14 00:44:42 +03:00
Psychotoxical f5ddb28d05 fix(discord-banner): give the icon an explicit size so it does not blow up on Windows (#1289)
* fix(discord-banner): give the icon an explicit size so it does not blow up on Windows

The icon carried a viewBox but no width/height, and the .discord-banner-icon
class it uses had no CSS rule at all -- so the SVG had no intrinsic size. WebKit
happened to render it small; Chromium (WebView2, i.e. Windows) fell back to the
300x150 default replaced-element size, so the icon swallowed the bar and pushed
the message out of the row.

Size it explicitly on the element (correct even before the stylesheet applies)
and add the missing rule with flex-shrink so it cannot be squeezed either.

* docs(changelog): add entry for PR #1289

* docs(changelog): place the #1289 entry in PR order
2026-07-13 22:53:59 +02:00
Psychotoxical 163e0e46a8 feat(player-bar): persistent shuffle mode (#1288)
* feat(player-bar): persistent shuffle mode

Shuffle reorders the queue itself -- the tracks after the current one -- and
remembers the order it came from, so switching it off restores that order. The
flag and the remembered order are persisted together: shuffle survives a
restart, so the order it can be undone to has to survive with it.

Keeping the list untouched and only *playing* in a hidden random order was
rejected: "what plays next" is derived from list order in four places (manual
next, gapless successor, chain preload, crossfade/AutoDJ plan), the engine is
handed the next track ~30s ahead with no way to take it back, and the server
play-queue and Orbit guests only ever see the list order -- a hidden permutation
would be invisible to them and lost on any other client.

The toggle lives with the other queue mutations (it pushes an undo snapshot and
syncs to the server like they do); the pure helpers stay in their own module,
free of store imports, so no new import cycle is introduced.

Restoring is id-based and total: duplicate track ids each keep their copy, rows
enqueued while shuffle was on go to the end (they had no original position), and
no row is ever lost or duplicated.

* i18n(player): shuffle on/off state and player bar item label in all 14 locales

* docs(changelog): add entry and credit for PR #1288
2026-07-13 22:16:38 +02:00
Psychotoxical d0edd925e4 feat(player-bar): configurable stop button, album line and reorderable actions (#1287)
* feat(player-bar): configurable stop button, album line and reorderable actions

Extends the existing player-bar layout store instead of adding a second one.

- 'stop' becomes a layout item, so the stop button can be hidden. It lives in a
  'transport' zone: visibility only, no reordering -- the play button is a
  centred special case and shuffling controls around it would fight the adaptive
  small-window layout. Hiding it leaves no dead end, since the primary button
  already acts as stop while previewing.
- Track info gains an optional album line under the artist, off by default and
  suppressed for radio and previews, which have no album.
- The right-hand action buttons are drag-reorderable via the shared
  useListReorderDnd primitive. Moves resolve by stable id against the full list,
  so the zone filter that decides which rows render cannot desync the reorder.
- The section is no longer gated behind Advanced.

Rehydrate keeps older stored layouts working: items added later ('stop') are
appended with their default instead of silently disappearing.

* i18n(settings): player bar constructor strings in all 14 locales

* docs(changelog): add entry and credit for PR #1287
2026-07-13 21:29:46 +02:00
Psychotoxical dac7bf56d9 fix(music-network): surface the underlying cause of a connect NETWORK error (#1285)
* fix(music-network): surface the underlying cause of a connect NETWORK error

NETWORK is the transport catch-all: a DNS failure, a TLS handshake broken by
a proxy or AV, a timeout and an unrecognised provider API error all collapse
into it. The connect form rendered only the translated string, so the cause
never reached the user or a bug report -- the same wire, transport and
token-poll strategy work for one provider and fail for another with no way to
tell why (#1283).

Add errorDetail(), which carries the transport message for NETWORK only
(capped), and append it to the message the connect form shows.

* docs(changelog): add entry for PR #1285

* fix(music-network): make the connect error line selectable so it can be copied
2026-07-13 20:27:48 +02:00
cucadmuh 52d927ca0b feat(radio): Web Audio EQ on HTML5 streams (fixes #1276) (#1284) 2026-07-13 15:36:16 +03:00
cucadmuh efb8e5782c docs(release): CHANGELOG order and WHATS_NEW for 1.50.0 (#1282) 2026-07-13 06:20:06 +03:00
cucadmuh 108a1fb74a chore(nix): refresh nixpkgs pin in flake.lock (#1281) 2026-07-13 06:05:17 +03:00
cucadmuh 9ca762dfd6 feat(ui): album art thumbs in track lists via standard cover pipeline (#1280) 2026-07-13 01:16:55 +00:00
cucadmuh ad5d88d105 feat(queue): adapt toolbar for Navidrome public share queues (#1279) 2026-07-12 23:22:36 +03:00
cucadmuh 6e448dcc3c fix(build): WiX MSI version mapping and album dynamic import (#1278) 2026-07-12 22:45:22 +03:00
cucadmuh 54bc9814f1 fix(windows): startup hang, stable device IDs, boot barrel guards (#1277) 2026-07-12 21:28:58 +03:00
cucadmuh 097438f9da feat(share): open Navidrome public share links for anonymous playback (#1275) 2026-07-12 17:21:03 +03:00
cucadmuh 83aaf253cc feat(eq): follow OS default output for per-device EQ (#1233) (#1274) 2026-07-12 01:08:02 +03:00
cucadmuh 96530f244e feat(servers): connect and run fully behind a custom-header gate (#1273)
* fix(servers): probe gated servers over native reqwest so custom headers connect

Adding a server behind a header gate (Cloudflare Access, Pangolin service
tokens) failed at the connect step (#1216). The connect probe ran in the
WebView (axios); a non-safelisted header such as Authorization / CF-Access-*
makes the browser send a CORS preflight OPTIONS first, and that preflight
carries no token — so the gate rejects it and the real request never leaves.
Streaming and covers already worked because they go through Rust (reqwest),
which never preflights.

Route the header-bearing connect probe through a new Tauri command
`probe_server_connection` that runs the Subsonic ping over native reqwest with
the per-server header context (endpoints + apply rule), reusing the same
resolver as the data plane. Header-less servers keep the lightweight WebView
path unchanged.

- Rust: add `probe_server_connection` (+ `ServerProbeResult`) in app_api/network,
  register in collect_commands!/generate_handler!, regenerate specta bindings.
- Frontend: `pingWithCredentialsForProfile` calls the command when the profile
  has custom headers; add `serverHttpContextWireForProbe` helper.
- Tests: SubsonicClient sends the gate header on ping via http_context (and
  misses the gated matcher without it); frontend probe routing + WebView
  fallback; result mapping unit tests.

* docs(changelog): note gated-server connect fix (#1272)

* fix(servers): keep add form open and surface the reason when connect fails

The add-server flow closed the form the instant you pressed Add and, on
failure, left only a small status dot — so a bad password, a rejected gate
header, or an unreachable host all looked the same ("nothing happened").

- The connect probe now returns a short failure reason (the server's own
  error message, an HTTP status, or a transport error) via ServerProbeResult
  and the axios fallback; no secrets or header values are included.
- ServersTab keeps the Add form open until the connect test succeeds and
  shows the reason in a toast on failure (edit flow surfaces it too).
- AddServerForm validates the server address and username before probing,
  with clear per-field messages instead of a silent no-op.
- New i18n keys (serverUrlRequired, serverUsernameRequired,
  serverConnectFailedReason) added across all shipped locales.

* fix(servers): route all WebView Subsonic REST through native proxy for gated servers

Once a header-gated server (Cloudflare Access, Pangolin, …) connected, every
view that still fetched over axios in the WebView came up empty — Main stage,
New Releases, Random Albums, Statistics, search, genres — because a
non-safelisted gate header makes the WebView send a CORS preflight the gate
rejects. Only the Rust-routed paths (streaming, covers, ping) worked.

- Add a generic `subsonic_proxy_request` Tauri command backed by
  `SubsonicClient::send_raw`: it runs the request natively (no preflight),
  applies the per-server gate header via ServerHttpContext, and returns the
  untouched JSON body for the WebView to parse as it does an axios response.
  Supports GET and form-POST (OpenSubsonic formPost) with a clamped timeout.
- `api`, `apiWithCredentials`, `apiPostFormWithCredentials` (and thus
  `apiForServer`/`apiPostFormForServer`) now switch to the native proxy exactly
  when a request would carry gate headers; header-less servers keep the
  lightweight WebView axios path unchanged.
- Tests: wiremock coverage for `send_raw` (GET gate header + form-POST body);
  frontend routing tests that gated `api()` calls hit the proxy and header-less
  ones stay on axios; updated the OpenSubsonic extensions test for the new path.

* fix(servers): apply gate header to media paths via URL fallback

Streaming, prefetch and artist-info fetches returned 403 on a gated
server: apply_for_http_url resolved custom headers strictly by
server_ref and attached nothing when the caller's ref (e.g. the audio
engine's playback server id) didn't match the registry key. Add a
resolve_context helper that falls back to matching the request URL
against the server's registered endpoints — the same fallback covers
and Navidrome browse already relied on — and route the audio engine,
apply_for_http_url and apply_optional_registry_headers through it.
Endpoint matching only hits a configured gated server and still honours
the apply-to LAN/public rule, so non-gated servers stay untouched.

Also pool the native proxy's reqwest clients by timeout bucket so the
extra gated-server browse traffic reuses keep-alive connections instead
of opening a fresh pool per request (which surfaced as spurious
timeouts / 499s on fast endpoints).

* fix(servers): register gate headers before probe/bind so native paths work

Root cause of the persistent 403s behind a header gate: the native
ServerHttpRegistry was populated only at the end of bindIndexedServer,
after ensureConnectUrlResolved + the bind session. When that probe was
slow, hung, or reported the server briefly offline, the sync never ran,
leaving the registry empty — so every Rust-initiated request (stream,
cover, prefetch, fanart getArtistInfo2, Navidrome /auth/login) resolved
no header and the gate returned 403. The inline-context proxy path kept
working, which is why browse succeeded while media/covers failed.

Register the header context up front: before the reachability probe in
bindIndexedServer, and authoritatively for all servers at the start of
bootstrapAllIndexedServers (once React has mounted and IPC is ready).
Add concise sync/sync_all diagnostics (endpoint URLs + header count,
never values) so gated-server registration is observable in logs.

* fix(servers): retry gated-server covers that 403'd during header-registry gap

Covers fetched while the native gate-header registry was momentarily empty
(e.g. a dev restart before the header sync landed) got a 403, which the cover
fetcher classified as a permanent "cover missing" and wrote a 30-minute
`.fetch-failed` marker for — so on a gated server most artwork stayed blank
long after the gate started answering 200.

- Treat a gate-style 401/403 (plus 408/425/429) on cover art as a transient,
  retryable hiccup rather than a permanent miss, so the short retry loop can
  ride out a brief registry gap. Genuine 404/410/400 stay permanent.
- On (re)bind of a gated server, once its header is registered, clear that
  server's stale `.fetch-failed` markers and kick a backfill pass so the
  covers re-download — same rationale as the URL-change retry.

* refactor(servers): funnel gate-header application through one helper

Collapse the remaining bespoke header-application variants onto the single
`apply_optional_registry_headers` / `resolve_context` entry point so gated-server
header logic lives in exactly one place:

- cover_cache fetch and Navidrome `/auth/login` used older
  `apply_for_http_url` / `get_for_server_url` + `apply_server_headers_for_http_url`
  combos; both now call the shared helper.
- The audio engine's `apply_playback_request_headers` delegates to it instead of
  re-implementing the resolve+apply.
- Drop the now-unused `ServerHttpRegistry::apply_for_http_url` /
  `apply_for_base_url` methods.

Behaviour is unchanged: a non-gated server (no registry match) leaves every
request untouched, exactly as before. This is purely removing duplicate ways to
do the same thing so new native paths have one obvious hook.

* chore(servers): drop server-http registry debug logging

Remove the `[server-http] sync/sync_all` eprintln diagnostics added while
tracing the header-registry timing bug. They printed to stderr on every sync
(startup + each persist-rehydrate) and echoed endpoint URLs; the behaviour is
verified and covered by tests now, so the noise is no longer warranted. The
dev-gated `[connect-probe]`/`[subsonic-proxy]` failure logs stay.

* docs(changelog): point gated-server entry at PR #1273

* fix(servers): await gate-header sync before probe/bind

bindIndexedServer registered the per-server gate headers with a fire-and-forget
`void syncServerHttpContextForProfile(...)`, so a direct bind (add / enable a
single server) could run the native reachability probe and bind session while
the header IPC was still in flight — the registry was empty and native paths
403'd behind the gate, the exact race this branch fixes. Await the sync before
probing/binding; the stale-cover retry stays off the critical path.

* fix(servers): reclaim LAN from a sticky public endpoint

For a dual-address profile, the first endpoint that answered after launch stuck
for the whole session: a public sticky endpoint was always tried first and kept
answering, so the LAN-first order was never re-run and the connection never
upgraded back to LAN. pickReachableBaseUrl now makes a short, no-retry, bounded
quick-probe of the higher-priority LAN endpoint before honouring a public sticky
entry, so a laptop returning to the LAN upgrades on the next reachability tick
while staying remote costs only one bounded probe (never the full retry cushion).

* fix(servers): refresh LAN/public badge on active-server switch; credit #1273

The connection badge read the endpoint kind from the last probe and never
re-probed when the active server changed, so switching between a LAN-only and a
public server left the badge stuck on the old server's classification until the
120-s tick. useConnectionStatus now resets the kind and re-probes when
activeServerId changes (mount still deferred to the polling effect). Also adds
the settings credits line for the gated-server work (PR #1273).

* fix(servers): LAN reclaim uses the probe's own timeout, not a 3s race

The reclaim quick-probe raced pingWithCredentialsForProfile against a fixed 3s
timer, but the underlying probe (native reqwest for gated servers, axios
otherwise) runs on a 15s timeout — so a LAN endpoint answering between 3s and
15s was wrongly treated as unreachable and the session stayed pinned to the
public sticky endpoint. Reclaim now does a single, no-retry probe on the LAN
endpoint using the normal ping timeout: a slow-but-reachable LAN still upgrades,
while a dead one still costs only one attempt (not the full retry cushion)
before falling through to the sticky sequence.
2026-07-11 16:17:17 +03:00
cucadmuh 1625f4a8be feat(settings): start minimized to tray on cold launch (#1271) 2026-07-11 01:41:44 +03:00
Psychotoxical 51140c613a fix(lyrics): read SYNCEDLYRICS from the concrete Vorbis comment block (#1267)
* fix(lyrics): read SYNCEDLYRICS from the concrete Vorbis comment block

Embedded lyrics were never found on a FLAC/Ogg file whose only lyrics field is
`SYNCEDLYRICS`. The lookup went through lofty's generic tag via
`ItemKey::from_key(tag_type, "SYNCEDLYRICS")`, which returns `None` because the
key is not one lofty knows — and the generic tag drops unknown Vorbis comments
entirely on conversion, so the value was not reachable there at all. The branch
had been dead since it was written; files fell through to the `LYRICS` field or
reported no lyrics.

Read the comment block off the concrete file type (FLAC, Vorbis, Opus, Speex)
instead, keeping the documented priority: SYNCEDLYRICS before plain LYRICS.

Tests cover the reader end to end against generated files: SYNCEDLYRICS and
LYRICS returned verbatim (Enhanced LRC word stamps included), SYNCEDLYRICS
winning over a plain fallback, USLT returned verbatim, and SYLT rebuilt as
line-level LRC — which is why that source can never carry word timing.

* docs(changelog): Vorbis SYNCEDLYRICS fix (#1267)
2026-07-10 04:07:03 +02:00
Psychotoxical ed99c8f2f7 test(lyrics): cover the embedded word-lyrics chain (#1268)
Pins the path from a local file's LRC tag to the word lines the pane renders:
Enhanced LRC yields word lines with clean text, plain LRC yields none so the
pane falls back to line sync. Verified to fail when the hook stops forwarding
word lines from the embedded source.
2026-07-10 04:06:43 +02:00
Psychotoxical 199eb24465 chore(audio): satisfy clippy::question_mark on the hi-res spill read (#1269)
Rust 1.97 flags the `match spill_path { Some(p) => …, None => return None }`
as a `?` in disguise, which fails `cargo clippy -- -D warnings` on CI for every
Rust pull request. Same control flow, written the way the lint asks for.
2026-07-10 03:58:08 +02:00
Psychotoxical c10b13944b fix(lyrics): strip Enhanced LRC word markers from displayed text (#1266)
* fix(lyrics): strip Enhanced LRC word markers from displayed text

The LRC parser only matched the leading line stamp and passed the rest of the
line through as text, so Enhanced LRC inline markers rendered literally:
`<00:12.34>Hello` instead of `Hello`. Offline and hot-cached tracks read their
embedded tags through this parser without the server ever seeing them, so a
track with embedded Enhanced LRC showed the markers in the pane.

Parse the markers instead of ignoring them: the text is now marker-free, and
where a line carries them they drive the same word-by-word highlighting the
server's songLyrics v2 cues do. A line without markers becomes one full-line
word, so word mode never drops a line.

Move the parser out of the LRCLIB client into `utils/lrc`, since embedded tags
and Netease feed it too, and move `LrcLine` next to the other lyrics types so
the parser can produce word lines without an import cycle.

* docs(changelog): Enhanced LRC marker fix (#1266)
2026-07-10 03:17:09 +02:00
Psychotoxical a307f04b88 feat(lyrics): word-level lyrics from OpenSubsonic songLyrics v2 (#1265)
* feat(lyrics): read word-level timing from OpenSubsonic songLyrics v2

Navidrome 0.63 advertises songLyrics v2, which adds word/syllable cues
behind a new `enhanced` flag on getLyricsBySongId. The lyrics pane already
renders word-level highlighting for the lyricsplus provider, so the server
source now feeds the same renderer.

- Request `enhanced` only where the capability catalog says the server
  speaks v2 (Navidrome 0.63+), since no spec forces a v1 server to ignore
  an unknown parameter.
- `enhanced=true` also returns translation and pronunciation layers, so the
  response is filtered to the main layer before display.
- Map cue lines to word lines: a missing cue `end` is all-or-nothing per
  line and resolves from the next cue, then the line end; multi-voice lines
  share an index and keep the main agent; a line without cues stays a single
  full-line word so no line is dropped.
- Bump the lyrics IndexedDB cache so line-only entries cached under the
  90-day TTL cannot suppress word timing.
- Move the shared lyrics value types out of the hook, which also drops the
  useLyrics/lyricsPersistentCache import cycle.

* i18n(settings): add the server word-sync hint

One line explaining what word-by-word lyrics need from the server.

* feat(settings): surface the server word-sync requirement in Lyrics Sources

Word-by-word lyrics need a recent server and lyrics that actually carry word
timing, which is not obvious from the source list alone.

Rebuild the block on the settings sub-card primitives while adding the hint:
the section description moves into the SettingsSubSection slot, the source
list and its ordering hints become a SettingsSubCard/SettingsField, and the
hint uses the field's note slot instead of a hand-rolled div borrowing an
unrelated class.

* docs(changelog): server word-level lyrics (#1265)

Adds the changelog entry, the contributor credit, and a What's New highlight
for the 1.50.0 line.
2026-07-10 02:57:49 +02:00
cucadmuh 4bae7005cd fix(sync): self-heal failed play-queue push without a nagging LED (#1264) 2026-07-09 17:46:07 +03:00
cucadmuh 51d08b7861 docs(contributing): document frontend layering contract (#1263) 2026-07-09 16:51:11 +03:00
norperz 8e0e707544 fix(sync): use formPost for large savePlayQueue to avoid HTTP 414 (#1262) 2026-07-09 16:50:35 +03:00
Psychotoxical 89140523dc chore(build): bind Windows cargo-test binaries to Common-Controls v6 manifest (#1257)
On Windows/MSVC the test executables for `psysonic-analysis`, `psysonic-audio`
and the top `psysonic` crate link the wry/tao windowing runtime (via the tauri
dependency) and statically import `TaskDialogIndirect` from comctl32. That symbol
only exists in Common-Controls v6; System32 comctl32.dll is v5.82 and lacks it,
so an unmanifested test exe aborts at startup with STATUS_ENTRYPOINT_NOT_FOUND
(0xC0000139) before any test runs. The app binary avoids this through the
manifest tauri_build embeds.

Declare the Common-Controls v6 dependency on the test binaries via build-script
link args, gated to windows-msvc. The app binary's manifest is unchanged
(byte-identical: tauri_build already embeds the same dependency, the linker
dedupes). Non-Windows/CI builds are unaffected.
2026-07-07 18:55:41 +02:00
cucadmuh fbd3aaa094 fix(library): prune orphaned cluster identity keys on rebuild (#1255) 2026-07-07 19:15:36 +03:00
cucadmuh 6445e12eb4 fix(library): heal stale album-artist reference on resync (#1256) 2026-07-07 19:08:36 +03:00
cucadmuh 15d9f2bd4e fix(library): #1252 Random Albums — artist links + missing tile cover art (#1254) 2026-07-07 18:22:22 +03:00
cucadmuh 9a0e388e4e fix(library): prune orphaned artist/album rows after resync (#1253) 2026-07-07 16:27:21 +03:00
Daniel Aquino fc56e0823b feat(i18n): add Italian translation (#1250)
Full Italian (Italiano) UI translation, selectable from the language picker on the Settings and Login screens.
2026-07-07 00:27:41 +02:00
Psychotoxical fa7034f100 Fullscreen player — Prism style (#1251)
* feat(fullscreenPlayer): add Prism style — full-bleed backdrop, right lyrics panel, glass control bar

Third fullscreen player style (Settings → Appearance). Reuses the existing
pipeline: full-bleed artist backdrop (fanart/artistBackdrop), FsLyricsApple in a
floating right-side glass panel, player-store transport, cover-derived accent.
New: the bottom bar layout (transport · time elapsed/−remaining · now-playing
pill with integrated scrubbable progress · volume/queue/lyrics-toggle/minimize)
and fullscreen-player-prism.css. Wired into the style toggle + AppShell routing
+ picker + 13 locales.

* fix(fullscreenPlayer): Prism controls stay visible; transport/utils float bare (no 3-box split)

* fix(fullscreenPlayer): Prism bottom bar is one continuous glass bar (not three separate elements)

* fix(fullscreenPlayer): Prism control row is a centred middle band; only the now-playing element is boxed

* fix(fullscreenPlayer): Prism bar has both an outer glass strip and a darker nested now-playing box

* fix(fullscreenPlayer): Prism outer bar is half-width and centred

* feat(fullscreenPlayer): Prism lyrics — progressive blur on upcoming lines + accent-tinted active line

* style(fullscreenPlayer): centre the title/album/artist text in the Prism now-playing pill

* refactor(fullscreenPlayer): share seek/volume/backdrop/time across FS players

The Prism player had copy-pasted the seekbar, volume, time readout and
backdrop resolution from the Static/Immersive players. Two of those copies
carried bugs: the progress input dropped FsSeekbar's touch/pointer handlers
(no scrubbing on touchscreens) and the volume toggle only recorded the
pre-mute level on the mute click (unmuting after dragging the slider to 0
restored a stale value).

Extract the shared logic into single sources of truth and rewire all three
players onto them:

- useFsArtistBackdrop — the artist-backdrop URL resolution (was duplicated
  verbatim in Static, Immersive and Prism).
- useImperativeSeek — the drag/preview/commit + progress-subscription loop
  with mouse, touch, pointer and keyboard handlers; FsSeekbar and Prism's
  progress line now share it, so touch scrubbing works everywhere.
- useVolumeToggle — mute toggle that continuously tracks the last non-zero
  volume, restoring it correctly regardless of how the level reached 0.
- FsTimeReadout gains `remaining`/`className` props and replaces Prism's
  bespoke time component.

Drops a dead aria-valuetext no-op on the progress input. Adds regression
tests for the volume-toggle restore path and a Prism smoke test.

* docs(changelog): add Prism fullscreen player style (#1251)
2026-07-06 18:14:45 +02:00
Psychotoxical e25f904b04 feat(fullscreenPlayer): selectable Minimal and Immersive fullscreen player styles (#1249)
* feat(fullscreenPlayer): recover immersive player — components, hooks, CSS, settings

Phase 1 of the Minimal/Immersive style toggle. Recover the pre-#1001 fullscreen
player from history into the feature folder and make it compile against the
current architecture:

- FullscreenPlayerImmersive + Fs{Art,Portrait,Seekbar,LyricsMenu,LyricsRail}
- hooks useFsDynamicAccent, useFsArtistPortrait
- fullscreen-player-immersive.css (core/mesh/portrait/controls/seekbar + rail +
  lyrics-menu + no-compositing overrides), shared Apple-lyrics stays in
  adaptive-portrait.css
- re-add settings removed in #1001: showFullscreenLyrics, fsLyricsStyle,
  showFsArtistPortrait, fsPortraitDim; plus the new fullscreenPlayerStyle toggle

Not wired into the shell yet (Phase 2). The two hooks still trip the current
set-state-in-effect lint rule — both are reworked in Phase 3 (portrait rewire to
artistBackdrop + dynamic-accent perf gating), which resolves it.

* feat(fullscreenPlayer): style toggle — route Minimal/Immersive + Appearance picker (13 locales)

* feat(fullscreenPlayer): immersive artist portrait via fanart backdrop pipeline; fix hook lint

* refactor(playback): add feature barrel; route immersive imports through barrels (dep:check)

* fix(fullscreenPlayer): restore lyrics-style i18n (13 locales) + close popover on style pick

* feat(fullscreenPlayer): immersive Apple-lyrics mode shows artist image as dimmed full-screen backdrop

* fix(fullscreenPlayer): address code-review findings on the immersive player

- star: pass currentTrack.serverId to queueSongStar (multi-server correctness)
- cover: use album-keyed ref (useAlbumCoverRef) to stop per-track cover flicker
- backdrop: honour backdrops.fullscreenPlayer.enabled (toggle was ignored)
- lyrics popover: Escape now closes only the popover, not the whole player
  (capture-phase listener + stopPropagation vs useFsIdleFade's bubble handler)
- settings: recover the Show-artist-photo toggle + photo-dim slider (13 locales),
  shown for the immersive style — the persisted settings had no UI
- apple mode: don't mount the (CSS-hidden) portrait — the full-screen backdrop
  already shows the image; avoids a duplicate 2000px load/decode per track
- dynamic accent: cache-as-source-of-truth + last-shown ref so a cache-hit album
  no longer surfaces a stale accent from an earlier album
- rehydrate: clamp fsPortraitDim (0–80) so a malformed value can't yield NaN dim
- FsArt: clear the layer when a track has no cover (was leaving prior art)
- FsSeekbar: pause the progress subscription during keyboard seeking (onKeyDown)

* fix(fullscreenPlayer): stop the immersive scrim over-darkening the artist portrait in rail mode

* fix(fullscreenPlayer): dynamic cover accent — re-run extraction when the async cover src resolves

* refactor(fullscreenPlayer): dynamic accent reads the cover blob via the cover cache, not a raw fetch

* test(fullscreenPlayer): unit-test dynamic accent hook + immersive player render/control smoke

* docs(changelog): note fullscreen player styles + credits (#1249)

* fix(fullscreenPlayer): satisfy npm run lint — drop no-explicit-any casts and ref-in-render
2026-07-06 16:49:51 +02:00
Psychotoxical 01633e3501 feat(themes): credit community theme authors and refine card what's-new (#1248)
* fix(themes): theme card what's-new shows only the latest version

* feat(themes): credit community theme authors in Settings System tab

* docs(changelog): note theme contributor credits and card what's-new (#1248)
2026-07-06 13:59:22 +02:00
cucadmuh 80a84481c9 fix(album): album favorite heart and server-backed album rating on detail (#1247) 2026-07-06 14:33:35 +03:00
Psychotoxical f24d605ca0 fix(discord): drop server cover source that leaked credentials (#1246)
* fix(discord): drop server cover source that leaked credentials

The "server" Discord cover source built an authenticated Subsonic
getCoverArt URL (carrying the username, auth token, and salt) and handed
it to Discord as the large image. Discord fetches external images through
its own proxy and exposes the full source URL to anyone viewing the rich
presence, leaking replayable server credentials.

- Remove the "server" cover source; keep "None" (app icon) and
  "Apple Music" (iTunes, credential-free), both resolved without server auth
- Migrate persisted "server" preference to "None" on rehydrate; new default
  is "None"
- Drop the now-dead frontend cover-URL builder and its test
- Move the two-option picker to the shared SettingsSegmented control
- Remove the obsolete locale key across all 13 locales

* docs(changelog): note Discord server cover credential fix (#1246)
2026-07-06 12:43:06 +02:00
cucadmuh a0c8da073b fix(album): localize track count on album detail header (#1245) 2026-07-06 12:28:49 +03:00
cucadmuh c8d2a55e86 fix(albums): keyboard year filter entry without per-keystroke clamp (#1244) 2026-07-06 12:09:16 +03:00
cucadmuh cf982a6ac2 fix(offline): on-disk-only local browse for hot cache and pins (#1243) 2026-07-06 05:51:57 +03:00
cucadmuh 37190775cb fix(library): load full genre catalog for all-libraries scope (#1242) 2026-07-06 03:39:38 +03:00
cucadmuh 4e6b58967c feat(library): multi-library filter — browse & search across selected libraries (#1241) 2026-07-06 03:02:15 +03:00
Psychotoxical a874192408 feat(themes): surface installed-theme updates in the store (#1240)
* feat(themes): show per-theme changelog and pin updatable themes

Surface theme updates in the store: each card gains an expandable "What's
new" built from the theme's optional manifest changelog (versions listed
newest first), and installed themes with a pending update now float to the
top of the list in both sort modes so they are easy to find.

* i18n(settings): add themeStoreWhatsNew across locales

New "What's new" label for the theme-store changelog disclosure, in all 13
locales.

* docs(changelog): add theme store changelog and pinned-updates entry (#1240)
2026-07-06 01:45:34 +02:00
ImAsra 41ae30f05d feat(discord): add dismissible community banner 2026-07-05 18:32:14 +02:00
Psychotoxical 9a556dfa12 chore(ci): repoint frontend hot-path coverage list to post-restructure paths (#1239) 2026-07-05 14:54:35 +02:00
Psychotoxical 84164570b9 fix(cover): show album cover for playlist tracks in Now Playing (#1218)
* fix(cover): show album cover for playlist tracks in Now Playing

* docs(changelog): note playlist now-playing cover fix (#1218)
2026-07-05 14:44:53 +02:00
cucadmuh 9557f8e10a feat(cli): relative volume and quieter CLI startup (#1238) 2026-07-05 14:57:39 +03:00
cucadmuh f48089e222 fix(artists): case-insensitive browse search for Cyrillic names (#1237) 2026-07-05 14:19:44 +03:00
cucadmuh db645ca72f fix(queue): resolve queue rows on scroll so off-window items stop showing '…' (#1236) 2026-07-05 14:07:16 +03:00
cucadmuh 32a21ec2c3 fix(playlist): batch playlist writes past the GET URL limit (#1227) (#1235)
* fix(playlist): batch playlist writes past the GET URL limit (#1227)

Adding tracks failed past ~341 songs because the write path re-sent the
entire song list as createPlaylist.view?songId=<all> query params on a GET,
blowing past the server's ~8 KiB URL limit. Writes now append incrementally
via updatePlaylist.view?songIdToAdd=<batch> (and songIndexToRemove for
clears/removals) in 150-id batches, so there is no practical size cap. A
per-server in-memory membership cache removes the full getPlaylist refetch
on every dedup, fixing the "slow add on big playlists" report.

Layering detangle (keeps the new cache from adding dep-cruiser cycles):
- lib/api/subsonicPlaylists.ts is pure of the store again; cache invalidation
  on write failure moved to the feature callers' catch blocks.
- membership cache extracted to the core layer (src/store/playlistMembershipStore.ts)
  so offline/orbit/contextMenu/playlist read it directly instead of routing
  through the @/features/playlist barrel.
- severed the offline -> playlist-barrel edge (pinnedOfflineSync name fallback
  through the live playlist list was dead: nameless callers are all gated by
  isSourcePinnedOffline, where offline meta already carries the name).
- confirmAddAllDuplicates moved into the playlist feature; contextMenu submenus
  import the add/merge helpers via the playlist barrel, not deep paths.

dep-cruiser baseline regenerated: 742 -> 714 (net -28, all no-circular). The
churn in the baseline is path-shift of the frozen playerStore SCC (cover/
playback/orbit), not new coupling; the new playlist modules have zero violations.

* docs(changelog): record #1235 playlist URL-limit fix (changelog + credits)

* fix(playlist): seed membership cache from full list, not library-scoped view (#1235 review)

F1: runPlaylistLoad seeded the dedup membership cache from the
library-scope-filtered songs, so out-of-scope members looked "new" and
addTracksToPlaylistWithDedup/collectMergeSongIds could re-add them as
duplicates. Cache now holds the full unfiltered server list while the UI
still shows the filtered view; add a regression test.

Also documents the two accepted trade-offs flagged in review:
- F2: >batch updatePlaylist clears then appends non-atomically (URL-limit
  workaround); a mid-step failure truncates server state, cache invalidation
  lets the client re-read truth.
- F3: dedup read-modify-append isn't atomic across the await; rare missed
  dedup on concurrent adds, self-heals on next load.
2026-07-05 01:24:04 +03:00
cucadmuh 5e075b2c36 fix(connection): ignore spurious navigator.onLine offline hint in Tauri (#1234) 2026-07-04 20:56:31 +03:00