Per bcorporaal's request in #252: album-oriented listeners want a
cleaner artist page focused on the album grid, with the freedom to
reorder/hide bio, top tracks, and similar artists.
Five sections are now reorder- and toggle-able from Settings →
Personalisation → "Artist page sections": Bio, Top tracks, Similar
artists, Albums, Also featured on. The fixed header block (artist
photo, name, action buttons) stays at the top.
Implementation reuses the existing sidebar customizer pattern:
- new `src/store/artistLayoutStore.ts` modelled on `sidebarStore.ts`
with the same persist + onRehydrateStorage migration so future-added
sections don't silently disappear from existing users' configs.
- `ArtistDetail.tsx` render block refactored from a flat sequence of
conditional JSX into a `renderableSectionIds.map()` driven by the
store. The "first rendered section gets marginTop: 0" rule now
works regardless of the configured order — both hidden-by-toggle
and empty-data sections are filtered before the layout decides
what's first.
- `ArtistLayoutCustomizer` component in Settings, lifted from
`LyricsSourcesCustomizer` (drag via `useDragSource` + `psy-drop`
event, per-row toggle, reset button).
- 7 new i18n keys per locale across all 8 languages.
The store hook MUST stay above the loading / !artist early returns —
otherwise React's hook call order mismatches between renders and the
component fails silently. Lesson learned during the refactor.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(queue): add Now-Playing Info tab with artist bio, song credits and Bandsintown tour dates
A third tab in the right-side queue panel surfaces context for the
currently playing track:
- Artist card: image + biography from Subsonic getArtistInfo (Last.fm
"Read more on Last.fm" anchor stripped), with read-more toggle that
only appears when the bio actually overflows the 4-line clamp.
- Song info: contributor credits from OpenSubsonic contributors[]
rendered stacked (name prominent, role muted). Section is hidden
entirely on servers without contributor support, and rows that just
re-state the main artist under role "artist" are filtered out.
- On tour: optional Bandsintown integration (opt-in, off by default).
HTTP fetch + JSON parsing happen entirely on the Rust side; the
frontend wrapper deduplicates concurrent calls and caches results in
RAM for the session. Limited to 5 events with a "Show N more" toggle.
When the toggle is off, the section becomes an in-place opt-in card
with a privacy info-tooltip explaining what data is sent — same
tooltip is also exposed on the matching toggle in Settings.
Caching: artist info and song detail are memoised by stable IDs across
component remounts, so jumping between tracks of the same album/artist
does not refire the network calls.
Implementation notes:
- Bandsintown app_id "js_app_id" — arbitrary strings (e.g. "psysonic")
now return HTTP 403 from rest.bandsintown.com; js_app_id is the ID
Bandsintown's own embeddable widget uses and is broadly accepted.
- Tour items use negative left/right margins so the date badge stays
visually aligned with the section title while the hover background
extends slightly past the section edges.
- New i18n namespace nowPlayingInfo across all 8 locales (en, de, fr,
nl, zh, nb, ru, es) including pluralised "Show N more" forms.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(now-playing-info): switch nested flex-columns to block layout to stop content-dependent drift
Repeated reports of the Tour and Song-info sections rendering at
inconsistent x-positions (sometimes left of the section title,
sometimes centred, sometimes correctly aligned) — varying by artist,
sidebar width, and even whether "Show more" was clicked.
Root cause: nested flex-direction: column containers
(.np-info → .np-info-section → .np-info-tour / .np-info-credits) where
the cross-axis (horizontal) sizing on WebKitGTK occasionally inherits
the intrinsic min-content of the longest child. With one
"Naherholungsgebiet/Freizeitgelände Lago Alfredo"-style venue name
that overflows the sidebar, the parent ul gets pushed past 100% width
and the entire layout drifts. min-width: 0 on every level didn't help
because cross-axis stretch in flex-column ignores it for content-driven
overflow.
Fix: collapse all the section/list containers to display: block. Block
layout is content-agnostic — children always render at 100% parent
width, left-aligned, deterministically. Spacing between siblings now
uses the lobotomy selector (`> * + * { margin-top }`).
Only the tour item itself stays flex (badge ↔ meta horizontal layout),
and that one still has overflow: hidden + flex: 1 1 0 + min-width: 0
on the meta column to truncate venue/place text with ellipsis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto mode is the new default for ReplayGain. It picks album gain when an
adjacent queue neighbour shares the same albumId (so "Play All" on an
album, or any contiguous album block, gets album gain), otherwise track
gain. Falls back to track gain when album gain is missing.
- authStore: replayGainMode widened to 'track' | 'album' | 'auto', default 'auto'
- playerStore: new resolveReplayGainDb(track, prev, next, enabled, mode) helper
replaces five inline ternaries (audio_play hot path, gapless preload,
cold-resume success + fallback, live update_replay_gain)
- Settings: third "Auto" button + contextual hint when active
- i18n: replayGainAuto + replayGainAutoDesc added to all 8 locales
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a System setting for Off/Normal/Debug logging, apply readable local timestamps to backend logs, and enable exporting buffered runtime logs to a file when debug mode is active.
Co-authored-by: Maxim Isaev <im@friclub.ru>
RandomMix: filters and genre mix panels collapse on mobile
- RandomAlbums / album grids: auto-fill favouring 3 columns at narrow widths
- BottomNav: More button opens a bottom sheet with remaining nav items
- MobileMoreOverlay: new sheet component with backdrop and slide-up animation
- Tracklist: mobile Title / Artist two-line stacked layout (Apple Music style)
- ArtistDetail: centred header (photo → name → buttons), icon-only buttons
except Play All, collapsible Similar Artists (5 default), 2-column album
grid, avatar glow derived from dominant cover colour
- Settings: fixed overflow in ReplayGain, Crossfade, mix rating filters,
theme scheduler, and Next Track Buffering sections
Co-authored-by: kilyabin <65072190+kilyabin@users.noreply.github.com>
Maps Navidrome's lastAccessAt onto NdUser and renders it in the
User Management row as a localized relative time (Intl.RelativeTimeFormat
with the current i18n language). Tooltip carries the absolute
timestamp. Navidrome returns 0001-01-01 for never-accessed users —
detected and shown as the localized "Never" label.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Navidrome library assignment to the User Management settings
panel: GET /api/library + PUT /api/user/{id}/library wired through
new Rust commands. UserForm gets a checkbox picker (hidden for
admins, who auto-receive all libraries server-side) with inline
validation. User rows compacted to a single clickable line with
hover highlight; native confirm() replaced by a portal-based
ConfirmModal (theme-aware, ESC/Enter, vertically centered).
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a dedicated Settings tab for managing Navidrome users (list, create,
edit, delete). Only visible to admins — gated by `/auth/login` probe on
mount. Uses Navidrome's native /api/user endpoints instead of Subsonic
(getUsers.view etc. return only the caller on Navidrome). All HTTP calls
go through new Rust commands to bypass WebView CORS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The psy-drop listener was attached in a useEffect keyed only on
setLyricsSources, so when the container was unmounted by the
{lyricsMode === 'standard' && …} wrapper (switching to YouLyPlus and
back), the listener stayed bound to the old DOM node and drag-to-reorder
silently did nothing.
Switch the container ref to a callback-ref stored in state, so the
effect re-runs on mount/unmount and always listens on the current DOM
node.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Preload mini player" toggle in Settings → General → App behaviour
(off by default). When enabled, the mini webview is built hidden at app
start so the first open is instant, matching the Windows experience.
Costs one extra WebKit process running in the background (~50–100 MB).
Windows already pre-creates the mini unconditionally as a hang workaround
(commit 71fbc717); the toggle is hidden there and the invoke is skipped,
so that platform is untouched.
- authStore: new `preloadMiniPlayer: boolean` (default false) + setter.
- lib.rs: new `preload_mini_player` command; idempotent, no-op when the
window already exists. Registered in the invoke handler.
- App.tsx: main window invokes `preload_mini_player` when the toggle is
on and the platform is not Windows.
- Settings.tsx: toggle row under "Minimize to Tray", gated with
`!IS_WINDOWS` so Windows users don't see it.
- i18n: `preloadMiniPlayer` + `preloadMiniPlayerDesc` added to all 8
locales (en, de, es, fr, nb, nl, ru, zh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an 'open-mini-player' entry to the in-app keybindings (Settings →
Shortcuts), default unbound. Pressing the chord from the main window
opens the mini and minimises main; pressing it from the mini hides the
mini and restores main — both paths invoke open_mini_player which
already handles the toggle.
The mini window has its own keydown listener (same pattern as Space /
arrows for play/skip) that reads the binding from useKeybindingsStore.
Binding changes in main propagate to an open mini through the existing
storage-event sync (now also covers psysonic_keybindings) so the user
doesn't need to restart the mini after rebinding.
Localized in all 8 supported locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the giant startup changelog modal. After an update, a compact
neutral-palette pill now sits above Now Playing in the sidebar saying
"Changelog — vX.Y.Z". Clicking opens a proper /whats-new page in the
main content area; X dismisses. Page renders the current version's
CHANGELOG entry with inline Markdown (headings, lists, blockquotes,
hr, bold/italic/code, and real [label](url) links that open via the
Tauri shell plugin).
Visual tokens are hardcoded slate/gray/blue so the banner and page look
identical across every theme (dark, light, skeuomorphic, whatever) —
requested for consistent contrast. Auto-mark-as-seen removed from the
page; the banner only goes away on explicit dismiss, so users can
re-read as often as they want.
Settings → About gains a "Release notes" row with a link that resets
the seen-version and opens the page, so the banner can be retriggered
manually (also helpful for dev builds ahead of the current tag).
The existing "Show changelog on update" toggle now gates the banner
instead of the modal; description strings updated in all 8 locales.
fix(themes): WCAG contrast audit — mocha + winmedplayer + wista
Mocha (Catppuccin dark)
.track-size used --ctp-overlay0 directly (3.36:1 on bg-app, 2.57
on bg-card). Swapped to --text-muted → 7.37 / 5.65. Fix also lifts
macchiato/frappe/latte which shared the failure.
WinMedPlayer (Luna)
--text-muted #b8d0f8 (3.87:1 on bg-app) → #e8f0ff (5.28)
--border #2a5090 (1.31 on bg-app) → #071027 (3.12, meets 3:1 UI)
Wista (Vista Aero)
Lyrics pane sits on bg-sidebar #0e1e3e but used --text-primary
(#0d1d3c) for active lines — 1.01:1, literally invisible. Added
component overrides: .lyrics-line / .lyrics-status / word-synced
variants → #aac8f0 (9.60), .lyrics-line.active → #ffffff (16.48).
Palette: --warning #c8980c → #735a00 (2.37 → 5.91 on bg-app),
--text-muted #4870a8 → #3f6aa0 (4.23 → 4.65 on bg-card).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Persist preference in auth store, sync from App on Linux, and expose a
Tauri command using webkit2gtk to toggle enable-smooth-scrolling at
runtime. Default on to match upstream; users may disable for discrete
GTK-style line steps.
Apply a one-time rehydrate migration so smooth scrolling stays on after
updates even if an older build persisted the wrong default.
Document-level zoom broke portal positioning and Tauri window measurement,
so the slider had been forced to 100 %. Scope the zoom to a wrapper inside
.main-content instead — sidebar, queue, player bar and the Linux custom
title bar live in sibling grid cells of .app-shell and stay 1:1.
- App.tsx: drop the document-level zoom + auto-reset; wrap main-content
children in <div class="main-content-zoom" style={zoom: uiScale}>.
- layout.css: add .main-content-zoom (flex column, fills parent).
- Settings.tsx: re-enable the slider and snap it to the 6 preset stops
(80/90/100/110/125/150) so the thumb lands directly under each preset
button. Legacy off-preset values snap to the nearest stop on load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
"Fetch covers from Apple Music" and "Custom text templates" are now both
tucked into a single collapsible "Advanced Discord options" block (default
collapsed) that only appears when Discord Rich Presence is enabled.
Reduces vertical noise in Settings → General for the common case where
users just flip Rich Presence on and leave the defaults.
Uses the same chevron-rotate pattern as the font picker and contributors
list. Adds `settings.discordOptions` key in all 8 locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two independent user-facing fixes rolled together.
── Lyrics: YouLyPlus provider (issue #172) ──
Adds word-by-word synced (karaoke) lyrics as an alternative lyrics mode.
Backed by the public lyricsplus aggregator (Apple Music / Spotify /
Musixmatch / QQ Music) — no API keys on our side, subscription costs are
borne by the backend operator. Five mirrors are tried on network failure.
Settings → Lyrics now exposes a mode radio (Standard vs YouLyPlus) plus
a static-only toggle. YouLyPlus misses silently fall back to the existing
server + LRCLIB + Netease pipeline so obscure tracks still resolve. The
drag-to-reorder source list is hidden in YouLyPlus mode since the
external aggregator manages its own source priority.
Rendering: per-word spans on each line, active word highlighted with
accent-tinted glow. Subscribed imperatively via usePlayerStore.subscribe
so 500 ms progress ticks do not re-render the whole lyrics block. The
Fullscreen Player reuses the same pattern; the 5-line rail layout is
unchanged. white-space: pre on .fs-lyric-word preserves the trailing
spaces the API embeds in each syllabus token (flex context would
otherwise collapse them).
i18n: new keys in all 8 locales (de, en, fr, nl, nb, ru, es, zh).
── macOS mic-prompt suppression ──
Ships a vendored cpal 0.15.3 at patches/cpal-0.15.3/ wired via
[patch.crates-io]. The only change is in src/host/coreaudio/macos/mod.rs:
audio_unit_from_device now unconditionally uses IOType::DefaultOutput for
output streams instead of conditionally choosing HalOutput.
AUHAL (HalOutput) is classified as input-capable by macOS TCC and
triggers the microphone-permission dialog at first launch / after each
update even for playback-only apps. Previous attempts (removing
NSMicrophoneUsageDescription, setting com.apple.security.device.audio-input
false in Entitlements.plist) did not suppress the prompt because TCC
fires at AudioUnit instantiation, not at plist level. The upstream fix
in cpal PR #1070 / cpal 0.17 only helps when the pinned device equals
the system default; always-DefaultOutput covers all cases.
Tradeoff: per-device output selection is a no-op on macOS — the stream
always follows the system default. Settings.tsx surfaces this via
audioOutputDeviceMacNotice (hides the CustomSelect on macOS) and skips
audio_list_devices + device-watcher effects there. Matches the behaviour
of Apple Music / Spotify on macOS.
Also removes the now-obsolete com.apple.security.device.audio-input
entitlement (sandbox is disabled anyway, so it was ignored).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Write psysonic-sync.json to device after sync/deletions; auto-import
on folder select when localStorage is empty (cross-platform handoff)
- Add cancel button during active sync: AtomicBool flag per job,
tasks bail after acquiring semaphore, cancelled state in UI
- Fix sync status staying "pending": normalize template path separators
to OS separator (Windows: '/' → '\') so compute_sync_paths and
list_device_dir_files produce matching strings for Set comparison
- Add Geist and JetBrains Mono as variable fonts; font picker collapsible
- Fix device mount detection on Windows: removable drive letters (E:\)
were incorrectly skipped alongside system roots; strip \?\ prefix
from canonicalized paths before mount-point comparison
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract ALL_NAV_ITEMS to src/config/navItems.ts and add randomMix/randomAlbums
nav items. A new toggle in Settings switches between a single "Build a Mix" hub
and separate "Random Mix" / "Random Albums" sidebar entries (randomNavMode in
authStore). Fixes reorder crash caused by hidden items being overwritten with
undefined during the merge step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Embed bash and zsh completion scripts and add a `completions` subcommand.
Extend the player CLI (library, audio device, instant mix) and surface
`music_library` in snapshots. Add a shared active-server switcher in the
header and locales; instant mix can reseed the queue from CLI and UI.
- Changelog for v1.34.11 (Opus, Device Sync, community themes, SSL fix)
- Contributors updated in Settings About (PR #181, #182, #183)
- Version bumped to 1.34.11 in package.json, tauri.conf.json, Cargo.toml
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(audio): stabilize Linux output device picker and watcher
Keep pinned ALSA/cpal device ids stable when enumeration omits the active
sink or returns an equivalent name. Stop Linux device-watcher from clearing
the pin based on missing list entries; macOS and Windows still treat repeated
absence as unplugged. Settings refresh flow calls canonicalize and refetches
the list; add i18n for the out-of-list device label.
* fix(settings): sort audio output devices by label
cpal enumeration order is arbitrary; order the dropdown by readable label
and place the current OS default device first among concrete outputs.
- Add missing contributions: cucadmuh (PR #144, #167, #173, #174),
kveld9 (PR #168), nisarg-78 (PR #115)
- Remove aboutFeatures tag line from all 8 locales (too dynamic to maintain)
- Changelog section now shows only the 3 most recent versions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(settings): clearer audio device labels for duplicate ALSA names
Show HDMI outputs as "Card (HDMI n)" from hdmi:DEV indices; include PCM and
optional subdevice for hw/plughw/sysdefault; label other ALSA plugins with
iface and PCM. When labels still collide, append a structured hint
(iface · card · PCM) instead of only truncating the raw device string.
* feat(settings): improve audio output device picker
Parse ALSA-style ids into clearer labels (HDMI with DEV index, PCM/subdevice
for hw/plughw/sysdefault). Disambiguate colliding labels; share stderr
suppression for Linux device enumeration.
Add audio_default_output_device_name and tag the matching list entry as the
current system output (i18n). While the Audio tab is open, refresh list and
mark on audio:device-changed and audio:device-reset without toggling the
refresh spinner. Show an error toast if listing devices fails.
- Device watcher requires 3 consecutive misses (~9 s) before triggering
audio:device-reset, preventing false positives when ALSA temporarily
hides a busy device from output_devices() enumeration
- Add stderr suppression to open_stream_for_device_and_rate (last source
of ALSA terminal noise on Linux)
- Fix tooltip showing raw key 'common.refresh' — replaced with
settings.audioOutputDeviceRefresh (added to all 8 locales)
- Device switch restarts track from beginning (no seek-back)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw ALSA names like "sysdefault:CARD=U192k" are replaced with
readable labels ("U192k") in the device dropdown. The underlying
value stays unchanged so rodio can still open the correct device.
Covers: sysdefault, hw, plughw, iec958, front, surround prefixes.
Names without ALSA structure (pipewire, pulse, default) are kept as-is.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The module-level cache prevented newly connected USB audio devices
from appearing in the dropdown. Replace with a manual refresh button
(RotateCcw icon, spins while loading) next to the dropdown.
Device enumeration now runs on every Audio tab open and on explicit
refresh. ALSA stderr noise is already suppressed by the dup2 guard,
so re-enumeration is silent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Device watcher (3 s loop) and audio_list_devices both called
cpal output_devices(), triggering ALSA to probe unavailable backends
(JACK, OSS, dmix) and spam stderr with error messages.
Fix: redirect fd 2 to /dev/null for the duration of each enumeration
on Unix via libc dup/dup2 + RAII guard. Also add a module-level
cache in Settings.tsx so audio_list_devices is only invoked once
per app session instead of on every Audio tab activation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the ability to choose which audio output device Psysonic plays
through — useful for USB DACs, dedicated soundcards, and multi-device setups.
Rust (audio.rs):
- open_stream_for_device_and_rate(device_name, rate): opens a named
device by cpal name, falls back to system default if not found
- AudioEngine.selected_device: persists the chosen device across
stream reopens so hi-res rate switches stay on the right device
- audio_list_devices: Tauri command — returns all output device names
- audio_set_device: Tauri command — switches device immediately,
drops old sinks, emits audio:device-changed
- start_device_watcher: now handles two cases:
1. No pinned device + system default changed → reopen on new default
2. Pinned device disappeared (DAC unplugged) → clear selected_device,
fall back to system default, emit audio:device-reset
Frontend:
- authStore: audioOutputDevice (string | null), persisted
- playerStore: applies stored device on cold start
- App.tsx: listens to audio:device-reset, clears authStore device
and restarts playback on the fallback device
- Settings → Audio tab: device dropdown at top (above Hi-Res and EQ),
uses CustomSelect (portal-based, styled), all 8 locales
- CustomSelect: added disabled prop
Note: exclusive mode (WASAPI exclusive, CoreAudio exclusive) is out
of scope for now.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
https:// was already supported by the code (startsWith('http') check) but
the placeholder text only showed bare host:port examples, giving no hint
that a full URL with protocol is valid.
- Updated serverUrlPlaceholder in login namespace (all 8 locales) to show
https://music.example.com as the domain example
- Added settings.serverUrlPlaceholder i18n key (all 8 locales) and wired it
into AddServerForm — previously the placeholder was a hardcoded English string
Closes#171
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Persist chords as ctrl/alt/shift/super plus key code; legacy single-key
bindings still match without modifiers. Settings capture uses buildInAppBinding;
App uses matchInAppBinding and skips chords also registered as global shortcuts.
Share MODIFIER_KEY_CODES and formatBinding with global shortcut formatting.
Fix seek-forward/backward hotkeys: seek() expects 0-1 progress, not seconds.
--bg-base is undefined — sticky filter bar would have rendered transparent.
Also added PR #165 to cucadmuh's contributions in Settings About.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>