* fix: RC3 queue link underline, genre album artist split, Artists "Other" bucket
Three zunoz reports, one change:
- Queue now-playing card: the artist and album links now underline on hover
(not just recolour), matching clickable names everywhere else. The album
link sits on .queue-current-sub itself; artist links are nested .is-link
spans — both selectors covered.
- Genre album cards split multi-artist credits into individual links like the
rest of the app. Root cause was the local-index genre query hardcoding NULL
for the album raw_json column, so OpenSubsonic artists[] never reached the
card and it fell back to the flat "A • B" single link. Select a.raw_json
instead (parity with the All Albums / advanced-search album queries).
- Artists page alphabet index: # is now digits-only, and a new "Other" bucket
collects accented Latin (Æ/Ø/Å…) and non-Latin scripts (CJK, Cyrillic, …)
that previously fell into the # catch-all. Adds artistBucketKey/compareBuckets
helpers (unit-tested), an OTHER bucket sorted last, and the artists.other
label across 9 locales.
* docs(changelog): queue link hover, genre card artist split, Artists Other bucket (#977)
* fix(tracks): RC3 layout regressions + multi-artist links
- Restore vertical rhythm on the Tracks hub: the header/hero/rails/browse
sections sat two wrapper divs below .tracks-page so its flex gap never
reached them and they collapsed together. New .tracks-hub-stack re-applies
the gap, fixing the tagline-touching-hero and Random-Pick-overlapping-hero
spacing (the rail's nav buttons no longer ride into the box above).
- Stop the sticky browse-table header going transparent on hover: its base
background became opaque but the :hover rule still forced transparent, so
rows bled through. Header now keeps its card background on hover.
- Widen the Duration column (56px -> 72px; 56 -> 64 on mobile) so the
"DURATION" header no longer clips/overruns.
- Split multi-artist tracks into individually clickable artist links in both
the "Track of the moment" hero and the browse list rows (OpenSubsonic
artists[] with single-artist fallback), matching the album tracklist.
* docs(changelog): Tracks RC3 spacing, Duration, header hover, multi-artist (#976)
* fix(mainstage): rename Home Page setting, add start-page fallback + empty state
- Rename "Home Page" personalisation section to "Mainstage" across 9 locales
so the heading matches the sidebar entry; add "mainstage" search keyword.
- Index route "/" now redirects to the first visible library item when the
Mainstage sidebar entry is hidden, instead of stranding the app on a blank
page. New pure resolveStartRoute() mirrors sidebar order + nav-mode gating.
- Show a guided empty state on Mainstage when every section is toggled off,
with a CTA into Settings -> Personalisation.
- Unit tests for resolveStartRoute.
* docs(changelog): Mainstage rename, start-page fallback + empty state (#975)
* fix(ui): pointer cursor on the queue collapse handle
The round handle is a click-to-collapse button but showed the col-resize
cursor like the seam strip. Use a pointer on the handle; the seam keeps
col-resize and a real drag still switches the body cursor to col-resize.
* fix(favorites): make Plays, Last Played and BPM columns sortable
The header marked these columns sortable (pointer cursor) but handleSortClick
gated on a separate, narrower column set, so clicks did nothing. Both the
comparator and the header now share one SORTABLE_COLUMNS source, so the
affordance and the behaviour can't drift apart again.
* docs(changelog): queue handle cursor + favorites column sorting (#974)
* fix(random-mix): limit exclusion toggle click area to checkbox and title
The audiobook exclusion was a full-width label wrapping the checkbox,
title and description, so clicking empty space or the description text
toggled it. Make only the checkbox and its title clickable; the
description and surrounding space are no longer hit targets.
* fix(random-mix): stop excluding Thriller and Fantasy as audiobook genres
These keywords match regular music (e.g. Trance/Metal genre tags, a track
titled "Thriller") because the exclusion checks genre, title, album and
artist by substring, dropping a few legit songs per mix. Remove both from
the audiobook keyword list.
* docs(changelog): random mix audiobook exclusion fixes (#973)
* feat(tooltip): 2s open delay and shared tooltipAttrs helper
Add a 2s hover open delay in TooltipPortal (single behaviour source) so
tooltips no longer flash on quick pointer passes; hiding stays immediate.
Add tooltipAttrs() to pair data-tooltip with a matching aria-label for
buttons touched in the unification work. Covered by Vitest.
* feat(tooltip): lower open delay to 1s
2s felt too long in testing; 1s gives the same anti-flash behaviour
without making intentional hovers wait.
* feat(tooltip): action tooltips on the artist overview
Add tooltips describing the action to Last.fm, Wikipedia, Play All,
Shuffle and Radio. Shuffle/Radio now show a tooltip on desktop too,
not just mobile. Strings added to all 9 locales.
* feat(tooltip): action tooltips on the album overview
Add tooltips describing the action to the desktop Play, Artist Bio and
Download (ZIP) buttons, matching the mobile layout. Strings added to all
9 locales.
* feat(tooltip): action tooltips on the All Albums toolbar
Add tooltips describing the action to the sort, year and genre filter
buttons. SortDropdown gains an optional tooltip prop; the year and genre
filter components carry their own, so the tooltips also appear on the
other browse pages that reuse them. Strings added to all 9 locales.
* feat(tooltip): action tooltips on song-list rows
Add Play and Add-to-queue tooltips to the per-row icons in SongRow, used
by the Tracks browse list, Search and Advanced Search. Also localizes the
aria-labels, which were hardcoded English. New common.addToQueue in all 9
locales.
* fix(tooltip): uniform tooltip placement on the Artists toolbar
The favourite and multi-select buttons forced tooltips below while the
view-mode buttons auto-flipped above, so the row looked inconsistent.
Pin the view-mode buttons below too, matching the rest of the row and the
Albums toolbar.
* feat(tooltip): clarify and align the Advanced Search scope row
Add a leading "Search in:" label and per-chip tooltips so the
All/Artists/Albums/Songs row reads as a scope limiter. Drop the forced
below-placement on the small star filter (used only here) so the
favourites chip flips with the others instead of sitting alone below.
Strings added to all 9 locales.
* docs(changelog): tooltip unification (#972)
* fix(playlists): Smart Playlist editor theme, toggles, and exclude-all-genres
Replace native sort select with CustomSelect, fix mode-button layout shift,
color-code included vs excluded genres, collapse exclude-all to untagged rule,
and handle empty smart playlists without false "not found".
* docs: CHANGELOG and credits for Smart Playlist editor fix (PR #970)
* chore(credits): drop minor fix entries from PR #958 onward
* fix(ui): shrink-wrap floating player bar instead of full-width strip
The bar used fixed left and right insets, which stretched its background
across the whole main column. Center it with max-content width so only
the pill-shaped controls are painted, and soften the drop shadow.
* docs: note PR #969 in changelog and settings credits
* fix(settings): improve in-page search matching and coverage
Index AudioMuse and individual shortcut rows, tighten fuzzy matching so
junk queries return no hits, and scroll to the parent subsection when a
shortcut result is selected.
* docs: note PR #968 in changelog and settings credits
* fix(player): clamp custom delay input and align preview with armed timer
Reject absurd custom minute values that overflow setTimeout, share one
delay helper between the modal preview and schedule actions, and refresh
countdown ticks when a new deadline is armed.
* docs: note PR #967 in changelog and settings credits
* fix(artist-detail): keep ext-link border visible on hover
Hover used --border-subtle, which on Catppuccin matches --bg-card and
visually erased the rim while only the fill changed. Match btn-surface:
--ctp-surface1 border, --ctp-overlay0 on hover.
* docs(changelog): credit zunoz on Psysonic Discord for PR #966
* fix(playlists): tooltips on Play/Add Songs and song count pluralization
Add data-tooltip to Play and Add Songs in playlist hero; switch
playlists.songs to count-based _one/_other forms (was {{n}} without
i18next plural suffix, breaking spacing and singular).
* fix(playlists): render BPM and optional cols in Suggested Songs rows
PlaylistSuggestions shared column headers with the main tracklist but
its row switch omitted bpm, genre, playCount, and lastPlayed.
* fix(random-mix): honor keyword blocks and scope genre list to library
Keyword filter was gated behind the audiobook exclusion checkbox, so
blocked artists still appeared after Remix. Genre Mix now loads genres
via fetchGenreCatalog (scoped index / library filter) instead of raw
getGenres; show empty state when all tracks are filtered out.
* docs(changelog): credit zunoz on Psysonic Discord for PR #965
* fix(home): scope Because you listened rail to sidebar library
Similar-artist album picks used getArtist without library filtering;
session cache also ignored musicLibraryFilterVersion. Filter picks to
the scoped album set and key cache/reserve by filter version.
* docs(changelog): credit zunoz on Psysonic Discord for PR #964
* test: satisfy SubsonicAlbum required fields in new unit tests
Fix tsc --noEmit CI: songCount and duration are required on SubsonicAlbum.
* fix(composers): drop Navidrome role rows with zero composer albums
Navidrome can list performer-only artists under role=composer with
stats.composer.albumCount 0; filter them out of the Composers catalog
so search no longer surfaces ghost entries like Apollo 440.
* docs(changelog): credit zunoz on Psysonic Discord for PR #963
* fix(artists): inset selection ring on grid cards, stop composer hover clip
Artist multi-select used a positive outline-offset that clipped in the
first grid row and sat outside the card border on hover. Match album
cards with an inset ring; drop composer-card hover translateY that
sheared the top edge in the in-page scrollport.
Reported by zunoz (v1.47.0-rc.3).
* fix(cards): selection ring via inset ::after overlay on WebKitGTK grids
Replace outline-based multi-select rings on album/artist/playlist cards
with the same inset ::after box-shadow pattern used for card focus rings
(card.css) — avoids clipping and the 1px gap vs the inner border on
overflow:hidden tiles in All Albums and related browse grids.
* docs: note browse grid selection ring fix in CHANGELOG (PR #962)
* docs(changelog): credit zunoz on Psysonic Discord for PR #962
* fix(composers): keep role-split names in page-local search
Composers browse already loads the Navidrome role-scoped catalog; scoped
search now filters that list instead of replacing it with generic artist
index/search3 hits that merge split credits into one joined name.
Reported by zunoz on the Psysonic Discord (v1.47.0-rc.3).
* docs: note Composers scoped-search fix in CHANGELOG (PR #961)
* fix(now-playing): split artist links and About the Artist tabs
OpenSubsonic artists[] now drives per-artist navigation on Now Playing
hero and the queue current-track row (matching player bar). About the
Artist loads bio for each performer via tabs when a track has multiple
artist ids; queue Info uses the primary ref for bio/tour fetch.
Reported by zunoz on the Psysonic Discord (v1.47.0-rc.3).
* docs: note Now Playing multi-artist fix in CHANGELOG (PR #960)
* fix(albums): scope All Albums genre filter to selected music library
When the sidebar narrows to one Subsonic library, the genre popover fell back
to server-wide getGenres() instead of the scoped local index catalog. Load
genre options from libraryGetGenreAlbumCounts for library-only scope and
enable the catalog path whenever the index is on and a library is selected.
* docs: credit All Albums genre filter fix (PR #959, report zunoz)
* chore: drop settingsCredits entry for small genre-filter fix
* fix(player): persist volume/repeat in dedicated localStorage key
Player prefs were bundled with the full queue blob in psysonic-player; after
thin-state #872 a large queue can exceed the quota and safeStorage silently
drops every write, so volume and repeat mode stopped surviving restarts.
Move them to psysonic_player_prefs, gate main-blob writes until rehydrate,
and sync volume to the Rust engine on startup.
* fix(player): split queue visibility and Last.fm cache from main persist blob
Move isQueueVisible and lastfmLovedCache to dedicated localStorage keys so
they keep saving when psysonic-player hits the quota on large queues. Include
the new keys in settings backup export.
* docs: note player prefs persist fix in CHANGELOG and credits (PR #958)
* fix(focus): keyboard focus ring no longer clipped by overflow or cover
The global :focus-visible ring used a positive outline-offset, so it was
drawn outside the element and clipped by any ancestor with overflow:hidden
or a scroll container (cards, rails, player bar, queue strip). Draw it inset
instead via shared --focus-ring-* variables (single source). Cards draw the
ring as an overlay above the cover, since a cover's own stacking context
(transform/contain for render stability) would otherwise paint over an inset
outline. Dracula now only sets --focus-ring-color.
* refactor(focus): fold scattered focus-ring overrides onto shared knob
Six components declared their own :focus-visible outline (mostly an exact copy
of the old global ring). Remove the redundant ones so they inherit the global
inset ring; keep the custom-coloured ones (genre pill, playback-delay modal)
but source width/offset from --focus-ring-*; move the because-card ring to the
central card focus ring (it has a cover and needs the lifted treatment).
* fix(settings): round theme accordion inner box to match its section
The theme picker's inner accordion had square corners, so the first (open)
group header ran flush into the rounded Theme card. Give .theme-accordion a
border-radius + overflow:hidden so its top/bottom corners continue the parent
card's rounding, matching the other settings sections.
* fix(search): unify search fields to one look
Live-search, Help and Settings search differed (pill vs rounded-rect, glow vs
plain border-change, mismatched backgrounds). Align them on the canonical input
look: radius-md, ctp-base background, accent border + soft accent-dim focus
glow. Drop the live-search pill radius, and suppress the input's own inset ring
so only the outer cluster glow shows (no double ring).
* fix(tracklist): column picker menu no longer clipped on short lists
The column-visibility dropdown was an absolutely-positioned menu inside the
tracklist, so a short list (e.g. a one-song Favorites view) clipped it via the
ancestor's overflow box. Render the menu in a portal to <body> with fixed
positioning anchored to the trigger (flips above when there's no room below),
following on scroll/resize. Outside-click + Escape close now live in the shared
TracklistColumnPicker (the menu is portalled out of the wrapper, so the old
wrapper-only check would have closed it on every in-menu click). Fixes albums,
playlists and favorites in one shared place. Adds a behaviour test.
* docs(changelog): UI/CSS fixes pass (#954)
* feat(tracks): distinct circular song cards with jump-to-album badge
Single-track cards looked identical to album tiles, so clicking the body
read as album behaviour even though it starts playback. Give them a round
vinyl-style cover (square stays album-only) via a shared .cover-circle
utility, and add a 'To album' badge under the artist that navigates to the
track's album. Card click still plays; the badge is the explicit nav path.
* i18n(tracks): add toAlbum label across 9 locales
* docs(changelog): track-card redesign + to-album badge (#953)
* fix(cover): follow connect-URL flips in library cover backfill
The native cover backfill was configured once with a snapshot of the runtime
connect URL. When a laptop moved off the LAN, the smart endpoint switch flipped
the sticky connect URL to the public address (playback/UI covers rebuild it per
request and followed it), but backfill kept fetching from the now-unreachable
local address — flooding the log with "error sending request" failures.
Make the connect cache observable (notify on effective flips) and have the
backfill hook reconfigure when the resolved URL changes, forcing a pass so the
.fetch-failed backoff from the stale address is cleared and those covers retry
on the reachable endpoint.
* docs(changelog): note cover backfill endpoint-switch fix (PR #952)
* fix(cover): abort stale backfill pass + rerun on connect-URL flip
Frontend reconfigure alone didn't fully cover the boot case: at startup the
first backfill pass starts on the primary (LAN) URL before the reachability
probe resolves, so when the probe flips to public the forced rerun was dropped
by the pass_running guard and the slow LAN pass (every cover timing out) ran to
completion with nothing re-running on the reachable address.
set_session now bumps a session generation; the running pass checks it on every
focus gate and abandons promptly when the URL flips (same server_index_key, new
rest_base_url). try_schedule_full_pass records a rerun when a pass is in flight
and drains it once the abandoned pass returns, so a fresh forced pass runs on
the new address.
* fix(cover-backfill): resolve connect URL per fetch instead of baking it into the queue
The backfill worklist no longer carries a URL. Each cover fetch reads the
current reachable address live from a single worker cell, so a LAN↔public flip
is honoured even by the pass already in flight — its remaining covers download
against the new endpoint without aborting/rebuilding the worklist.
- Drop rest_base_url from CoverBackfillSession; add live base_url cell read in
ensure_one. Remove the session-generation abort machinery (no longer needed).
- New lightweight library_cover_backfill_set_base_url command pushes the URL on
every connect-cache flip; a real change clears the stale .fetch-failed backoff
and runs a forced pass so covers that timed out on the old address retry.
- Split useLibraryCoverBackfill into a configure effect (server/creds/strategy)
and a flip effect that only pushes the URL.
- Keep a single rerun_pending flag for the boot case (flip mid-pass), since the
finished pass re-arms the idle gate.
* perf(analysis): measure tpm over trailing 5s instead of full minute
Mirror the cover cpm change: a 60s rolling average added too much inertia and
flattened real bursts/stalls. Count completions in the trailing 5s window and
extrapolate to per-minute, so analysis tpm reacts promptly and decays to 0
within the window when idle. Retention stays at 60s.
* docs(changelog): note trailing-window throughput rate (PR #948)
* feat(perf): add on-demand (ui) throughput to cover pipeline cpm
Cover cpm previously measured only the native backfill (lib) via the
cover:library-progress done delta. Add a parallel UI series: every completed
on-demand Rust cover ensure (grid / now-playing) records a timestamp, surfaced
as a covers-per-minute rate. Both are exposed in the live cover diag, shown as
separate Backfill (lib) / On-demand (ui) cards in the Monitor tab (each pinnable
to the overlay) and as lib/ui rows in the Cover pipeline overlay block.
* docs(changelog): note cover on-demand (ui) throughput (PR #947)
Add CHANGELOG entry and credits line for the UI cover cpm metric.
* fix(perf): source on-demand (ui) cpm from backend produced-cover count
The JS ensure-queue counter never tracked: produced covers return hit:true
(only misses/errors are hit:false), and ensure-queue dedup/HMR made client
counting unreliable. Count on-demand covers natively in ensure_inner on the
produce path (non-bulk, past the cache-hit gate), expose a cumulative
uiEnsuredTotal in the pipeline stats, and derive the per-minute rate on the
frontend from polled deltas — mirroring the lib backfill series.
* perf(cover): measure cpm over trailing 5s instead of full minute
A 60s rolling average added too much inertia, flattening real bursts and
stalls in both the lib backfill and on-demand (ui) cover throughput. Compute
the rate from the trailing 5s of samples (still extrapolated to per-minute),
so the figure reacts promptly and decays to 0 within the window when idle.
* feat(perf): live runtime logs tab in Performance Probe
Add a Logs tab that streams the backend runtime log ring buffer in-app, so the
stdout/stderr console (unreachable on Windows without exporting) can be read
live. The buffer now tracks a monotonic seq; a new tail_runtime_logs command
returns lines incrementally and get_logging_mode reports the current depth.
The tab has a depth switch (off/normal/debug) mirroring app settings, a line cap
(500-5000), pause/clear, auto-follow, and an ordered comma-separated word filter
where a plain word includes and a -word excludes, applied left to right as
layers (sequence matters).
* docs(changelog): note Performance Probe logs tab (PR #946)
Add CHANGELOG entry and credits line for the live runtime logs tab.
* fix(perf): pin log view position when scrolled up
Auto-scroll keeps the logs tab at the tail, but once the user scrolls up the
view now stays put — the previously-topmost line is re-pinned each tick while
the log keeps appending below for further scrolling. History under the viewport
is no longer trimmed while scrolled up (kept up to the ring-buffer ceiling); the
cap is re-applied when following resumes. Buffer overflow is shown in the status
line instead of an injected marker row.
* fix(perf): scope logs tab to its own internal scroll
The whole probe body scrolled (controls + filter + log) because the log
container sized via height:100%, which WebKitGTK does not resolve against the
flex body. Make the body a flex column with hidden overflow on the Logs tab and
let the log view flex-fill, so depth/keep/pause/clear and the filter stay fixed
while only the log lines scroll.
* feat(perf): cover pipeline throughput (cpm) in performance probe
Mirror the analysis pipeline's tpm for covers. A new coverPerfStore samples the
backfill `done` progress from cover:library-progress events and derives a rolling
one-minute covers-per-minute rate. Surfaced as a live diag in perfLiveStore, a
pinnable "Cover backfill" throughput card in the Monitor tab, and a cpm row in
the Cover pipeline overlay block.
* docs(changelog): note cover pipeline cpm metric (PR #945)
Add CHANGELOG entry and credits line for the cover-pipeline covers-per-minute
throughput metric in the Performance Probe.
* fix(cover): stop per-song cover over-fetch (album/mf-* explosion)
album_has_distinct_disc_covers returned true as soon as two tracks on the same
disc had different cover ids. On Navidrome every song has its own mf-<id>
coverArt, so almost every album was flagged "distinct disc covers" and backfill
warmed one cover per track (~520k for ~170k tracks), filling album/ with mf-*
dirs instead of ~one cover per album.
Treat a release as having distinct disc covers only when each disc has a single
consistent cover that differs across discs (genuine box set); per-song ids now
collapse to one cover per album. Mirror the same fix in the TS
albumHasDistinctDiscCovers used by on-demand warming. Adds regression tests on
both sides.
* docs(changelog): record per-song cover over-fetch fix (PR #944)
Give the album/mf-* over-fetch fix its own [1.47.0] Fixed entry and a
settingsCredits line under PR #944.
* feat(cover): log failed cover downloads with album/artist name
A non-200 (or network-failed) getCoverArt download was swallowed silently. Now
the failure is logged with the resolved album/artist name and the server error,
so a server refusing covers under backfill load (5xx/429/timeouts) is visible.
- cover_resolve: describe_cover_entity() resolves a human label from the local
index (album "Name" — Artist / artist "Name"), best-effort with id fallback.
- cover_cache: log_cover_fetch_failure() in ensure_inner logs on the download
error path; threads optional library_server_id through CoverCacheEnsureArgs so
the name lookup happens only on failure. Backfill logs at normal level,
on-demand misses at debug level.
* fix(cover-backfill): snapshot-diff worklist and live-tunable parallelism
Aggressive cover backfill pegged one tokio worker at ~100% on large,
fully-synced libraries while the download queues stayed empty.
- Take two snapshots once per pass — the DB catalog (single GROUP BY) and
the on-disk cover bucket (one directory walk) — and download the
set-difference. No per-row `stat` syscalls and no re-scan loop; the empty
cache case (heavy backfill) costs zero per-item disk hits.
- Replace the front-loaded enumeration with a producer/consumer pipeline:
the producer streams the catalog in chunks and feeds misses into a bounded
channel; a fixed consumer pool keeps the download/encode pools saturated.
- Make cover backfill parallelism runtime-tunable from the Performance Probe
(threads slider + "Run full pass now"); HTTP download and CPU encode
semaphores resize live. Not surfaced in app settings.
- Add a "nothing changed" idle gate (catalog signature) so a settled pass is
not re-run on every library:sync-idle, mirroring the analysis worker.
- Cancel promptly on switch to lazy: consumers bail on enabled/focus change
and the producer feeds via try_send so a full channel cannot deadlock.
- Drop the per-item recursive disk walk from the ensure hot path.
* fix(cover-backfill): cheap idle gate, settle on 404s, transient retries
Follow-up to the snapshot-diff backfill: stop the periodic CPU spikes and the
89%-plateau wake storm on libraries whose covers can never reach 100%.
- Idle gate is now disk-free: compare only the catalog COUNT(DISTINCT) instead
of walking ~all cover dirs on every sync-idle. "Did the server change?" never
touches the filesystem. Clear-cache commands re-arm the gate (rearm_idle_gate)
since a clear leaves the catalog total unchanged, and the settings UI wakes the
active server after a clear.
- Settle the gate on any completed pass regardless of pending: remaining items
are unfetchable-for-now (404), so the wake/sync-idle storm stops once the
fetchable set is exhausted.
- Stop auto-clearing .fetch-failed markers every pass (it defeated the 30-min
backoff and re-attempted 404s forever). The manual "Run full pass now" sends
force=true to clear them and retry; wake/sync-idle/configure stay opportunistic.
- Rate-limit sync-idle passes (60s cooldown) as defence against chatty syncs.
- Retry cover downloads up to 3x with backoff on transient failures (5xx / 429 /
network), but never on a real 4xx so missing covers don't hammer the server.
* fix(cover-cache): stop re-walking cover dirs from offline & cache menu
The settings cover-cache section polled disk usage + progress every 15s for
every server, each call doing a full recursive walk of the per-server cover
directory. On a fully populated cache this caused periodic CPU spikes whenever
that menu was open.
- mod.rs: add a 10s TTL memo around the per-server cover dir walk
(cached_dir_usage_for_server), shared by cover_cache_stats_server and
library_cover_progress; invalidate on clear (per-server and clear-all).
- CoverCacheStrategySection: recompute on entry only; rely on the
cover:library-progress and cover:cache-cleared events for live updates;
drop the per-cover cover:tier-ready refresh storm; turn the 15s loop into a
5-minute safety net.
* fix(cover-backfill): keep emitting progress during the whole pass
The producer finishes enumerating the worklist long before the consumer pool
finishes downloading it, so progress was only emitted while feeding the channel
— the "offline & cache" menu and overlay then froze through the entire drain
phase. Replace the per-chunk emit with a 3s progress ticker that runs for the
lifetime of the pass and is aborted once the consumers drain (final accurate
emit still happens at settle).
* docs(changelog): record cover-backfill idle-CPU fix (PR #943)
Add [1.47.0] Fixed + Changed entries and a settingsCredits line for the
cover-backfill idle CPU / offline & cache menu work.
* fix(player-stats): exclude paused time from listened duration
While paused, the Rust engine stops feeding active progress ticks to the
listen session, so the tick baseline (`lastTickMs`) stayed frozen at the
pause point. The first progress tick after resume then computed its
wall-clock delta against that stale timestamp and billed the entire
paused span as listened time, inflating Player stats.
Settle the partial segment played up to the pause and mark the session
paused; the first resumed progress tick rebaselines instead of counting
the gap. Wire the freeze into the single `pause()` transport action.
* docs(changelog): note paused-time player-stats fix (#942)
Reorder Added/Changed/Fixed blocks by ascending PR number per team changelog policy — several recent entries had been appended at section tops instead of the bottom.
* fix(perf): skip Performance Probe CPU snapshot poll on Windows
Windows has no Rust CPU/RSS sampler, but the probe still invoked
performance_cpu_snapshot every 2s when the modal or overlay pins were
active. Skip the IPC on unsupported platforms and only poll JS-side
metrics; do not start overlay polling for CPU/memory pins alone.
* fix(analysis): park backfill coordinator until Advanced is configured
#881 started run_coordinator_forever at app init with a 2s sleep even when
disabled, waking tokio on every platform for no work. Park on Notify instead;
wake on configure (enable/disable) and library sync-idle. Long sleeps use
select with wake so sync-idle can interrupt COMPLETED_RECHECK waits.
Investigation branch — not for merge until periodic CPU root cause is confirmed.
* fix(perf): stop probe overlay flicker on live poll updates
Publish CPU samples only after a valid jiffies baseline, skip no-op
snapshot emits, and sync sparkline history atomically in the store.
Overlay uses wall-clock sparkline time and auto-scales low CPU values.
* fix(perf): cut idle Rust CPU from probe scan, cover prefetch, and storage poll
Move performance_cpu_snapshot /proc work to spawn_blocking so tokio
workers are not charged with probe sampling. Stop lazy cover strategy
from running route prefetch disk stats every 1.5s, and slow hot-cache
size refresh on Settings → Storage to 15s.
* fix(perf): stabilize probe sparkline clock between live poll ticks
Track sampleAt separately from updatedAt so CPU rate history and overlay
sparklines only advance on real % changes, not FPS re-renders or RSS-only
poll ticks. Hold CPU sparkline Y scale with a peak ref to avoid scale jumps.
* fix(cover): restore lazy route prefetch without idle disk stats poll
Re-enable lazy cover registry warm-up so cached WebP paths reach
diskSrcCache before cells mount. Skip cover_cache_stats on every 1.5s
tick — drain batches via ensure only, poll full disk usage every 30s
when the registry is idle.
* docs: CHANGELOG and credits for PR #939 idle CPU perf fix
* fix(cover): peek before route prefetch ensure to match main responsiveness
Route prefetch moved batch drain ahead of cover_cache_stats for idle CPU,
which removed the accidental throttle and flooded ensure invoke slots.
Use warmCoverDiskSrcBatch first (cached hits skip ensure), ensure misses
only, and yield while high-priority viewport work is queued.
* feat(artists): scoped live search badge replaces page filter
Move Artists browse text search into the header Live Search with a page
scope badge (Users icon), field-local undo, and double-click/backspace
to clear scope. Block the live-search dropdown while scoped so results
only filter the Artists grid; mobile overlay follows the same rules.
* fix(artists): plain grid for scoped search fixes broken card layout
Route the browse grid through VirtualCardGrid, switch to non-virtual CSS
grid when live search filters the catalog, reset scroll on filter changes,
and skip content-visibility on plain tiles to avoid blank/black cards.
* fix(search): scope badge double-Backspace and single clear control
Require two Backspaces on an empty scoped field after prior text input;
one Backspace still clears the badge when the field was never filled.
Move live-search clear/advanced controls inside the field pill, drop the
extra outer clear button, and use type=text to avoid native search clears.
* fix(search): drop duplicate outer live-search clear button
Keep the native in-field clear on type=search and the original pill layout;
remove only the extra × control outside the search border. Reset dropdown
state when the query is cleared via the native control.
* refactor(search): generic scoped browse query helper, drop dead code
Rename artistsBrowseSearchQuery to scopedBrowseSearchQuery with an
expectedScope argument; wire Artists via useScopedBrowseSearchQuery.
Remove unused liveSearchScoped dropdown helper (scoped mode blocks it).
* feat(search): ghost scope badge and single-click badge remove
After clearing the artists scope on /artists, show a faded ghost chip to
restore page-only search while keeping the global search placeholder.
Active badge removes on one click; tooltips and styles updated.
* feat(search): scoped live search for All Albums and New Releases
Wire albums and newReleases scope badges with debounced album title search
(local index title-only FTS + filtered search3). Plain grid, scroll reset,
and session query restore on album grid browse pages.
* fix(browse): preserve scroll restore after album/artist detail back
Only reset in-page scroll when filter resetKey changes, not when
isScrollRestorePending clears after session restore.
* feat(search): scoped live search for Tracks browse
Wire /tracks to header live search with wide title/artist/album FTS,
hide hero and discovery rails while search is active, and remove the
inline search field from the browse list.
* fix(search): clear header query when leaving scoped browse pages
Prevent global live search from firing on album/detail routes after a
scoped browse query; browse session stashes still restore on back.
* fix(tracks): restore scroll after back from detail during scoped search
Hold stashed song results across fetchSongPage churn, defer leave-stash
teardown past AppShell scroll reset, restore tracks scroll after the list
is ready, and save scroll snapshot when opening artist from song context menu.
* fix(tracks): hide discovery headings during scoped search
Hide the page subtitle and "Browse all tracks" section title when
tracks search is active, matching hero/rails chrome behavior.
* feat(search): scoped live search for Composers browse
Wire /composers to header live search with composers scope badge,
session stash, scroll restore, and plain grid/list during text filter.
Remove the in-page filter input; add i18n and navigation helpers.
* docs: CHANGELOG and credits for scoped browse live search (PR #938)
* feat(genres): genre detail browse via local index with aligned counts
Move genre detail albums/play/shuffle onto the local library index with
Albums-style in-page scroll, session restore, and genre-scoped stash. Unify
genre album totals between the cloud and detail pages via
library_get_genre_album_counts, and fix grouped browse totals to count
distinct albums rather than matching tracks.
* perf(genres): local genre browse with scoped counts cache
Add dedicated Rust genre album pagination and indexes, slice-mode grid
loading, library-filter-aware counts, and a long-lived in-memory catalog
cache invalidated on sync so genre pages avoid repeated full-library SQL.
* fix(genres): restore scroll after album back; play hold-to-shuffle
Pin restore display count in refs so clearing the return stash no longer
reloads the genre grid mid-restore. Load the first SQL page only (60 rows),
use long-press on Play for shuffle, and add genre play tooltips.
* fix(genres): fall back to Subsonic byGenre when local index unavailable
Genre detail album grid now matches All Albums: try library_list_albums_by_genre
first, then getAlbumsByGenre when the index is off, not ready, or errors.
* docs: add CHANGELOG and credits for PR #937
* feat(albums): restore scroll position when returning from album detail
Save in-page scroll and grid depth when opening an album from All Albums,
then on browser back restore filters, preload enough rows, and apply scroll
before revealing the grid to avoid a visible jump from the top.
* feat(albums): smart back navigation and restore browse session on return
Remember the originating route when opening album detail, restore All Albums
filters/scroll on back (including explicit returnTo navigation), hide the grid
until scroll is applied, and fix filters being cleared after albumBrowseRestore
state is stripped from the location.
* feat(search): restore Advanced Search session when returning from album
Stash filters and results when leaving /search/advanced for album detail,
then restore them on back navigation (POP or returnTo with advancedSearchRestore).
* feat(search): restore Advanced Search album row scroll on return from album
Save horizontal scrollLeft when opening an album from Advanced Search and
reapply it via AlbumRow on return; keep main viewport at top. Add snapshot
helpers and session stash fields; extend AlbumRow with restoreScrollLeft.
* feat(search): restore Advanced Search session scroll and artist return path
Save filters, main scroll, and album-row scroll when leaving to album or
artist; restore without flash via hidden-until-ready. Add useNavigateToArtist,
restoreMainViewportScroll helper, and AppShell scroll reset only on pathname change.
* feat(search): speed up Advanced Search back restore and year-only queries
Reveal the page right after sync scroll instead of blocking on full viewport
and album-row restore. Retry local index without the ready gate during sync;
use open-ended byYear params on network fallback, matching All Albums browse.
* feat(search): restore Advanced Search artist row scroll on back
Save leave snapshot when opening artist from ArtistCardLocal, persist
artistRowScrollLeft in session stash, and keep row restore targets in refs
so horizontal scroll survives finishLeaveRestoreUi like vertical scrollTop.
* feat(nav): route mouse back on album/artist detail like UI back
Trap history popstate when returnTo is set and call navigateAlbumDetailBack
so browser/mouse back restores browse/search session the same way as the header button.
* feat(artists): restore browse filters and scroll on back from artist detail
Persist Artists page filters, view settings, and vertical scroll when opening
an artist and returning via UI or mouse back, matching All Albums behavior.
* feat(search): unify quick and advanced search; fix LiveSearch dismiss on Enter
Serve /search and /search/advanced from one page with shared session restore
and scroll snapshot. Reset live search overlay state when navigating to full
search so the dropdown does not linger or reopen.
* feat(tracks): unify with search session and restore scroll on back
Route /tracks through AdvancedSearch with shared leave snapshot, song
browse stash, and main-viewport scroll restore when returning from album
or artist detail. Wait for hero/rails layout before applying scroll.
* refactor(search): rename AdvancedSearch page to SearchBrowsePage
The shared route shell serves /search, /search/advanced, and /tracks;
rename the page component and refresh stale file references in comments.
* feat(albums): restore New Releases and Random Albums on back from detail
Unify album grid leave-restore with surface-scoped session stash, live scroll
snapshot sync, and in-page scroll for Random Albums. Keep the same random
batch when returning from album detail; Refresh fetches anew and scrolls up.
* docs: add CHANGELOG and credits for PR #936
* fix(queue): extend server-pin contract to auto-add paths
The infinite-queue top-up and radio top-up paths in nextAction.ts
read state.queueServerId directly inside their set callbacks. When
the queue was populated without a queue-replacing playTrack (single-
track enqueue from a SongRow + button, AdvancedSearch row, etc),
queueServerId stayed null, seedQueueResolver skipped its store-write
under the if (serverId) guard, and the auto-added refs landed with
an empty server key. Every auto-added row rendered as the resolver
placeholder (… / 0:00) until the next time something happened to
bind the server. Same symptom PR #892 fixed for the manual enqueue
surface, just on the auto-add paths.
Extract ensureQueueServerPinned() from the private helper in
queueMutationActions.ts into playbackServer.ts so it can be shared.
Call it before every set callback that appends or splices refs in
nextAction.ts — appendTracksAndPlayFirst, proactive infinite top-up,
proactive radio top-up. Helper returns the pinned canonical key so
the caller does not need a second store read.
Regression coverage in ensureQueueServerPinned.test.ts: pin on null
+ active server, idempotent on already-bound, empty-string fallback
when no active server, canonical-key return value matches what
toQueueItemRefs expects (not the raw auth uuid). Existing
b1QueueServerIdentity.test.ts continues to cover the manual
enqueue surface unchanged.
* docs(release): CHANGELOG for queue auto-top-up placeholder fix (PR #930)
After #731 the .deb/.rpm/Nix packages follow the session display server,
but AppImage still pins GDK_BACKEND=x11 via its AppRun hook. Document the
asymmetry in the install guidance and complete the #731 changelog entry so
users know which package gives a native-Wayland launch.
* fix(ci): refresh npmDepsHash on channel branch before app-v* tag
Promote workflows push with GITHUB_TOKEN, so nix-npm-deps-hash-sync never
runs on the finalize commit. verify-nix ran after create-release and opened
a PR, leaving app-v* tags pointing at commits with stale npmDepsHash.
Move Nix hash/lock refresh into prepare-nix-sources (before tagging), commit
directly to the channel branch, and build from the prepared commit SHA.
* docs(changelog): note npmDepsHash CI fix (PR #927)
* feat(genre): add paginated songs-by-genre API
Wraps the Subsonic getSongsByGenre endpoint plus a fetchAllSongsByGenre
helper that paginates until exhausted, capped to keep the queue and the
burst of requests bounded for very large genres.
* refactor(playback): extract shared bulk play/shuffle/enqueue helper
A single fetchTracks-driven core (loading flag, empty guard, canonical
shuffleArray) so async detail-page play buttons stop growing divergent
copies. Artist detail now reuses it, dropping its weaker sort-random
shuffle.
* feat(genre): play, shuffle and queue buttons on the genre view
Header buttons load the genre's songs and start ordered or shuffled
playback, or append them to the queue. The slice is bounded to stay
within the queue resolver's cache budget so every row resolves instead
of rendering as a placeholder. Strings added across all nine locales.
* docs(changelog): genre play/shuffle buttons (#926)
* fix(ui): center button label text
.btn was inline-flex with align-items:center but no justify-content, so
buttons wider than their content (min-width / flex:1) rendered the label
left-aligned — visible on the Advanced Search button (min-width 100).
* docs(changelog): centered button label (#925)
* fix(radio): show ICY track in OS media controls (#816)
Internet radio streams through the WebView <audio> element, for which
WebKitGTK registers its own MPRIS player — the one Linux desktops show.
souvlaki metadata pushes were overridden by it, so the OS overlay only
ever showed the app name. Feed the resolved ICY/AzuraCast metadata to
that player via navigator.mediaSession (and mirror to souvlaki), so the
overlay updates per track. Falls back to the station name when a stream
sends no metadata.
* docs(changelog): radio track info in OS media controls (#924)
* ci: make hot-path coverage gates required PR checks
Remove continue-on-error from frontend and Rust coverage jobs now that
the hot-path lists have stabilized; update docs and script headers.
* docs: note hard coverage gates in changelog and credits (PR #921)
* chore: drop credits entry for CI-only PR #921
Contributor credits are for user-visible work, not infra toggles.
* fix(queue): pin queueServerId on first enqueue so refs resolve (thin-state)
Adding a single track from a page that doesn't replace the queue (Advanced
Search row, SongRow + button, SongCard) left queueServerId null whenever
the app had not yet seen a queue-replacing playTrack. seedIncoming then
became a no-op, the new refs landed with an empty server key, and the
queue panel rendered every row as the resolver placeholder ("…" / 0:00)
until the next time something happened to bind the server.
Add an ensureQueueServerPinned step at the entry of every add-to-queue
mutation (enqueue / enqueueAt / playNext / enqueueRadio). It runs after
blockCrossServerEnqueue so a guarded cross-server enqueue still bails
without touching the pin, and after the undo snapshot so undo restores the
pre-pin baseline. Idempotent: no-op when already pinned or when no active
server is available to pin (e.g. unit tests without an authed store).
Regression cluster in b1QueueServerIdentity.test.ts covers enqueue /
enqueueAt / enqueueRadio cache-hit after pin, the no-active-server
fallback, and the already-pinned no-op.
* docs(release): CHANGELOG for queue placeholder fix (PR #892)
* feat(perf): explicit toggle for live thread-group CPU polling
Replace implicit thread-group collection (section open / pin) with a persisted
checkbox so Linux /proc scans run only when the user opts in for diagnosis.
Fix IPC: pass includeThreadGroups (camelCase) so Tauri maps the flag to Rust;
reset the CPU baseline when the option changes so thread % deltas are valid.
* docs: CHANGELOG and credits for PR #891
* fix(perf): gate CHILD_RESCAN_EVERY to Linux/macOS only
Avoid dead_code warning on Windows where perf child-PID rescan is unused.
* fix(cover): per-server cache stats and cover pipeline perf probe
Stop count_cached_cover_ids from borrowing sibling bucket counts so
Settings progress no longer attributes one server's disk cache to another.
Add cover pipeline queue stats (ui ensure queue, ui vs lib HTTP/WebP
semaphores) to Performance Probe overlay, with clearer ui/lib labels.
* fix(browse): stabilize in-page infinite scroll and cap cover memory caches
Extract useInpageScrollSentinel for album grids and song lists so sentinel
reconnects do not spam loadMore during scroll. Harden useAlbumBrowseData with
sync loading refs, tighter root margin, and hasMore termination when dedupe
adds nothing. Pause middle-priority cover work during SQL pagination and bound
diskSrc/resolve/ensure tail maps on long cold-cache sessions.
* refactor(browse): unify in-page infinite scroll hooks and sentinel UI
Extract shared transport (viewport ref, async pagination guards, client slice)
and InpageScrollSentinel so Albums, New Releases, Artists, and song lists
use one pagination pattern instead of duplicated IntersectionObserver wiring.
* fix(browse): prioritize album SQL pagination over cover ensures
Pause the entire webview ensure pump during grid page fetches, resume after
SQL settles, add cover-queue backpressure before load-more, and re-probe the
sentinel when pagination finishes so cold-cache scroll does not stall.
* fix(browse): unblock covers, SQL spawn_blocking, and pagination retry
Pair grid-pagination hold begin/end on stale fetches, resume the ensure pump
after SQL, retry load-more when the cover backlog drains while the sentinel
stays visible, and run album browse SQL on spawn_blocking so Tokio stays
responsive during library_advanced_search.
* feat(browse): All Albums client-slice scroll on local index (Artists-style)
Load the filtered catalog once from SQLite when the library index is ready,
then grow the visible grid with useClientSliceInfiniteScroll instead of
offset SQL pagination per scroll. Network-only servers keep page mode.
* fix(browse): lazy local catalog chunks instead of full 50k SQL fetch
All Albums slice mode now loads 200 albums first, shows the grid immediately,
then appends catalog chunks in the background as the user scrolls. Avoids the
blocking library_advanced_search that hung the app on large libraries.
* fix(browse): keep album covers loading during active grid scroll
Pass high ensure priority and the in-page scroll root to AlbumCard on All
Albums, stop pausing cover traffic for background catalog chunks, and never
trim high-priority ensure jobs from the queue during scroll bursts.
* fix(cover): viewport priority tiers and unstick ensure invoke pump
All Albums uses IO-driven high/middle instead of blanket high; release
only on unmount so scroll-ahead jobs are not dropped on reprioritize.
Ensure queue shares one Rust flight per cover id, attaches duplicate
waiters without consuming invoke slots, and times out wedged calls.
Warm the first viewport slice on large grids; acquire CPU permits before
spawn_blocking in cover_cache to avoid blocking-thread deadlocks.
* fix(cover): wire in-page scroll root on New Releases and Lossless grids
AlbumCard IO uses the same viewport id as VirtualCardGrid so cover
ensure priority tracks visible in-page rows like All Albums.
* fix(browse): lazy local artist catalog in 200-row chunks
Replace runLocalBrowseAllArtists bulk fetch with paginated local-index
chunks so large libraries do not hang on open; preserve text search,
starred, letter filter, and client-slice scroll behavior.
* feat(perf): add RSS and thread CPU groups to Performance Probe
Extend performance_cpu_snapshot with process RSS (psysonic + WebKit
children) and in-process thread CPU breakdown. Classify tokio-rt-worker
and tokio-* workers separately from glib, audio/pipewire, reqwest, and
other misc threads (Linux /proc only).
* feat(perf): redesign Performance Probe with tabs, pins, and overlay layout
Split the probe into Monitor (live metric cards, per-metric overlay pins,
corner and opacity controls) and Toggles (diagnostic tree). Share live
polling via perfLiveStore; label analysis/cover pipeline blocks in the HUD.
* feat(perf): overlay sparklines, macOS CPU/memory, and sync fixes
Add 1-minute pinned-metric sparklines with right-aligned growth and a shared
poll clock. Enable macOS performance snapshots via sysinfo. Fix overlay infinite
loop from unstable history snapshots, bar/sparkline tick jitter, and probe bar
rescale flicker.
* docs: CHANGELOG and credits for PR #890
* perf(probe): scoped CPU poll, adjustable interval, lazy thread groups
Read only psysonic + WebKit children instead of the full process table;
macOS uses sysctl host CPU and refreshes cached child PIDs. Add 0.5–10s
poll slider (default 2s). Collect /proc thread groups only when the
Monitor section is open or a thread metric is pinned.
* feat(perf): three-way overlay mode switch (off / FPS / pinned)
Add Monitor control for overlay visibility: hidden, FPS-only, or pinned
metrics from Monitor. Live CPU poll runs only in pinned mode with live pins.
* fix(cover): sanitize server_index_key on disk so Windows accepts ":port" URLs
`serverIndexKeyFromUrl` (frontend) strips the URL scheme and leaves the rest of
the host as the index key — for a Navidrome instance running on the default
`:4533` port that is `host:4533/...`. On Linux/macOS the `:` is fine as a path
segment; on Windows `CreateDirectory` rejects the whole path with
`ERROR_INVALID_NAME` (os error 123). Result: every `cover_cache_ensure` and
`cover_cache_peek_batch` rejected its promise and the album / now-playing /
mainstage / lightbox surfaces stayed blank. Empirically verified — switching
the active server to a colon-free reverse-proxy URL made the covers load again
without any other change.
Centralize the fix in a new `cover_server_dir(root, key)` helper next to the
existing `cover_entity_relative_dir`: it runs `sanitize_path_segment` on the
server key the same way kind/entity ids are already cleaned, so `host:4533`
becomes `host_4533` and embedded URL paths collapse into one flat bucket
instead of nested directories. Every call site that wants the server bucket —
`cover_dir`, `count_cached_cover_ids`, `dir_usage_for_server`, the clear-server
command, and `clear_cover_fetch_failures` in the backfill worker — now goes
through it.
The on-disk layout changes (no more colons, no more nested URL paths), so bump
`LAYOUT_STAMP` to `canonical-segment-v4`. The existing stamp-mismatch sweep at
startup wipes the legacy buckets — users with a previously-working
(colon-free) layout rebuild the cache lazily as they browse. Library, offline,
and hot-cache data are not touched.
Adds two unit tests covering the sanitization on `cover_server_dir` and the
`cover_dir` passthrough.
Follow-up to #878 (which introduced `cover_cache_layout.rs` with
`sanitize_path_segment` applied only to kind/entity_id).
* chore(windows): silence dead_code warnings in debug taskbar_win build
`lib.rs` gates `taskbar_win::init` on `cfg(not(debug_assertions))` (PR #866 —
debug runs alongside an installed release instance and must not fight it for
the taskbar subclass). The `update_taskbar_icon` command still ships in debug
and early-returns until `init` populates the COM/HWND atomics, so the
init-only helpers (icon HICONs, button IDs, subclass plumbing, `make_buttons`,
`subclass_proc`, `init` itself) all look unused — 14 dead_code warnings on
every Windows debug `cargo build`.
File-level `#![cfg_attr(debug_assertions, allow(dead_code))]` suppresses those
warnings only in the debug profile. Release builds keep the strict dead-code
check, so a real removal would still surface there.
* docs(release): CHANGELOG for windows cover-cache server-key fix (PR #889)
* feat: add long press to shuffle with a wave animation to singnify how long to press
* refactor: long-press shuffle cleanup
Follow-up on the long-press shuffle PR: shared hook/overlay, playback parity,
pointer events, broader surface coverage, locales, and tests.
* docs: credit ImAsra for long-press album shuffle (PR #888)
Add CHANGELOG entry and Settings credits for the hold-to-shuffle play
interaction shipped in Psychotoxical/psysonic#888.
* fix: restore playAlbumShuffled and long-press hook wiring
The follow-up merge dropped playAlbumShuffled and reverted the shared
long-press hook in album play buttons, breaking tsc and vitest on PR #888.
---------
Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
* fix(artist): align top-track covers with album grid cover path
Top tracks now resolve album.id + album.coverArt like AlbumCard, use
the same useAlbumCoverRef/CoverArtImage dense pipeline, and batch-warm
covers on page load instead of a custom sparse resolver.
* docs: CHANGELOG and credits for PR #886 artist top-track covers
* fix(artist): match All Albums cover warm tier and prefetch on detail page
Warm top-track and discography covers at dense grid tier (140px) instead of
32px thumb tier so disk peek hits cached WebP. Register high-priority dense
prefetch like All Albums and ensure top-track cells at high priority so dense
defer-until-visible does not stall visible thumbs.
* fix(artist): satisfy tsc for top-track cover warm helpers
Use optional coverArt access in pushAlbumWarmRow and align album pick
types with topSongAlbumForCover (id + name + coverArt).
* feat(discord): override activity name in member list with track title (configurable template)
The Discord member list and the collapsed Rich Presence card display the
activity's `name` field next to the music icon. Without an override
Discord falls back to the registered application name ("Psysonic"), so
the line reads "Psysonic" while every track plays instead of the actual
track title that comparable players show.
Add a fourth user-configurable template `discordTemplateName` (Settings ->
Integrations -> Discord Rich Presence). The Rust side passes it through
`Activity::new().name(...)` when the rendered template is non-empty; an
empty template falls back to Discord's default application name. Default
template is "{title}".
Tauri boundary: additive optional `nameTemplate` field on the existing
`discord_update_presence` command. Existing call sites that omit it keep
working -- the Rust handler applies the same "{title}" fallback.
* i18n(settings): discord name template label in all locales
Adds the discordTemplateName label across en, de, fr, es, nl, nb, ro,
zh, ru -- shown next to the new "User list line (name)" template input
under Discord Rich Presence.
* docs(release): CHANGELOG and credits for discord name template (PR #885)
* feat(input): opt-in WebKitGTK focus repaint workaround for Linux (#342, #782)
Some Linux users on WebKitGTK 2.50.x report a freeze when clicking
text fields: the field receives focus (right-click paste still works)
but the canvas never redraws until the window is resized. Likely a
layer-flush / composition scheduling bug in 2.50.x's Skia rendering
pipeline.
Off by default. When enabled in Settings -> System -> Behavior, every
input/textarea focus triggers a sync reflow read plus a one-frame
translateZ(0) toggle on the input's parent so WebKit re-evaluates the
layer tree. Side-effect: search-icon siblings flicker briefly on
focus -- accepted trade-off, only paid by users who opt in.
The toggle row is gated on IS_LINUX and sits next to the existing
Linux WebKitGTK options. The effect subscribes to the auth store and
re-attaches the focusin handler whenever the flag flips, so toggling
off cleanly removes the listener.
* i18n(settings): linux input repaint toggle in all locales
Adds linuxWebkitInputForceRepaint label + description across en, de,
fr, es, nl, nb, ro, zh, ru.
* docs(release): CHANGELOG and credits for linux input freeze workaround (PR #884)