* feat(server-scan): plumbing for triggering library scans
Adds `startScan` / `getScanStatus` against the Subsonic API
(`fullScan=true` is Navidrome's extension), a small per-server scan
store, and a global polling hook (2 s cadence) that emits a toast when
each scan finishes. Scans can run on any configured server, including
inactive ones, by reusing `apiForServer`.
UI surfaces follow in the next commit.
* feat(server-scan): expose Quick / Full Scan in switcher + settings cards
Adds a `ServerScanActions` component with two variants (compact for the
server-switcher dropdown, card for the Settings server cards) backed by
the scan store from the previous commit. Full Scan requires a second
click within 3 s to confirm, matching the playlist-delete pattern.
Status slot shows a spinner with running track count while scanning, a
green check when finished, and a red icon on error.
The switcher row is converted from a single button to a flex container
so per-server scan controls don't hijack the server-switch click.
i18n added across all 9 locales.
* fix(server-scan): reorder switcher row to check / name / scan actions
Moves the check / spinner slot from the right edge to the left so the
spinner pop-in on server switch doesn't sit next to the scan icons.
Removes the layout shift that briefly hovered the Quick scan button
when the row re-rendered.
* feat(servers): edit existing server profiles in Settings → Servers
* Pencil-button on each server card opens an inline edit form that
replaces the card (prefilled name / URL / username / password).
* `AddServerForm` reused with an `editingServer` prop — title flips to
"Edit Server", submit label to "Save", magic-string field hidden (the
edit scope is manual fields; magic-string remains an add-time invite
shortcut).
* Edit saves unconditionally — ping runs post-save as a status indicator
(analog to the existing Test button) instead of gating the save. Lets
users update a profile when the server is currently unreachable.
* Translations across all 9 locales (`editServer`, `editServerTitle`).
* fix(servers): submit Add/Edit Server form on Enter
Wrapped the form body in a real <form>, made the submit button
type="submit", marked Cancel as type="button" so Enter no longer
cancels. Add-Mode now also responds to Enter — same flow, consistent
across both modes.
* fix(servers): collapse card action buttons to icon-only on narrow screens
* Quick-Scan / Full-Scan / Test buttons in each server card hide their
text label below 1100px viewport via the .server-card-btn-label class
and a single media query in connection-indicator.css.
* Labels remain accessible via data-tooltip and aria-label so screen
readers + hover both keep working in the collapsed state.
* No content reflow above the breakpoint — pure additive CSS.
* fix(servers): include Use button in icon-only narrow-screen collapse
The Use ("Verwenden") button on inactive server cards lacked the
.server-card-btn-label wrapper, so its text stayed visible at narrow
viewports and pushed Edit/Delete off-screen. Added a Power icon and
wrapped the label so it collapses alongside the other action buttons.
* docs(changelog,credits): #780 server scan + edit
* feat(settings): dim disabled toggle rows + rename Clear → Clear queue
* Settings toggle rows (`.settings-toggle-row`, `.sidebar-customizer-row`) dim
their non-toggle content to 0.6 opacity when the switch is off. Driven by a
single `:has(.toggle-switch input:not(:checked):not(:disabled))` rule so it
applies across every Settings tab without per-row markup. Mutex-disabled
toggles (Crossfade/Gapless) are excluded to avoid stacking with the existing
inline 0.45 dim on their row.
* Queue toolbar "Clear" button now reads "Clear queue" in all 9 locales for
parity with "Shuffle queue" and to distinguish from playlist clear/delete.
Adopted from @kveld9's PR #558 — cherry-picked the spirit, rewrote the dim as
a single global selector instead of three inline-styled customizer rows, and
extended the rename to the Romanian locale that landed after #558 was opened.
Co-Authored-By: kveld9 <179108235+kveld9@users.noreply.github.com>
* docs(changelog,credits): credit @kveld9 for #778
Co-Authored-By: kveld9 <179108235+kveld9@users.noreply.github.com>
---------
Co-authored-by: kveld9 <179108235+kveld9@users.noreply.github.com>
The speaker button in the player bar toggled volume to 0 (or back to
the pre-mute level) but exposed no `data-tooltip`, so hovering it
showed nothing — users couldn't tell what the icon did. Add a tooltip
that switches between "Mute" and "Unmute" with the current state, and
mirror the same string on `aria-label` for screen readers. New
`player.mute` / `player.unmute` keys land across all 9 locales.
* fix(library): show empty-state on Mainstage, Albums, New Releases, Random Albums
When the active library has no albums, Mainstage and the three album-list
pages rendered a fully blank page — every rail and grid was empty but
nothing told the user why. Add a shared `common.libraryEmpty` message
(all 9 locales) and render it in place of the empty grid:
* Home: when no rail has any albums after loading.
* Albums: when no albums and no filter (genre/year/starred/comp) is
active — filtered "no matches" is a separate state and stays as-is.
* New Releases: when no albums and no genre filter is active.
* Random Albums: when no albums after loading.
Pages that already had a dedicated empty-state (Artists, Genres,
Composers, Playlists, Favorites, MostPlayed, LosslessAlbums,
LabelAlbums, InternetRadio) are left alone — their wording is
intentional and per-page.
* docs(changelog): note empty-library states on Mainstage / album lists (#750)
* feat(settings): clock format setting (Auto / 24h / 12h)
Reported on the Psysonic Discord — the Queue side panel's ETA label
and the sleep-timer preview both render via `formatClockTime`, which
just calls `toLocaleTimeString` and so follows the user's system
locale. On en-US that means AM/PM, with no in-app way out.
Add a tri-state **Clock Format** setting under
**Settings → System → App Behavior**:
* `auto` (default) — keep the existing locale-driven behaviour, so
bestehende installs are unaffected on first launch.
* `24h` — force 24-hour wall-clock output everywhere
`formatClockTime` is used.
* `12h` — force AM/PM output.
Wired through `authStore` (`clockFormat`, `setClockFormat`), exposed
via `CustomSelect` in `SystemTab`, and threaded into the two
consumers (`QueueHeader`, `PlaybackDelayModal`) so they re-render on
change. `formatClockTime` itself stays a pure helper — it accepts the
setting as an optional second argument and maps it to `hour12`.
Locale coverage: all nine bundled locales (en, de, es, fr, nl, nb,
ru, zh, ro) get the four new settings strings. Pin tests added for
the `setClockFormat` setter and the `hour12` mapping in
`formatClockTime`.
* docs(changelog): clock format setting + contributors (PR #742)
* fix(home): mainstage row matches "New Releases" sidebar + page label
The Mainstage row whose title chevron links to `/new-releases` was
labelled **Recently Added** (`home.recent`) while the sidebar entry and
the page itself are **New Releases** (`sidebar.newReleases`) — three
labels for the same destination. Reported on the Psysonic Discord.
Reuse `sidebar.newReleases` in both consumers (the row title in
`Home.tsx` and the section label in `HomeCustomizer.tsx`) so the
string lives in exactly one place. The now-orphaned `home.recent` key
is dropped from all nine locale files.
* docs(changelog): mainstage New Releases label fix (PR #741)
* feat(playback): stream buffering UI, ranged M4A tail prefetch, demuxer fix
Defer seekbar/progress until HTTP stream is armed for both legacy and
RangedHttpSource; show buffering overlay on cover art. Add MP4 tail
prefetch and Symphonia isomp4 bounded-mdat/moov-at-EOF probing so
moov-at-end M4A can start without reading the full mdat.
* feat(hot-cache): spill large ranged streams to disk for promote
When a ranged HTTP download completes above the 64 MiB RAM promote cap,
write the existing buffer once to app-data stream-spill/ and register it
for hot-cache promote (rename) and replay via fetch_data. Analysis seeds
from the spill file up to the local-file cap (512 MiB).
* fix(ui): stream buffering — grayscale cover and static clock icon
Desaturate player and queue cover art while isPlaybackBuffering; keep a
non-animated clock overlay for visibility without the spinning animation.
* fix(playback): review follow-up — tests, i18n, spill cleanup, changelog
Clippy and test layout fixes; stream spill orphan cleanup on startup;
buffering flag guard in progress handler; bufferingStream in all player
locales; CHANGELOG and contributor credits for stream/M4A work.
* docs: attribute stream buffering and M4A streaming to PR #737
* test(audio): avoid create_engine in stream spill unit test
CI runners have no audio output device; test spill take/consume via
the Mutex slot only, matching install_stream_completed_spill tests.
* feat(hero): prev / next arrows on the Mainstage featured strip
Adds left and right chevron buttons over the featured-album hero so a
single click flips to the previous / next album. Hitting the 8 px dot
indicators was awkward and often opened the underlying album by
mistake; the arrows give a generous 44 px touch target on each edge.
- New `goPrev` / `goNext` callbacks wrap with modulo, restart the
auto-advance timer on click (same pattern the old dot handler used).
- Buttons live in a new `.hero-nav` flex wrapper with `inset: 0` and
`justify-content: space-between`, so they pin to the hero's left and
right edges regardless of theme padding. `pointer-events: none` on
the wrapper + `auto` on the buttons keeps the rest of the hero
click-through (navigate to album).
- Dots are now decorative spans, not buttons — `pointer-events: none`,
no hover state, no `onClick`. Clicking near a dot used to navigate
to the album because the dot was too small to land on.
- i18n: `previousAlbum` / `nextAlbum` keys in all 9 locales.
Reported by zunoz on Discord.
* docs(changelog): note Mainstage hero prev / next arrows (#735)
* feat(tracklist): play count, last played, and BPM columns (#516)
Adds three opt-in columns to Album / Playlist / Favorites tracklists,
plus the same fields in the Song Info modal. Picks up Navidrome's
existing `playCount` / `played` / `bpm` from the Subsonic response — no
new API calls.
- `SubsonicSong` gains `playCount`, `played`, `bpm` (already populated
by Navidrome's Subsonic API, just unmapped in the TS model).
- `albumTrackListHelpers.COLUMNS`, `PL_COLUMNS` (PlaylistDetail), and
`FAV_COLUMNS` (Favorites) get the three new entries. Genre also added
to the playlist column set for parity with the other two lists.
- Render uses `.track-duration` (12px tabular, centered, muted) for the
numeric stats and `.track-genre` (11px text, muted) for the relative
last-played timestamp via the existing `formatLastSeen` helper.
- Sort logic extended in `playlistDisplayedSongs`,
`useAlbumDetailSort`, and `useFavoritesSongFiltering` for the three
new keys.
- `SongInfoModal` gets matching rows (BPM / Play count / Last played).
- `useTracklistColumns.gridTemplate` / `gridMinWidth` fall back to the
ColDef's `defaultWidth` when a visible column has no saved width
(newly added column on an old prefs blob would otherwise emit
`undefinedpx` and collapse the row layout until reset-to-defaults).
- BPM cells skip the rendering when Navidrome returns 0 (default for
untagged files) — show `—` instead of `0`.
- i18n: `albumDetail.trackPlayCount / trackLastPlayed / trackBpm` and
`songInfo.playCount / lastPlayed / bpm` in all 9 locales.
Suggested by jbigginswyl (#516).
* docs(changelog): tracklist Plays / Last played / BPM columns (#730)
Adds a new sub-section under Settings → Personalisation (Advanced) that
hides individual controls in the player bar: Star rating, Favorite
(heart), Last.fm love, Equalizer, Mini player. Last.fm love still only
renders when a Last.fm session exists; the overflow row in the player
collapses when both Equalizer and Mini player are hidden.
- New `playerBarLayoutStore` (Zustand + persist, items[{id, visible}] +
rehydrate sanitize) following the queueToolbar / playlistLayout
pattern; defaults to all visible.
- New `PlayerBarLayoutCustomizer` reuses the same row + toggle pattern
as the other personalisation customisers.
- Gates threaded through `PlayerTrackInfo` (3 controls), `PlayerBar`
(EQ + Mini buttons), and `PlayerOverflowMenu` (EQ + Mini in the
overflow row, with row-level conditional).
- `PersonalisationTab`: added as the last advanced sub-section so it
only appears when the global Advanced Mode toggle is on.
- Settings search index gets entries for both Playlist page layout and
Player bar (playlist row was missing).
- New i18n keys `settings.playerBar*` in all 9 locales.
Reuses kveld9's design from PR #627; not merged because the locale
split and the Advanced Mode refactor landed afterwards. Credited via
Co-Authored-By trailer + a new line in settingsCredits.ts under the
existing kveld9 entry.
Co-authored-by: Kveld. <kveld912@proton.me>
* feat(offline): show cached albums from all servers in Offline Library
List every offline album regardless of active server; load cover art per
source server and switch before play/enqueue. Sidebar, mobile nav, and
disconnect auto-nav use any cached content; multi-server cards show a label.
* docs(changelog): link offline library PR #719
* chore(pr-719): address review — CHANGELOG in Added, helper tests
Move release note to ## Added per team changelog policy; cover
offlineAlbumCoverArt and ensureServerForOfflineAlbum in unit tests.
Adds a per-element visibility toggle for the playlist detail page (Add
Songs, Import CSV, Download ZIP, Cache Offline, Suggestions) and reworks
the way uncommon options are surfaced: instead of a per-tab collapsible
group, a global "Advanced" toggle in the Settings header reveals all
`advanced` sub-sections across every tab and marks each one with a small
badge. Sets the pattern up so any future advanced option lives in its
natural tab, gated by the same switch.
- New `advancedSettingsEnabled` boolean on `authStore`
(UiAppearance slice, persisted with the rest of the store).
- `SettingsSubSection` gains an `advanced?: boolean` prop. Hidden when
the toggle is off; renders an "Advanced" pill in the header when on.
- Settings header gets a Toggle-Switch next to the search lupe.
- `PersonalisationTab` flattens — Sidebar + Home stay always visible;
Artist sections, Queue Toolbar, and the new Playlist layout get
`advanced` and disappear by default. `PersonalisationAdvancedGroup`
component + CSS removed.
- New `playlistLayoutStore` (Zustand + persist, items[{id,visible}] +
rehydrate sanitize) following the queueToolbarStore pattern.
- `PlaylistHero` and `PlaylistSuggestions` gate the four toolbar buttons
and the suggestions rail on the store directly.
- One-time migration in MainApp on mount: if the user had opened the
old per-tab Advanced group (`psysonic_personalisation_advanced_open
=== 'true'`) OR already customised any of the three sub-sections,
Advanced Mode auto-enables on first launch. Idempotent via a
localStorage flag; legacy key removed afterwards.
- New i18n keys `settings.advancedMode`, `settings.advancedModeTooltip`,
`settings.advancedBadge`, `settings.playlistLayout*` in all 9 locales.
Reuses kveld9's design from PR #556; not merged because the locale split
landed afterwards. Credited under the existing kveld9 entry in
settingsCredits.ts.
Co-authored-by: Kveld. <kveld912@proton.me>
* fix(playback): pin queue streams, cover art, and library links to queue server
When the active server changes while a queue from another server is playing,
keep streams and UI on queueServerId; switch back for artist/album links and
queue or player-bar context menus.
* fix(playback): switch to queue server when opening Now Playing
Ensure active server matches queueServerId before Subsonic fetches on the
Now Playing page, mobile player route, and queue info panel; scope caches
by server id.
* docs(credits): mention Now Playing in PR #717 contribution line
* fix(playback): route scrobble and queue sync to queue server
Address PR review: apiForServer for scrobble/now-playing/savePlayQueue,
clear queueServerId on server removal, mini-player queueServerId sync,
block cross-server enqueue with toast, and regression tests.
Global queue paste opens the preview modal before play; overlay scrollbar,
context-menu suppression, changelog/credits for PR #716 and DanielWTE (#551).
Implement share-link detection in search (track, queue, album, artist,
composer): enqueue tracks/queues without interrupting playback; preview
album/artist/composer without switching the active server; queue preview
modal with scrollable track list. Based on community PR #551.
Co-authored-by: Daniel Wagner <daniel.iuser@icloud.com>
Each src/locales/<lang>.ts (~1800 LOC) becomes a folder src/locales/<lang>/
with one module per i18n namespace (44 each) plus an index.ts barrel that
reassembles <lang>Translation in the original key order.
Mechanical, script-driven split with a JSON round-trip check: every
locale object is byte-identical to its pre-split form. i18n.ts is
unchanged — './locales/<lang>' now resolves to the folder index.
The per-namespace settings.ts files land ~440-460 LOC; a single i18n
namespace is the natural, non-arbitrary split unit for a flat string
table, so they are intentionally left whole.