mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
02e23b5755402d0cfb429dfd77042786d8febc6a
215 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
02e23b5755 |
fix(home): align mainstage row title with "New Releases" (#741)
* 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) |
||
|
|
6ea0acede5 |
feat(playback): stream buffering UI, M4A moov-at-end streaming, hot-cache spill (#737)
* 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. |
||
|
|
31abdc03ef |
feat(hero): prev / next arrows on the Mainstage featured strip (#735)
* 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) |
||
|
|
efd85ffde3 |
feat(tracklist): play count, last played, BPM columns + Song Info rows (#730)
* 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) |
||
|
|
2d27428056 |
feat(settings): player bar layout — per-control visibility toggles (#627) (#721)
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>
|
||
|
|
ea6ac49885 |
feat(offline): Offline Library shows cached albums from all servers (#719)
* 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. |
||
|
|
651a3f276a |
feat(settings): global Advanced Mode toggle + playlist page layout (#556) (#720)
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>
|
||
|
|
45e0e1206f |
fix(playback): pin queue playback to source server when browsing another library (#717)
* 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. |
||
|
|
33b0206ea2 |
feat(search): queue preview on Ctrl+V paste and modal polish
Global queue paste opens the preview modal before play; overlay scrollbar, context-menu suppression, changelog/credits for PR #716 and DanielWTE (#551). |
||
|
|
e7431b94b8 |
feat(search): queue pasted share links from Live Search and mobile search
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> |
||
|
|
0f5ece6d03 |
feat(settings): library card grid max columns in Appearance (4–12, default 6)
Persist libraryGridMaxColumns, wire useCardGridMetrics and card grid layout, add i18n and settings search index; document performance hint in copy. |
||
|
|
2409a1fec8 |
refactor(i18n): split locale files into per-namespace modules (Phase K) (#688)
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. |
||
|
|
7a4bdbc88e |
Feat/romanian translation (#663)
* feat(i18n): Add Romanian translation * feat(i18n): Update Romanian translation to lang file changes * feat(i18n): Add new Romanian translation entries * fix(i18n): add settings.languageRo to remaining locale bundles Romanian was missing from the language picker labels when UI was not en/ro; add endonym-style names per locale (de/fr/nl/nb/ru/es/zh) for consistency. * fix(i18n): use Romanian autonym for settings.languageRo everywhere Match existing language picker convention (e.g. languageDe is Deutsch in every locale bundle). Replaces UI-language translations of Romanian. |
||
|
|
64b33e6941 |
feat: customizable queue toolbar with drag-and-drop reordering and visibility toggles (#534)
* Add drag-and-drop reordering and visibility toggles for queue toolbar * docs(changelog): credit PR #534 (queue toolbar customization) Adds the v1.46.0 CHANGELOG entry and a new bullet on kveld9's contributors block in Settings → System. --------- Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
308eb36f05 |
feat(analysis): re-analyze waveform when clearing loudness cache
Add analysis_delete_waveform_for_track, invoke it from loudness reseed, clear waveformBins in the UI, and extend queue strings for tooltips/toast. |
||
|
|
a702a5dd5b |
feat(orbit): in-app diagnostics popover with copyable event log (#524)
* feat(orbit): in-app diagnostics popover with copyable event log Multiple users on Discord report Orbit guests stopping after the first song with no errors anywhere — Settings → Debug → Export Logs is too buried for non-technical reporters, and the relevant code branches have no logging at all (silent fail). This adds a one-click "Copy log" path right inside the Orbit session bar. The new Activity-icon button next to Help opens a popover with: - Live mini-display: role, host vs. guest track id + position, drift, age of the host's last state write — all updating once a second. - Scrolling event log textarea fed by an in-memory ring (200 events). - Copy + Clear buttons. Copy formats `[ISO] [scope] body` lines and drops them on the clipboard — paste straight into a Discord report. Instrumentation lands at the previously-silent decision points: - Guest pull tick: full snapshot of host vs. guest state on every read. - Each branch of the divergence detection in `useOrbitGuest.ts` logs which path it took and why (initial / track-change-followed / track-change-diverged / play-pause-flip), making the "stuck after first song" symptom diagnosable from the buffer alone. - Host pushes log track id, isPlaying, queue length, guest count. Events are also bridged to the existing `frontend_debug_log` Tauri command when Settings → Logging is on Debug, so power users still get the same data in `psysonic-logs-*.log` for offline triage. i18n: full `orbit.diag.*` namespace in all eight locales. EN + DE are native; ES / FR / NB / NL / RU / ZH are first-pass and may want a polish from native speakers later. * docs(changelog): add orbit diagnostics popover entry |
||
|
|
8b781a848d |
refactor(i18n): show language names as endonyms in picker (#514)
* refactor(i18n): show language names as endonyms in picker * docs(changelog): add language-endonyms entry for #514 |
||
|
|
c982362884 |
fix(i18n): OpenDyslexic font supports Cyrillic (#513)
* fix(i18n): OpenDyslexic font supports Cyrillic The OpenDyslexic subtitle in the font picker stated "no RU/ZH support", but the bundled `@fontsource/opendyslexic` 5.x ships Cyrillic glyphs and Russian renders correctly — verified empirically. Only Chinese (CJK) actually falls back to the system font. Updated the subtitle string in all 8 locales accordingly; the Russian locale itself no longer claims unsupported-self. * docs(changelog): correct OpenDyslexic locale-coverage claim (#513) The #507 entry in the unreleased v1.46.0 block claimed `Latin + Latin-extended only` and listed Cyrillic among the system-font fallbacks. Cyrillic glyphs ship in @fontsource/opendyslexic 5.x and render correctly — only Chinese (CJK) actually falls back. Updated the bullet in place to match the corrected subtitle hints from this PR. |
||
|
|
f520f7951a |
feat(settings): OpenDyslexic font option for dyslexic readers (#507)
* feat(settings): OpenDyslexic font option for dyslexic readers Next step on the accessibility track. The first pass was on the colour side — WCAG contrast audits across every theme and dedicated colour- vision-deficiency variants for the protanopia / deuteranopia / tritan- opia palettes. Typography is the other axis: some users with dyslexia find a font with a heavier weighted baseline and asymmetric glyph shapes (b/d, p/q never mirror, italic forms differentiated rather than slanted-regular) easier to track than a typical sans. Adds OpenDyslexic to the existing Fontsource font picker. SIL OFL licensed, freely redistributable, and the de-facto open-source standard for this use case. Non-variable axis, ships as four discrete weight/style files (regular, bold, italic, bold-italic) — the Settings picker grew an optional `hint` field on font entries so this one row can carry a "dyslexia-friendly · no RU/ZH support" subtitle without bloating the other 14 entries. Latin + Latin-extended only. Cyrillic and CJK locales (RU, ZH) fall back to the system font when this is selected; the subtitle calls out that limitation upfront. i18n: hint string in all 8 locales (settings.fontHintOpenDyslexic). Accessibility is intentional product positioning here — it's an underserved corner of the Subsonic-client ecosystem. * chore(nix): sync npmDepsHash with package-lock.json * docs: changelog entry for PR #507 Logs the OpenDyslexic font option in v1.46.0 "## Added". * docs(settings): contributor entry for PR #507 Adds the OpenDyslexic accessibility bullet to Psychotoxical's contributions list. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
bd56177e2c |
feat(home): Lossless Albums rail + dedicated page + sidebar nav (#506)
* wip(home): Lossless rail + dedicated /lossless-albums page Rail under Home > mostPlayed and a dedicated infinite-scroll page that list albums whose tracks are tagged in lossless containers. Walks Navidrome's native /api/song?_sort=bit_depth&_order=DESC, dedupes by albumId on the way down, stops when the song stream crosses into lossy (bitDepth==0) or the server runs out of rows. _filters has no operators on quality columns, so a sort + walk is the only path; equality on sample_rate / bit_depth probes returned empty (verified). The page paginates through the song-cursor with an in-flight ref so overlapping IntersectionObserver fires don't double-add albums, plus a cancelled flag so React StrictMode's double-mount doesn't apply two parallel result sets in dev. Suffix allowlist excludes ambiguous wrappers — m4a/m4b can be ALAC *or* AAC and Navidrome's response carries an empty codec field, so we can't tell them apart; same story for wma. Allowlist is flac, wav, aiff/aif, dsf/dff, ape, wv, shn, tta — containers that are only lossless. ALAC-in-m4a setups will miss out, acceptable trade-off without a reliable codec field. Status: WIP. Settings home-customizer label and en.ts strings landed, no other locales yet, no quality badge on AlbumCards across the rest of the app, no CHANGELOG. Branch was 'feat/home-hires-rail' during the earlier hi-res-only iteration before the lossless broadening. * wip(home): Lossless page header parity + streaming + sidebar nav Page header now mirrors All Albums: selection mode with three action buttons (Enqueue Selected, Add Offline, Download ZIPs) wired to the same handlers Albums.tsx uses, selection-counter title swap, and the perfFlags.disableMainstageStickyHeader respect path. Filters were intentionally skipped — the rail is sorted by bit_depth, mixing client- side filters with server-driven pagination would produce gaps. Loading feels noticeably faster: ndListLosslessAlbumsPage takes an optional onProgress callback that fires once per internal fetch with the entries discovered in that fetch, so the page can stream new albums into state instead of waiting on the whole loadMore. Page-side budget dropped from 5×200 to 2×100 songs per loadMore (~1 MB worst case vs 5 MB before), since the rail's catch-em-all pass is wrong for infinite-scroll UX. Subtitle under the page title primes the user that this is slower than other album pages because Psysonic walks the song catalog by quality (Navidrome ignores _fields, so per-song responses ship with lyrics + tags + participants whether we want them or not). Sidebar nav entry registered under 'losslessAlbums' with a Gem icon, defaults to visible:false (matches composers / folderBrowser / deviceSync — niche browsing modes). Existing users get the entry appended at the end of their persisted sidebar list automatically via the onRehydrateStorage merge that sidebarStore already runs for new DEFAULT_SIDEBAR_ITEMS. i18n: full coverage across all 8 locales for sidebar.losslessAlbums, home.losslessAlbums, losslessAlbums.empty, losslessAlbums.unsupported and the new losslessAlbums.slowFetchHint subtitle. ru/zh are machine-translation quality, flagged for a polish pass. * feat(home): default Lossless sidebar entry to visible Flips the DEFAULT_SIDEBAR_ITEMS entry for `losslessAlbums` from false to true. Existing installs keep whatever the user has in persisted storage; fresh installs see the entry in the sidebar from the start. Earlier wip commit defaulted it off (matched composers / folderBrowser / deviceSync as a niche browse mode), but the rail + page do show something useful for any library with at least one FLAC/WAV/etc. album, so off-by-default just hid the feature. * docs: changelog entry for PR #506 Logs the Lossless Albums rail + page + sidebar entry in v1.46.0 "## Added". * docs(settings): contributor entry for PR #506 Adds the Lossless Albums rail/page bullet to Psychotoxical's contributions list. |
||
|
|
ddb1f29af9 |
refactor(settings): remove redundant Animations 3-state setting (#495)
* refactor(settings): remove redundant Animations 3-state setting under Seekbar Style The `animationMode` setting (Full / Reduced / Static) duplicated work the perf-flag system and OS-level reduced-motion preference already covered: - `perfFlags.disableMarqueeScroll` already kills marquee scrolling on demand, replacing what `static` mode used to gate. - The `data-perf-disable-animations` html-level switch already strips every `*` animation, replacing what `static` mode used to do globally. - `@media (prefers-reduced-motion: reduce)` honours the OS setting for every user that asked for it via system preferences. - The 30 fps cap that `reduced` mode applied to the seekbar wave was better served by per-feature perf toggles cucadmuh added later. Removed: - `AnimationMode` type, `animationMode` field + setter from auth store. - Settings UI block (3 buttons + hint text) under Appearance > Seekbar Style. - `animationMode === 'static'` short-circuit in WaveformSeek's rAF effect; `isReduced` skip-every-other-frame logic; `static`-checks in `drawNow` / `needsDirectDraw`. - `animationMode !== 'static'` guard and `data-anim-mode` attribute in MarqueeText. - `[data-anim-mode="static"]` and `[data-anim-mode="reduced"]` rules in layout.css. - Seven i18n keys (animationMode + 6 variants) across all eight locales. Migration: the persist layer strips `animationMode` (and the legacy `reducedAnimations` boolean predecessor) so anyone who had `'reduced'` or `'static'` selected silently lands on the former `'full'` path on first launch after upgrade. No user-facing prompt — the missing setting just stops existing. cucadmuh's PR #472 (FPS overlay), #476 (preview-freeze main seekbar, sleep-recovery hooks, card-hover removal) and #486 (interpolation anchor reset on resume) are all preserved untouched — they live in separate effects / files and were not driven by `animationMode`. * docs(changelog): add Removed section for animationMode setting (PR #495) * docs(changelog): refine animationMode removal rationale (drop prefers-reduced-motion overstatement) |
||
|
|
d1ff2fab51 |
feat(home): Because you listened recommendation rail (#489)
* feat(home): "Because you listened" recommendation rail New Home rail (under Recently Added, default on, toggleable in Settings → Personalisation → Home Page) that surfaces 3 albums from artists similar to one of your top-played artists. Anchor rotates per Home mount so a different top-artist seeds the recommendations each visit; within each anchor, both the similar-artist subset and the chosen album per artist are randomised, so the same anchor returns different picks on subsequent visits. Card layout matches the regular Album cards' surface (--bg-card with accent-tinted border + 1px inset top highlight) and gets the same Play / Enqueue hover overlay buttons. Cover and meta scale via CSS only — no infinite animations, no filter/blur/transform, no compositing layers. Grid wraps below 3-up at <400px card width instead of shrinking. API budget: one getArtistInfo2 + 6 parallel getArtist calls per Home mount, both reusing the existing mostPlayed payload to derive the anchor pool (no extra API call to find top artists). All 8 locales seeded. * fix(home): ru plurals + per-server anchor + narrower card grid - ru: add _few / _many for becauseYouLikeTracks (CLDR Russian needs 4 forms — 3 треков was wrong, now 3 трека). - Anchor rotation memory is now per-server. The localStorage key becomes psysonic_because_anchor:<serverId>; switching servers no longer aliases server A's rotation onto server B's pool. - because-card grid minmax(400px, 1fr) -> minmax(340px, 1fr) so two cards fit side by side at typical sidebar-expanded widths instead of collapsing to a single card per row. * docs: CHANGELOG + Contributors entry for Because-you-listened rail (PR #489) * style(home): blurred cover backdrop + centred layout for Because-cards - Each Because-card renders the album cover as a blurred, low-opacity full-bleed background layer behind the existing cover thumb and text. Resolved through useCachedUrl so the cache layer feeds it (same key as the thumbnail) instead of a fresh salted URL on every render. - Card content (cover thumb + text block) now centred horizontally and vertically within the card; the meta line lives in a small pill that sits centred under the artist row. - Text contrast halo and meta-pill background are theme-aware via color-mix on var(--bg-card) / var(--text-primary), so the same rules read on dark and light themes (was hard-coded rgba black before and smudged the type on Latte / Nord Snowstorm). |
||
|
|
59744601d4 |
feat(composer): Browse by Composer page (issue #465) (#487)
* feat(composer): Browse by Composer page (issue #465) New library section listing every artist credited as composer on at least one track, with a detail page showing all works they're credited on in that role. Targeted at classical-music libraries where the "recording artist" tag carries the orchestra and the "composer" tag carries Bach / Mozart / Chopin. Hits Navidrome's native /api/artist?_filters={"role":"composer"} for the listing and /api/album?_filters={"role_composer_id":"…"} for the works grid — Subsonic getArtist only follows AlbumArtist relations and returns 0 albums for composer-only credits, so the native API is the only path that works. Requires Navidrome 0.55+ (uses library_artist.stats role aggregation); on older / pure-Subsonic servers the page shows a one-line capability banner. - Two new Tauri commands: nd_list_artists_by_role + nd_list_albums_by_artist_role, generic over participant role so conductor / lyricist / arranger pages are trivial to add later. - Composers grid: text-only compact tiles (name + participation count pulled from stats[role].albumCount). No avatars — composer libraries carry no useful imagery and the listing endpoint exposes no image URLs anyway. - ComposerDetail: hero with Last.fm bio (via getArtistInfo2) plus the full work grid, with a graceful fallback when the artist has no external info synced. - Sidebar entry default off (Feather icon) — opt-in for the niche classical use case. - nd_retry backoffs widened from [500] to [300, 800, 1800] — helps every nd_* call survive intermittent TLS-handshake-EOF errors that some reverse-proxy setups produce when keep-alive pools churn. - Distinguishes "server can't do this" (HTTP 400/404/422/501) from transient errors so the capability banner only fires when the server actually rejects the request shape; everything else gets a retry button. - i18n in all 8 supported locales. * fix(composer): address review feedback on detail page + role queries - Re-fetch ComposerDetail when music-library scope changes; previously the album grid stayed stale until navigation while the list refreshed. - Thread library_id through nd_list_artists_by_role and nd_list_albums_by_artist_role so role queries respect the active Navidrome library, matching the Subsonic musicFolderId already piped through libraryFilterParams(). - Fix CachedImage cache-key mismatch on ComposerDetail: a Last.fm header image was stored under the Subsonic cover-art key, aliasing cache entries and risking cross-source pollution. - Consolidate the two contradictory composer-imagery comments in Composers.tsx into a single accurate one (the older one referenced an Images toggle that was never implemented). - Align openLink toast duration with ArtistDetail (1500ms -> 2500ms). * fix(composer): keep bio across scope changes, add share, degrade gracefully Three remaining items from the latest review pass on the composer flow. 1. Bio survives a music-library scope change. The previous fix added musicLibraryFilterVersion to the load effect, but that effect also did setInfo(null) while the getArtistInfo effect still depended on [id] alone — so a scope bump on the open page wiped the bio without re-fetching it. Move the info reset into the bio effect (keyed on id) and out of the load effect: the album grid still refreshes on scope change; the Last.fm header image and biography survive untouched, since both are library-independent. 2. Composers join the share pipeline as a first-class entity kind. Extend EntityShareKind with 'composer' (and isEntityKind), branch applySharePastePayload to validate via getArtist (same id pool) and navigate to /composer/:id, and wire a Share button into ComposerDetail. A pasted composer link now opens the composer view instead of the artist view, matching what was copied. i18n added in all 8 locales (sharePaste.composerUnavailable, openedComposer; composerDetail.shareComposer, unknownComposer). 3. Partial server failure no longer hides the works. If getArtist rejects but ndListAlbumsByArtistRole succeeds, the page used to show full "not found" despite having data to display. Switch the not-found gate to require both empty (`!artist && !albums`) and render a degraded header (placeholder name, no Wikipedia / favourite / share / Last.fm image) when only metadata is missing. * fix(composer): right-click share copies a composer link, not an artist link The context menu opened from a composer card / row uses type='artist' because every composer-action (radio, favourite, rating, add-to-playlist) is identical to the artist counterpart — they share an id space and a backend representation. Sharing was the one exception: the "Share Link" entry produced a 'psysonic2-' string with k='artist', so a paste opened /artist/:id even though the user came from /composers. Add an optional shareKindOverride to openContextMenu (default: undefined, preserves existing behaviour) and have the artist-typed branch consult it when calling copyShareLink. Composers.tsx now passes 'composer' on both right-click sites; nothing else changes downstream because the override only affects the share kind. * polish(composer): show Last.fm avatar even without server metadata Two minor follow-ups from the latest review. - ComposerDetail: drop the `&& artist` guard on the header-avatar render path. info?.largeImageUrl can resolve through getArtistInfo(id) without ever needing the SubsonicArtist record, so the previous gate hid a perfectly good Last.fm portrait whenever getArtist failed but the bio fetch succeeded. Replace artist.name with displayName so the alt / aria-label degrade to the localised "Composer" placeholder instead of empty strings. - copyEntityShareLink: doc comment now mentions composer alongside track / album / artist. * fix(composer): derive Last.fm cache key from route id, not from artist record Follow-up to the previous polish: the avatar render path no longer requires `artist` to be populated, but the cache-key gate still did. So when getArtist failed but getArtistInfo returned a Last.fm portrait, the key fell through to coverKey — which is empty without an artist record, re-creating the very aliasing bug the earlier Subsonic-vs-Last.fm fix was meant to close. Switch the Last.fm branch to the route id (same id namespace as the SubsonicArtist record), so the key stays stable whenever Last.fm art is shown, independent of getArtist succeeding. * docs: CHANGELOG + Contributors entry for composer browsing (PR #487) |
||
|
|
e215694301 |
feat(help): rewrite Help page — trimmed Q/A, 10 sections, live search (#485)
* feat(help): rewrite English Q/A entries — trim, consolidate, refresh The Help page had grown to ~50 entries over time, with several that the UI itself answers (double-click to play, click the cover for fullscreen, click the repeat button to cycle, …) and other groups that were better folded into a single answer (rating + Skip-to-1★, Internet Radio basics + supported formats, Device Sync overview + filename template + cross-platform behaviour, …). This pass: - drops obviously-redundant entries (q4, q7, q8, q11, q22, q24, q25, and the trivial Settings → X pointers q12, q13, q15, q32, q42, q43) - consolidates the natural groupings (q5+q31, q37+q38+q39, q53+q54+q55, q34+q35+q47, q26+q27+q28, q12+q41, q56+q57) - adds entries for features that did not exist yet when the previous Q/A list was written: Orbit (Listen Together), Magic Strings sharing, LUFS Smart Loudness Normalization, Mini Player + Floating Player Bar, Smart Playlists, Track Preview, Search and Advanced Search, Statistics, Tracks library hub, Genre tag-cloud browser, Discord Rich Presence, Bandsintown tour dates, Multi-select + Shift-click range selection, Sidebar / Home / Artist Page customization, Sleep Timer, Open Source Licenses Result: 45 focused entries across 10 sections (Getting Started / Playback & Queue / Audio Tools / Library & Discovery / Lyrics / Sharing & Social / Personalization / Power User / Offline & Sync / Integrations & Troubleshooting), each one answering something the UI does not already answer at a glance. * feat(help): restructure into 10 sections with live search Page is now organised into ten focused sections (Getting Started, Playback & Queue, Audio Tools, Library & Discovery, Lyrics, Sharing & Social, Personalization, Power User, Offline & Sync, Integrations & Troubleshooting) each rendered as its own column-friendly accordion group with a Lucide icon. A search input lives in the page header. Typing filters every Q+A pair across all sections by case-insensitive substring; sections that end up empty are hidden, matched items are auto-expanded so the user sees the answer without having to click each result, and a "no results" empty state appears when the query matches nothing. Clearing the input restores the manual accordion behaviour. An × button next to the input clears the query in one click. CSS uses dedicated `.help-search`, `.help-search-icon`, `.help-search-input`, `.help-search-clear` rules instead of leaning on the global `.input` class — the latter brought its own focus-ring styles that doubled with the wrapper border. Focus state highlights the wrapper border to `--accent` via `:focus-within`. * i18n(help): translate the new Help page to 7 locales Updates de, fr, nl, zh, nb, ru, es to match the new English Q/A structure (45 entries across 10 sections, plus the live-search labels: title, searchPlaceholder, noResults). DE / FR / NL / NB / ES were translated directly. RU and ZH are structurally correct but written at machine-translation quality; both could use a pass from the original locale maintainers (@cucadmuh for RU, @jiezhuo for ZH) — none of the wording is load-bearing for the i18n keys, so the page renders correctly today and refinements can land as follow-up touch-ups without coupling. * docs: changelog + contributors for PR #485 Adds the v1.46.0 "Changed" entry and the Psychotoxical contributors line for the Help page rewrite. |
||
|
|
dc35f53674 |
feat(artist): group albums by release type on artist page (#471)
* feat(artist): group albums by release type on artist page Uses the releaseType field to group albums/releases into sections like Albums, Compilation, Live, etc. If there's no release type it falls back to normal view * feat(artist): i18n release-type group labels * fix(artist): deterministic release-type group order * refactor(artist): replace inline styles with CSS classes * i18n(artist): translate release-type labels in remaining 7 locales Sayykii's `releaseTypes` namespace was added to en.ts only. Fills in de, fr, nl, zh, nb, ru, es with the same 8 keys (album, ep, single, compilation, live, soundtrack, remix, other) so users on non-English UIs see translated section headers on the artist page instead of the raw title-cased fallback. * docs: changelog + contributors for PR #471 Adds the v1.46.0 "Added" entry and bumps Sayykii's contributors line for the artist-page release-type grouping. --------- Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
8692e50603 |
feat(settings): Open Source Licenses section in System tab (#477)
* feat(licenses): tooling and initial data generation Adds the maintainer-only generator that produces src/data/licenses.json: - src-tauri/about.toml + about.hbs: cargo-about config + handlebars template for the Rust-side license enumeration - scripts/generate-licenses.mjs: orchestrator that runs cargo-about and license-checker-rseidelsohn (via npx, no devDep), merges the outputs into a single per-crate JSON with full license texts, and writes the result to src/data/licenses.json The script is invoked directly with `node scripts/generate-licenses.mjs` — no npm script wrapper on purpose, since adding one to package.json would trigger the nix-npm-deps-hash-sync workflow on every push. Initial generation covers 575 cargo crates + 71 npm packages (646 entries total, all with full license text bundled, ~1.4 MB JSON). * feat(licenses): Settings panel UI Adds a new Open Source Licenses section under Settings → System, sitting below Contributors. Components: - LicensesPanel.tsx: search input, curated highlight block of ~10 key dependencies (Tauri, React, rodio, symphonia, etc.), TanStack-Virtual list of all 600+ entries - LicenseTextModal.tsx: full-screen-ish modal showing the bundled license text plus name/version/license-id badges + repository link - licensesData.ts: lazy dynamic-import loader (Vite emits the JSON as a separate chunk, so the heavy ~1.4 MB payload is only loaded when the user actually opens the panel — no runtime fetch, the data is fixed into the build artifact) The panel registers itself in the Settings in-page search index under the System tab. * feat(licenses): i18n in 8 locales Adds the `licenses` namespace (title, intro, highlights, search placeholder, no-results, loading / load error, no-license-text, view-source, total line, generated-at) across en, de, fr, nl, zh, nb, ru, es. License names themselves (MIT, Apache-2.0, GPL-3.0, …) stay universal and are rendered as-is. * docs(release): document licenses regeneration step Adds a Step A.3 to the release SOP describing the maintainer-only `node scripts/generate-licenses.mjs` workflow, the cargo-about prerequisite, and explicitly notes why no npm script wrapper exists (would trigger the nix-npm-deps-hash-sync workflow). |
||
|
|
d33abf565c |
feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch (#466)
* feat(ui): StarFilterButton component + common i18n keys Reusable toggle button for "favorites only" filtering. Three size variants for different toolbar contexts: - default: icon + label (Albums-style) - compact: icon-only with 0.5rem padding (Artists view-mode buttons) - small: icon + label at 12px / 4×14 padding (AdvancedSearch tabs) Adds common.favorites + favoritesTooltipOff/On in all 8 locales. * feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch Client-side filter using the existing useMemo pipelines on each page. Reads starred state from item.starred + playerStore.starredOverrides (O(1) Map lookup, picks up live star toggles without refetch). - Albums: toolbar button (default size) next to compilation filter. - Artists: toolbar button (compact / icon-only) before the Images toggle. - AdvancedSearch: toolbar button (small) next to the result-type tabs; filters all three result categories (artists / albums / songs) and updates the count badges accordingly. Filter state is ephemeral per-page (not persisted) so users don't get surprised by hidden items after a restart. Zero extra server calls. * docs(contributors): credit + changelog entry for #466 |
||
|
|
0fab2849e5 |
feat(queue): preserve Play Next order toggle (#464)
* feat(queue): add preservePlayNextOrder setting + playNext store action - New Track.playNextAdded flag (analogous to autoAdded / radioAdded). Stale flags behind queueIndex are harmless — only forward streak scan. - New playerStore action playNext(tracks): tags incoming tracks and delegates to enqueueAt for unified undo + server sync. - New authStore boolean preservePlayNextOrder (default false). When on, playNext appends behind the existing Play-Next streak (Spotify-style) instead of inserting directly after the current track. * refactor(context-menu): centralise Play Next; add Settings toggle + i18n - Replace 3 inline splice/enqueueAt call sites in ContextMenu with the new playNext action. Side-benefit: the single-song path now goes through enqueueAt and gets undo + queue sync (previously missing). - Settings → Audio → Playback: new toggle below Gapless. - 8 locales: preservePlayNextOrder + preservePlayNextOrderDesc. * docs(contributors): credit + changelog entry for #464 |
||
|
|
e1f2cb4c37 |
feat(discord): add server cover art source (#462)
* feat(discord): add server cover art source
The old Apple Music toggle is replaced with a radio selector which let's you choose
between Apple Music, Server and no image.
It's important to note that the server needs to be publicly accessible.
Translations have been added for all locales
* feat(discord): toggle UI for cover source and tightened defaults
- Replace cover-source radio buttons with three indented sub-toggles
(none / server / apple) under Discord Rich Presence; mutex via
setDiscordCoverSource — turning one on flips the others off.
- Default discordCoverSource is now 'server' for fresh installs
(opt-in friendly: own server, no third-party data leak). Existing
users keep their state via the legacy bool migration.
- Tighten template defaults: details {artist}, state {title}, largeText
unchanged. Existing users keep their persisted values.
* docs(contributors): credit Sayykii + changelog entry for #462
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
|
||
|
|
dc7a785f94 |
feat(ui): bulk entity ratings, Random Albums multi-select, album New badge (#446)
Add star rating rows to multi-artist and multi-album context menus so the selection shares one rating control (mixed ratings show empty until set; keyboard navigation supported). Pass selectedAlbums into AlbumCard on Random Albums so multi-select context menu works. Add i18n aria labels for bulk rating controls. Move the New album badge to the top-right of the cover and stack it with the offline badge to avoid overlap. |
||
|
|
3b4d54431b |
feat(random-mix): playlist size selector + filter panel layout cleanup (#445)
* feat(random-mix): playlist size selector + filter panel layout cleanup Adds a 5-button playlist-size picker (50/75/100/125/150) at the top of the Random Mix filter panel, persisted via authStore. Clicking a size immediately reruns the current mix (genre-scoped or All Songs) at the new size — no second click on Remix needed. Filter panel layout cleaned up: - Two sub-sections "MIX SETTINGS" and "EXCLUSIONS" with a divider between them so the panel reads cleanly with the new size row. - Larger panel-level headers (FILTERS / GENRE MIX) so the hierarchy panel-title > sub-section is visually unambiguous. - Italic muted note under MIX SETTINGS calling out that large mix sizes may return fewer unique tracks if the server's random pool runs short — sets honest expectations instead of users wondering why a 150 request returned ~126. fetchRandomMixSongsUntilFull now scales batch size, max-batch ceiling and dup-streak budget with target size; when no Settings-level mix filter is active, the first call asks for the full target so a 150 mix can finish in a single round-trip on most libraries. The loop falls through to top up with deduped follow-up calls if the server returns fewer than requested. * docs(changelog): add #445 Random Mix playlist size selector entry * chore(credits): add #445 to Psychotoxical contributions |
||
|
|
1799e90e04 |
feat(tracks): Highly Rated rail + per-card star display (#443)
* feat(tracks): Highly Rated rail + per-card star display Adds a new SongRail above the Random Pick on the Tracks page that surfaces the user's highly-rated tracks (sorted by rating DESC). Auto-hides on non-Navidrome servers and when the library has no rated tracks yet. Reuses the existing SongRail layout, with the standard reroll button forcing a cache bypass. Per-card stars: any SongCard whose `userRating > 0` now shows a small five-star row (filled to the rating value) below the artist line — visible everywhere SongCard is used, not only in the new rail. Read-only display; rating is still done via the row's context menu or the Now Playing star widget. Cache layer in `ndListSongs`: opt-in `cacheMs` parameter (skipped by VirtualSongList; used only by the Highly Rated rail with a 60 s TTL). Cleared on `setRating` mutation so a freshly-rated track shows up on the next page revisit, and on server switch alongside the existing token cache. The reroll button explicitly invalidates before refetching, so a manual refresh always hits the network. * docs(changelog): add #443 Tracks Highly Rated rail entry * chore(credits): add #443 to Psychotoxical contributions |
||
|
|
98ff73d17a |
feat(perf): 3-state animation mode (Full / Reduced / Static) (#441)
* feat(perf): 3-state animation mode (Full / Reduced / Static) Replaces the boolean `reducedAnimations` toggle with a three-way `animationMode` setting, suggested by Viktor Petrovich after the Windows audio fix (PR #426) shipped and confirmed a measurable GPU drop: - `full` (default): native frame rate, marquee scrolls normally - `reduced`: 30 fps cap on the animated seekbar wave; player marquee runs at half speed - `static`: rAF loop disabled; the seekbar repaints from the ~2 Hz audio:progress heartbeat. Player title/artist truncate with ellipsis instead of scrolling. Migration in `onRehydrateStorage` maps legacy `reducedAnimations: true` to `'reduced'`, anything else to `'full'`. Static is opt-in only. Settings UI follows the ReplayGain Auto/Track/Album pattern with a contextual hint that explains what each mode does. i18n: 5 new keys across 8 locales, 2 legacy keys removed. * docs(changelog): add #441 3-state animation mode entry * chore(credits): add #441 to Psychotoxical contributions |
||
|
|
4483552c94 |
fix(i18n): backfill shortcut labels for #435 in 7 locales (#436)
* fix(i18n): backfill 10 settings.shortcut* keys for #435 PR #435 added 10 new settings.shortcut* labels to en.ts (start search, advanced search, toggle sidebar, mute, equalizer, repeat, open now playing, lyrics, favorite current track, open help) but left the other 7 locales without translations — i18next would fall back to English at runtime. Adding translations for de, fr, nl, zh, nb, ru, es in the same position as en.ts (right after shortcutOpenMiniPlayer), styled after the existing shortcut* entries in each locale. * docs(changelog): add #435 shortcuts action-registry entry * chore(credits): add #435 to cucadmuh's contributions |
||
|
|
1e05180418 |
feat(shortcuts): action registry + dynamic CLI help + new input targets (#435)
* feat(shortcuts): unify action-driven shortcut and CLI routing Centralize shortcut action metadata in one TypeScript registry and route keyboard, global shortcut, mini-window, and CLI inputs through shared runtime handlers. Keep CLI as an abstract transport layer by emitting player-command payloads without depending on shortcut definitions. * feat(shortcuts): generate CLI action help from shortcut registry Move no-arg player commands and their descriptions into the central action registry so CLI parsing and --player help are derived dynamically from one source of truth. Also route runtime action execution through the registry and remove duplicated shortcut runtime handling. * feat(shortcuts): add new input actions and hidden F1 help binding Add the requested input actions (search, advanced search, sidebar, mute, equalizer, repeat, now playing, lyrics, favorite current track) to the central shortcut action registry and wire runtime handlers for sidebar/equalizer toggles. Keep Help bound to F1 by default while hiding it from Settings input lists, and backfill persisted keybindings with new defaults so F1 works for existing users. Requested by @zunoz (Discord community). |
||
|
|
e6a20f03cd |
fix(i18n): restore reducedAnimations + statistics.export* strings dropped by #419 squash (#433)
* fix(i18n): restore reducedAnimations locale strings dropped by #419 squash The 'Reduce animations' toggle introduced in PR #426 ships with two i18n keys (settings.reducedAnimations + settings.reducedAnimationsDesc). The #419 squash-merge silently removed them from all 8 locales, and the #429 restore took the locale files from HEAD instead of merging them, so the keys never came back. The toggle was rendering as raw keys in the UI. Restoring the strings to the |
||
|
|
18b4a982ef |
feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing
* deleted endsAt and showDuration strings, changed eta update to 30s
* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon
* feat(queue): fold ETA into existing total/remaining toggle as third mode
The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.
ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.
i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.
* docs(changelog): add #419 queue UX improvements entry
Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
|
||
|
|
2e9618cf54 |
fix(audio): Windows playback stutter under GPU load (#334) (#426)
* fix(audio): promote WASAPI render thread to MMCSS Pro Audio on Windows
Wraps the outermost audio source in a `PriorityBoostSource` that calls
`AvSetMmThreadCharacteristicsW("Pro Audio")` on its first sample. The
cpal output-stream callback runs `Source::next` on the WASAPI render
thread, which is otherwise normal-priority and gets preempted under
WebView2 / DWM / GPU pressure — producing the audible click/stutter
reported in issue #334. No-op on Linux/macOS (PipeWire/rtkit and
CoreAudio promote their audio threads externally).
* fix(build): repair Windows compile after audio split + lib decompose
Two pre-existing build breakers on Windows that surfaced after the
`use super::*;` cleanup (
|
||
|
|
7064ca500e |
feat(stats): shareable Top-Albums card export (#425)
* feat(stats): add shareable Top-Albums card export
Adds a shareable PNG export of the user's most-played albums,
available from the Statistics page under the "Most Played Albums"
section header.
Layout:
* 3 formats — Story (1080×1920, 9:16), Square (1080×1080), Twitter
Card (1200×675, 16:9). Story and Square stack a centered URL footer;
Twitter packs logo + label + URL into a single header row.
* 3 grid sizes — 3×3 (default), 4×4, 5×5. Modal fetches up to 25
frequent albums on open so larger grids work even when the entry
surface only loaded a handful.
* Cover tiles render the rank + play count in a thin black info strip
at the bottom of the cover (rank left, "N Plays" right).
* Header label is hardcoded English ("Top Albums") so a shared image
remains legible to followers who don't share the user's UI language.
Caller can override via `opts.meta`.
Source path is local-only — `getAlbumList(type='frequent')` from
Subsonic. Last.fm is intentionally not used.
Implementation:
* `src/utils/exportAlbumCard.ts` — pure Canvas-API renderer. Reads
theme accent / bg from the document's CSS variables so the export
matches the active theme. Cover art is loaded through the existing
`getCachedBlob` IndexedDB cache, so repeated exports are cheap.
Wordmark is rendered from the `PsysonicLogo` React component via
`renderToStaticMarkup`, with both gradient stops baked to a single
accent color for crisp single-tone branding.
* `src/components/StatsExportModal.tsx` — UI: format + grid pickers,
live downscaled preview, native save dialog via
`@tauri-apps/plugin-dialog` + `plugin-fs`.
* `src/components/AlbumRow.tsx` — adds optional `headerExtra` slot so
the share button sits next to the existing scroll-nav arrows.
* i18n keys added under `statistics.*` (`en`, `de`) — other locales
fall back to English.
* docs(changelog): add Top-Albums card export for PR #425
|
||
|
|
9ad0f8af6d |
feat(ui): UI refinements — sidebar indicators, adaptive header, and interaction polish (#397)
* feat(ui): unify queue toggle handle behavior Show the queue toggle in the header when the queue is collapsed and use a seam-aligned drag handle when it is open. Hide the seam handle while the main content is actively scrolling to reduce accidental interactions. * feat(ui): add adaptive header search collapse behavior Collapse header search to a magnifier when top controls get crowded and expand it as an overlay only while active. Use measured header space with hysteresis to avoid flicker and keep neighboring controls stable. * chore(ui): remove leftover search prototype artifacts Drop an unused icon import from live search and remove an unused header container-type rule left from an earlier layout experiment. * feat(ui): persist sidebar and queue visibility state Save left sidebar collapse and right queue open/closed visibility in local storage helpers so both panel modes are restored after app restart. * feat(ui): unify player overflow menu behavior Use a single overflow menu for click and wheel interactions, with a volume-only mode that keeps the same layout and volume controls as the full menu. * feat(ui): add wheel seek controls to waveform Apply 10-second wheel seek steps with trailing 1-second debounce and keep the waveform preview stable so the playhead moves smoothly during rapid scroll input. * fix(now-playing): stabilize narrow dashboard layout Switch now-playing responsiveness to container-based breakpoints and prevent stacked widgets from overlapping when width is constrained. * fix(search): reduce collapse jitter and avoid header overlap Add a short collapse-state cooldown to prevent threshold flicker and hide conflicting header controls while collapsed search expands as an overlay. * fix(i18n): localize player overflow controls across locales Replace hardcoded player overflow labels with translation keys and add the missing keys for all shipped locale files. * fix(search): keep advanced control clickable in collapsed mode Prevent focus loss on the advanced search button in collapsed overlay mode so its click handler consistently runs. * fix(i18n): restore queue translation in offline library Use the existing queue.appendToQueue key for the offline enqueue button tooltip and label instead of a missing key and hardcoded English text. * fix(ui): apply overlay scrollbar to right-panel text tabs Switch now-playing content, lyrics, and info panes to OverlayScrollArea and harden tour-item layout so long concert metadata stays within panel bounds. * fix(ui): add unread indicator for new releases and guard sidebar drag clicks Track unread new-release IDs per server/library scope and clear the badge when opening the New Releases page. Also prevent click-through navigation after sidebar drag release and keep related i18n/responsive sidebar-adjacent refinements in this snapshot. * fix(ui): stabilize live dropdown layering and unread reset flow Render the topbar Live dropdown via a portal so it consistently overlays sidebar layers. Rework new-releases unread tracking to handle library scope baselines, ignore stale refresh races, and mark items as seen after a 5-second stay on the New Releases page. * feat(ui): add localized New badges for recently added albums Show a theme-consistent New badge on album cards and album detail for albums created within the last 48 hours. Localize the badge label across all supported locales and centralize recency logic in a shared utility to avoid duplication. * fix(album): prevent tracklist jump when entering multiselect Move bulk selection actions from the tracklist body into the album toolbar next to the track filter. Keep selection controls stable in the header area so enabling multiselect no longer shifts the tracklist content downward. * fix(tray): add playback-state badge and finalize queue handle tooltip Show play/pause/stop icons in the Linux tray now-playing entry and persist state safely in Tauri managed state. Also switch the queue-resize handle tooltip to the dedicated localized key across all locales. * fix(header): prioritize search collapse before Live/Orbit labels Make topbar compression deterministic by collapsing search first and compacting Live/Orbit labels only in sustained low-space mode. Add sticky hysteresis-based header compact state to prevent oscillation while resizing. * fix(ui): stabilize header compaction and show tray state icons Prevent topbar flicker in the narrow-width range by tightening compact-mode thresholds, gating on real overflow, and removing width transitions from live search. Also include playback state icons in tray tooltip text across platforms while preserving tooltip length limits. * fix(tray): keep tooltip iconization Windows-only Revert Linux tray tooltip/title fallback attempts and keep state icons only in Windows tray tooltips, while Linux continues to show playback state in the now-playing menu entry. * fix(ui): restore queue resize response after overlay scroll interactions Hide the queue handle while scrolling on both the main route viewport and the now-playing viewport, and clear stale thumb-drag state before starting queue resize. Also ignore inactive/faded overlay thumbs in resizer suppression so horizontal pointer transitions no longer leave the queue seam unresponsive. * docs(changelog): summarize ui-refinements branch features Document the branch-level feature additions in 1.45.0 as separate changelog sections and group remaining branch-local fixes under a single polish entry. * docs(changelog): add PR #397 references for ui-refinements Attach PR metadata to the new 1.45.0 ui-refinement sections and the polish entry so release notes map directly to the merged branch discussion. |
||
|
|
2ea22635e5 |
feat(tray): show now-playing track in tray + i18n menu labels (#395)
* feat(tray): show now-playing track in tray icon tooltip
Mirrors the current track to the tray tooltip ("Artist – Title") on every
play/pause/track change, falling back to "Psysonic" when nothing is playing.
The cached value survives tray hide/show via a new TrayTooltip state, and
is truncated to 127 chars for Windows NOTIFYICONDATA.szTip.
Closes #383
* feat(tray): linux fallback — disabled menu item shows now-playing track
AppIndicator on Linux has no hover-tooltip API, so the tooltip command was
a silent no-op there. Adds a disabled menu entry at the top of the tray
right-click menu that mirrors the same text. Updated in lockstep with the
Win/macOS tooltip via set_tray_tooltip.
* i18n(tray): localize tray menu labels
Adds a `tray` namespace in all 8 locales (en/de/fr/nl/zh/nb/ru/es) for
Play/Pause, Next/Previous, Show/Hide, Exit, and the Linux-only
"Nothing playing" placeholder.
Rust side: TrayMenuLabels state holds the cached translations and
TrayMenuItems holds the live MenuItem handles. New set_tray_menu_labels
command pushes new strings + applies them via set_text without rebuilding
the tray icon.
Frontend pushes the labels on mount and on every i18n.on('languageChanged').
|
||
|
|
20a083a9a6 |
feat(player): preview indicator in player bar + smart stop semantics (#394)
* feat(player): preview-active state on play button (ring + stop icon) Checkpoint: play button mirrors the inline preview button from tracklists during preview playback — hollow circle, accent ring depleting over the preview duration, Square (stop) icon. Click still resumes main playback, which the Rust audio engine cancels the preview for. i18n key player.previewActive in all 8 locales for tooltip + aria-label. * feat(player): show preview track in player bar + smart stop semantics The player-bar info cell (cover, title, artist) now mirrors the previewing track during preview playback, with a small accent "Preview" pill above the title and an accent top-border on the bar. Rating, fullscreen hint and album/artist link clicks are suppressed while previewing — they target the queued track, not the preview. Stop semantics for the two transport buttons during preview: - Big play button (Square+ring visual): stops preview, main auto-resumes if it was playing before. Matches the tracklist preview-button behaviour. - Small Stop button: new audio_preview_stop_silent Rust command — stops preview AND leaves main paused, so "Stop = silence" actually goes silent. previewStore now stores the full PreviewingTrack (id, title, artist, coverArt) — the seven startPreview call sites pass it through. i18n key player.previewLabel in all 8 locales. |
||
|
|
a14dba8167 |
feat(audio): rust track preview engine + inline play/preview buttons (#392)
* feat(audio): rust preview engine with secondary sink Adds a parallel rodio Sink on the existing OutputStream for 30s mid-track previews. Two new Tauri commands (audio_preview_play, audio_preview_stop) plus three events (audio:preview-start / -progress / -end). The main sink is paused with Sink::pause() and auto-resumed on preview end iff it was playing beforehand. * feat(playlists): migrate suggestion preview to rust audio engine Replaces the HTML5 <audio> path with the new rust preview engine. previewStore mirrors the engine's start/progress/end events so any tracklist row can render preview UI from a single source of truth. Spacebar redirects to stopPreview while a preview plays, hardware mediakeys are silently dropped (Q5), and tray clicks cancel the preview before forwarding the original action. * feat(albums): inline play + preview buttons in tracklist rows Track number stays static on hover instead of swapping to a play icon — the dedicated Play and Preview buttons in the title cell take over click-to-play and click-to-preview. Active+playing rows keep the eq-bars (also on hover), active+paused rows fall back to the static accent-coloured number. Pilot for the wider rollout to other tracklists. * feat(tracklists): roll out inline play + preview buttons Mirrors the AlbumTrackList pilot across the remaining track-row based lists: PlaylistDetail main tracks, Favorites, ArtistDetail top tracks, RandomMix (both genre-mix and filtered-songs lists). Track number stays static, the dedicated Play + Preview buttons in the title cell take over click-to-play and click-to-preview. * feat(settings): track preview toggle + configurable position and duration Adds an opt-out switch and two sliders to Settings → Audio: start position (0-90 % of track length, default 33 %) and preview duration (5-60 s, default 30 s). The progress-ring animation follows the duration via a CSS variable so the visual matches the engine's auto-stop. Disabling the feature hides every inline preview button via a single root-level data attribute, no per-row conditional rendering required. i18n keys added in all 8 locales. * fix(audio): cancel preview when main playback (re)starts audio_play, audio_play_radio, audio_resume and audio_stop did not know about the parallel preview sink, so clicking Play on a track that was currently being previewed left the preview running on top of the freshly started main playback. New helper clears the resume flag, bumps the preview generation, drops the sink and emits an 'interrupted' end event before any of those commands touches the main sink. * feat(settings): per-location track preview toggles Splits the single trackPreviewsEnabled toggle into a master + 6 per-location sub-toggles (suggestions, albums, playlists, favorites, artist, randomMix). Master remains the kill switch; sub-toggles are only honoured when master is on. Each tracklist container is marked with `data-preview-loc="<id>"` and hidden via scoped CSS when the matching root attribute is "off". startPreview now takes a location argument so the store can guard logic too. i18n added in all 8 locales. * fix(contextmenu): use ChevronsRight for Play Next to distinguish from preview |
||
|
|
2bea55bedd |
feat(playlists): suggestion-row preview UX (#365)
* feat(playlists): suggestion-row preview UX (30s preview, double-click play-next, scroll keep)
Reworks the interaction on the suggestion rows below a playlist so users
can audition songs before deciding what to do with them.
- New "Preview" pill button left of each track title plays a 30-second
mid-song sample via a parallel HTML5 audio element. The main player
pauses on the first preview and auto-resumes when the preview ends.
Switching previews chains without re-resuming. External main-player
playback (spacebar, mediakey) cancels the preview without resuming.
Animated underline shows the 30 s progress.
- Double-click the row to insert the song at queueIndex + 1 and skip to
it ("Play next"). Single-click is intentionally inert so a stray click
next to the preview button no longer drops a song into the queue by
accident. Tooltip on the row makes the affordance discoverable.
- The + button still adds to the playlist, and now restores the
.main-content scroll position so the page no longer jumps to the top
after pressing it.
- i18n keys in all 8 locales: playlists.preview / previewStop / previewShort
/ previewStopShort / suggestionDoubleClickPlayNext.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playlists): WebKitGTK NULL-instance crash on preview teardown
cucadmuh hit GLib-GObject-CRITICAL: invalid (NULL) pointer instance /
g_signal_connect_data assertion failed, with the UI freezing after
clicking a preview button. Root cause was the audio-element lifecycle:
- `audio.src = ''` to "stop" leaves WebKitGTK's GStreamer playbin in a
half-initialized state. The next signal_connect dereferences NULL.
- Creating `new Audio()` per click stacked half-torn-down playbins
during rapid switches.
- `loadedmetadata` listeners on orphaned audio elements could call
play() on an already-discarded instance.
Fix: reuse one <audio> element per component, tear down the source
via removeAttribute('src') + load() (which resets the playbin
cleanly), and gate async listeners behind a session counter so stale
metadata events on switched-away previews are ignored.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(playlists): icon-based action buttons + add-on-doubleclick + hint
- Replace text-pill Preview button with circular icon button + animated
SVG progress ring (Play/Square icon swap when active).
- New Play-next button (filled accent circle, white triangle) sits
left of the Preview button — explicit affordance for the action that
used to be a hidden double-click.
- Double-click on a suggestion row now triggers Add-to-playlist (the
same action as the + button on the right) — discoverable shortcut
for mouse users.
- Subtitle under the Suggested Songs header announces the add-on-
doubleclick affordance, in all 8 locales.
- Drop the now-obsolete previewShort / previewStopShort short-label
keys (text replaced by icons); rename suggestionDoubleClickPlayNext
→ playNextSuggestion to match its new role on the Play-next button.
- Keep the session-counter guard from the prior commit so async
loadedmetadata handlers from a switched-away preview can't play()
on a discarded element.
Note: header column labels visually drift from the data columns when
suggestion rows have the action buttons; left as-is for now.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(playlists): apply LUFS pre-analysis attenuation to suggestion previews
When the main player is on loudness normalization, analysed tracks come
out reduced toward target while the unanalysed preview <audio> blasts
the file at its natural level — cucadmuh reported the previews are
audibly louder than the playlist playback.
Apply the user's stored pre-analysis attenuation (the slider value, not
the target-offset effective form) as a linear gain on the preview's
audio.volume. Default −4.5 dB lands the preview at ~60% of the player
volume, which roughly tracks how aggressively the Rust engine pulls
naturally-loud tracks toward target.
If the engine is off, behavior is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* style(playlists): use chevron icon for suggestion preview button
Distinguishes the preview button from the adjacent play-next button
by mirroring the Play Next chevron from the context menu.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
87373edb17 |
fix(loudness): target sync, effective pre-analysis trim, and queue/settings copy (#333)
* fix(loudness): target sync, -14 pre-analysis ref, queue UI, and reseed - Front: coalesce loudness refresh by target LUFS; replay-gain IPC dedupe keys include norm target and effective pre-attenuation so TGT changes apply. - Rust: placeholder gain before integrated LUFS uses pivot at -14 LUFS; UI gain from effective trim; reseed loudness after delete when waveform cache would skip. - Pre-analysis: store attenuation relative to -14 LUFS; engine and UI use an offset for other targets; migrate legacy absolute values on rehydrate. - Queue/Settings: Loudness/TGT labels vs value buttons; styles; i18n for help. * fix(i18n): simplify loudness pre-analysis helper copy Remove reference-target wording from loudness pre-analysis helper text and keep only the effective adjustment shown for the current LUFS target in all locales. |
||
|
|
e0c53da94d |
feat(playlists): confirm before adding songs that are already in the playlist (#329)
Closes #327. When every selected song is already in the target playlist, the "Add to Playlist" flow used to silently show an "all skipped" toast. Users could not tell whether they had hit the wrong target or whether the action was deliberately ignored, and they had no way to add duplicates intentionally (e.g. for a song they actually want twice). New behavior: if every id is a duplicate, ask via the existing GlobalConfirmModal — "Add anyway" appends them as duplicates, "Cancel" keeps the previous silent-skip toast. Mixed cases (some new, some duplicate) and the all-new path are unchanged. Applied at all three add-to-playlist call sites in the context menu (single/multi-track, album selection, artist selection). Strings added to all 8 locales (en, de, es, fr, nl, nb, ru, zh). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
756b189bcc |
ui(settings): restructure Normalization section for clarity and breathing room
The mode picker (Off / ReplayGain / LUFS) used to live in the right-hand action slot of a settings-toggle-row and the per-mode controls were stacked into the same parent with footer-style help text. Hard to scan, visually cramped, and odd compared to the rest of the Settings page. Refactor: - Mode picker becomes a full-width segmented row with even-flex buttons, using the new .settings-segmented utility. - Each mode renders its own .settings-norm-block sub-section with a subtle accent tint and border so the active configuration reads as one coherent group. - Inside the block, every setting is its own .settings-norm-field (control row + per-control help text immediately below). 1.1 rem gap between fields, 0.45 rem between row and help — clearly groups related text without crowding. - Sliders no longer max-cap at 200 px and instead flex to fill the row. - Inactive ghost buttons (Off, ReplayGain, RG mode, LUFS targets) get a visible border and a faint surface tint so they read as selectable slots in dark themes too. - LUFS mode gets a dedicated note-box explaining that brief volume drift on the very first play of a new track is the analysis pass at work, not a bug — subsequent plays use the cached measurement, and queued tracks are usually pre-analysed during the previous song. - "Trim before measurement (dB)" renamed to "Pre-analysis attenuation" (and equivalents in 8 locales). - New i18n keys: normalizationDesc, normalizationOff/ReplayGain/Lufs, loudnessTargetLufsDesc, loudnessFirstPlayNote, replayGainPreGainDesc, replayGainFallbackDesc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ed76090a54 |
fix(seekbar): split waveform style into truewave (analyzed) + pseudowave (deterministic)
The waveform-loudness-cache merge replaced the existing deterministic per-track-ID waveform with a bins-based one driven by the analysis cache. The bins-based variant is the better default but the old deterministic look is still valuable when no analysis is available (brand-new track, cache-miss, etc.) and several users prefer it. Split into two explicit options in the seekbar style picker: - 'truewave' (default, replaces old 'waveform') — bins from the analysis cache, with morph-on-arrival animation and flat-line fallback while empty. - 'pseudowave' — pseudo-random heights derived deterministically from the track ID. No analysis dependency, no morph, instant render. Existing persisted seekbarStyle: 'waveform' is migrated to 'truewave' in onRehydrateStorage so users keep the visual they have today. The useEffect that builds heightsRef now lists seekbarStyle in its deps so switching between the two is live. i18n labels added in all 8 locales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4b60495e38 |
feat(playback): loudness bind rules, analysis seeding, and normalization UI
Resolve integrated loudness from SQLite only at decode bind; keep pre-trim until a row exists, then allow provisional gain from live updates. Pass DB-stable hints from the web app into play and gapless preload. Default pre-measurement attenuation -4.5 dB with an icon reset in Settings; drop redundant normalization copy and shorten pre-trim descriptions. analysis_cache seed_from_bytes returns an outcome and skips redundant waveform work on cache hits; wire callers and related frontend/backend glue. |
||
|
|
c6fc3ec844 |
fix(waveform): stabilize progressive rendering and reduce streaming contention
Improve seekbar fallback behavior by using consistent bar-based rendering and smoother transitions to analyzed waveform data. Reduce hot-path analysis overhead during ranged streaming and add controls to clear cached waveform entries. |