Purple & Gold palette (Deuteranopia / Protanopia / Tritanopia safe).
Vision Dark: near-black #0D0B12 + Gold #FFD700 (~14.7:1 WCAG AAA).
Vision Navy: deep navy #0A1628 + Gold #FFD700 (~14.5:1 WCAG AAA).
New 'Accessibility' group in ThemePicker.
Removed stale theme count from aboutFeatures across all locales.
This is an initial step toward colorblind accessibility — color variables
alone cannot cover all CVD use cases. Structural improvements (secondary
indicators, pattern/shape cues) are still needed in future iterations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- "Diesen Song entfernen" bekommt Trash2-Icon
- Neuer Eintrag "Künstler öffnen" (goToArtist) nach "Album öffnen" in beiden Song-Kontextmenüs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PlaylistDetail: Ctrl/Cmd+Click enters select mode; bulk drag emits
{ type: 'songs', tracks } when ≥2 selected; filtered-view rows now
also draggable as single songs
- Favorites songs: full multi-select system — Ctrl+Click, Shift+Click,
header toggle-all checkbox, bulk-selected highlight, bulk drag to
queue, bulk-bar with Add to Playlist + Clear
- ContextMenu: ArtistToPlaylistSubmenu resolves all artist album songs
and forwards to AddToPlaylistSubmenu
- Locale: common.clearSelection + playlists.addSelected in all 7 locales
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds full keyboard navigation (arrow keys, Enter, Ctrl+Enter for context menu) across Folder Browser columns. Context menus for all row types with keyboard-operable submenus and star-rating control via arrow keys + Enter. Now-playing path is visually emphasized and stays stable; rebuilds on hotkey re-invoke or active-path follow-along. Adaptive column layout for deep trees with right-side visibility priority. New configurable 'Open Folder Browser' keybinding. StarRating animation sync for keyboard-driven changes. All 7 locales updated.
Adds no_compositing_mode Tauri command; frontend adds html.no-compositing on Linux to replace GPU-only CSS effects (backdrop-filter, filter, mask-image) with software-friendly equivalents.
Settings → Appearance → Fullscreen Player: toggle for artist portrait visibility + 0–80% dimming slider.
Fix: long words in lyric lines now wrap correctly.
Replaces two sidebar entries (Random Mix, Random Albums) with a single 'Build a Mix' item (Wand2 icon) at /random. Landing page shows two cards — Mix by Tracks and Mix by Albums — with glow-on-hover. Old routes updated throughout. All 7 locales updated.
Replaces dedicated sort buttons with clickable column headers in album and playlist detail views. Three-click cycle: asc → desc → natural order. Sortable by title, artist, album, favorite, rating, duration. Active column shown bold with ▲/▼ indicator.
Resolves PLS and M3U/M3U8 playlist URLs to their first direct stream URL
before playback and ICY metadata fetching. Stations configured with a
.pls or .m3u URL (e.g. SomaFM, schizoid.in) now play correctly and
report track metadata via ICY headers.
- Rust: parse_pls_stream_url / parse_m3u_stream_url helpers
- Rust: resolve_playlist_url (shared) + resolve_stream_url Tauri command
- fetch_icy_metadata: auto-resolves playlist URLs before connecting
- playerStore: playRadio() awaits resolve_stream_url before setting audio src
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Netease is queried only when server and LRCLIB both return nothing,
preserving the existing lyrics chain completely. Off by default.
- Rust command `fetch_netease_lyrics` proxies Netease API (CORS bypass)
- `src/api/netease.ts` TypeScript wrapper via invoke
- `authStore.enableNeteaselyrics` toggle (default: false)
- `useLyrics`: Netease fires last in fallback chain when enabled
- Settings toggle in the Lyrics section
- `lyricsSourceNetease` label in LyricsPane
- i18n: all 7 languages (en, de, fr, nl, nb, ru, zh)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use OpenSubsonic `artists[]` array to render each artist separately
with · separator; artists with an ID are clickable, others plain text
- Fall back to single artist (artistId/artist) on non-OpenSubsonic servers
- Settings reset-to-defaults buttons changed from btn-ghost to btn-danger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves conflict in ContextMenu.tsx: keep useShallow import from main
alongside getSimilarSongs added by this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Throttle audio:progress from 100ms to 500ms; WaveformSeek and
FsSeekbar use imperative DOM updates instead of React re-renders
- Fix all usePlayerStore() calls without selectors across pages and
components (useShallow / individual selectors throughout)
- Remove filter:blur and transform:scale from Hero, AlbumDetail and
FullscreenPlayer backgrounds — eliminated expensive software
compositing layers on WebKitGTK
- Replace translate3d with 2D translate in FS mesh and portrait
animations; remove will-change:transform from mesh blobs
- Move Hot Cache section from Storage tab to Audio tab; group Preload
and Hot Cache under a shared 'Next Track Buffering' section with a
mutual-exclusivity note and auto-disable logic
- Add 'off' toggle to Preload (replaces Off button with toggle switch);
enabling either method now automatically disables the other
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Parse type and serverVersion from Subsonic ping; persist per-server identity and
run a background Instant Mix probe (random songs + getSimilarSongs) for
Navidrome 0.60+. Hide the AudioMuse server toggle when the probe returns no
similar tracks; clear prefs when the server is no longer eligible.
Artist pages fall back to Last.fm similar artists when AudioMuse is enabled but
the server returns none. Instant Mix failures set a per-server issue flag with a
settings warning and toast. Settings description links the official plugin repo
via i18n Trans.
Add per-server toggle for AudioMuse-style discovery: Instant Mix via
getSimilarSongs, server similar artists instead of Last.fm on artist pages,
and higher similar-artist count in Now Playing when enabled.
When browsing a single music folder, filter similar/top song results by
album ids from that scope (Navidrome does not apply musicFolderId to those
endpoints). Request larger similar batches under a narrow scope.
Keep radio-from-track as two blocks (shuffled top songs, then shuffled
similar-by-artist) so it differs from Instant Mix. Show Alpha badge beside
the setting. Add research/ to .gitignore.
- Revert _msg → msg in DecodeError match arm; the variable is used in the
debug_assertions eprintln! block so the underscore prefix caused a compile error
- Replace SlidersHorizontal with SlidersVertical (lucide-react) in PlayerBar,
LiveSearch, and AdvancedSearch so the EQ/filter icon shows vertical bars
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fix(fonts): update CSS var declarations to @fontsource-variable naming
('Inter Variable', 'Outfit Variable', etc.) so dynamic font switching works
- fix(layout): 100vh → 100% on .app-shell to fix Windows WebView2 playerbar drift;
1fr → minmax(0,1fr) in all grid-template-rows + remove min-height: 720px to fix
Linux playerbar disappearing at high zoom or small window sizes
- fix(updater): replace shell open() with Rust open_folder command to bypass
shell:allow-open capability scope blocking local paths on Windows
- fix(lyrics): add get_embedded_lyrics Tauri command (id3 crate for MP3 SYLT/USLT,
lofty for FLAC SYNCEDLYRICS/LYRICS); fix parseLrc regex for LRC without fractional
seconds; fix SubsonicStructuredLyrics to accept both synced and issynced fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the small corner toast with a centered modal that appears on
startup when a newer GitHub release is detected (4 s delay).
Features:
- Skip this Version: stores skipped tag in localStorage, reappears only
for newer releases
- Collapsible changelog: renders GitHub release body as markdown accordion
- OS-aware download: Windows → .exe installer, macOS → .dmg (aarch64
preferred), Linux Arch → AUR hint (yay/pacman), Linux other → .AppImage/.deb
- In-app downloader: Rust download_update command streams to ~/Downloads/,
emits update:download:progress every 250 ms for a real-time progress bar
- Post-download: Show in Folder button opens Downloads dir via shell.open
- Buttons: Download Now / Skip this Version / Remind me Later
Rust: check_arch_linux() reads /etc/arch-release + /etc/os-release
platform.ts: adds IS_MACOS, IS_WINDOWS alongside existing IS_LINUX
Settings About: Preview Update Modal button for testing
Fixes: renderInline regex had nested capture group causing undefined entries
in split() result → TypeError → React crash → WebKit white-screen freeze
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove currentTime from Zustand persist partialize: it caused 100ms
localStorage writes that grew with the queue, crashing WKWebView SQLite
after ~10 min of Artist Radio on macOS
- Trim old played radio tracks from queue (keep last 5) to cap localStorage
payload size during proactive radio top-up in next()
- Add reconnect logic for internet radio stall events (max 5 retries)
- Shuffle initial Artist Radio queue via Fisher-Yates so positions 2+
are drawn from similar-artist tracks, not predictable top-5 tracks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ZIP downloads (PlaylistDetail, Albums, NewReleases, RandomAlbums) now use
invoke('download_zip') via Rust streaming instead of fetch+blob+arrayBuffer,
eliminating JS heap saturation on large files. Progress shown in ZipDownloadOverlay.
Offline cache downloads (600+ songs) no longer freeze the UI: transient job
state moved to a separate non-persisted offlineJobStore, reducing localStorage
writes from ~1200 down to 2 for an entire download. Also adds playlist offline
toggle — clicking the cache button when already cached removes it from cache.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NowPlaying polling: only active while dropdown is open + respects
Page Visibility API — was firing every 10s unconditionally (~8.6k req/day)
- Connection check interval: 30s → 120s (4× reduction)
- Queue sync debounce: 1.5s → 5s (prevents burst on rapid skip)
- Rating prefetch: add 7-minute in-memory TTL cache for artist/album
ratings — repeated random album loads no longer re-fetch known ratings
Fixes server log flooding reported by users behind reverse proxies (Traefik).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bypass ImageCache 5-slot queue: fetch cover art directly, create blob URL
for canvas extraction, revoke immediately after use
- Module-level cache (artKey → accent) avoids re-fetching within the same
album — same-cover tracks get the color instantly
- Keep previous color visible on cache miss instead of flashing to theme color
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously a separate getCachedUrl call fetched the 300px cover art just
for color extraction, racing with the 500px fetch for display. Now reuses
resolvedCoverUrl directly — color appears as soon as the display image is
ready, with no redundant network request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New page /folders with macOS Finder-style column layout:
- getMusicDirectory / getMusicIndexes / getMusicFolders Subsonic API methods
- Root level uses getIndexes.view (music folder IDs are not getMusicDirectory IDs)
- Columns fill available width with flex: 1, min-width: 200px
- Auto-scroll to rightmost column on expand
- Cancelled-flag prevents stale state on fast navigation
- Clicking a track plays it in the context of the current column's tracks
- FolderOpen/Folder/Music icons, accent-colored selection, ellipsis truncation
- Hidden in sidebar by default (user can enable in Settings)
- i18n: EN DE FR NL NB ZH RU
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply a destination-in gradient mask after drawing the waveform bars so
both the left and right edges fade smoothly to transparent instead of
cutting off abruptly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CachedImage now shows /logo-psysonic.png when an image fails to load
instead of a broken icon. The fallback switches to object-fit: contain
with a card background so the logo stays proportional inside the container.
If the caller provides a custom onError (e.g. to hide the element), that
handler is used instead. A separate fallbackSrc state prevents React from
re-writing the broken URL on re-renders. The DOM-level onerror = null
guard prevents any infinite error loop.
extractCoverColors skips color extraction when the logo fallback is
active, falling back to the theme accent color instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pulsewave: draw flat played line only up to where the Gaussian envelope
starts (startX), eliminating overlap between the flat line and the wave.
Retrotape: replace dual fixed-position reels with a single spinning reel
that tracks the playhead position across the full seek bar width.
fix(i18n): remove literal 'N' from skip-star descriptions in all languages
Replace 'After N skips' with natural phrasing ('after several skips') in
EN, FR, NL, NB, ZH. Simplify ratingsSkipStarThresholdLabel to a single
word (Skips / Sauts / Overslagen / Hopp / 跳过次数). DE was already fixed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Time-based theme scheduler: auto-switches between day/night theme
based on configurable times; setInterval re-checks every minute
- UI scale slider (80–150%) via CSS zoom on document root; preset
buttons aligned to actual slider positions
- CustomSelect: scroll listener keeps dropdown anchored on scroll
- All features i18n in 7 languages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Song ratings in context menu + player bar, entity ratings (PR #130),
5 new seekbar styles, custom Linux title bar, album multi-select,
mix rating filter, top-rated stats, compilation filter, scroll reset.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Seekbar styles (5 new, by Psychotoxical):
- Neon Glow: transparent track, played section as multi-layer glowing neon tube
- Pulse Wave: flat line with animated gaussian sine pulse at playhead (rAF)
- Particle Trail: particles spawn at playhead and drift with glow (rAF)
- Liquid Fill: glass tube fills with liquid and animated wave surface (rAF)
- Retro Tape: two spinning reels connected by tape, shrink/grow with progress (rAF)
Ratings system (PR #130 by cucadmuh):
- Shared StarRating component with pulse/clear animations, disabled/locked states
- Entity ratings for albums and artists via OpenSubsonic probe (setEntityRatingSupport)
- Skip→1★: after N consecutive manual skips, set unrated track to 1★ (persisted per server)
- Mix rating filter: exclude low-rated songs/albums/artists from RandomMix, RandomAlbums, Hero, Home
- Batch refill in fetchRandomMixSongsUntilFull to fill list despite strict filters
- Prefetch artist/album ratings via getArtist/getAlbum for incomplete list payloads
- Settings: new Ratings section for skip threshold and per-axis mix filter stars
- i18n: all 7 languages (en, de, fr, nl, zh, nb, ru)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Apply the same per-axis star cutoff to random mix (multi-batch fetch until
full), random albums, Hero, and Home hero/discover rows. Prefetch artist and
album ratings via Subsonic when list payloads omit them. Exclude items when
0 < rating ≤ threshold; 0 or missing counts as unrated.
Settings: rating filter description interpolates sidebar menu labels; Russian
strings for custom title bar; short axis labels aligned across locales.
Add Ratings section under General: manual skip count before setting 1★,
and per-axis minimum stars for random mixes/albums (song, album, artist).
StarRating shows five stars with selection capped at three; shared max in authStore.
Queue filtering via passesMixMinRatings; OpenSubsonic-style entity rating fields on types.
Add five seekbar styles selectable in Settings → Appearance:
Waveform, Line & Dot, Bar, Thick Bar, and Segmented. Each option
shows an animated canvas preview using requestAnimationFrame.
Style is persisted in authStore (seekbarStyle, default: waveform).
Translations added for all 7 languages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Probe OpenSubsonic after connect; persist album/artist ratings via setRating when supported. Add shared StarRating (repeat same star to clear, pulse and shrink animations, hover freeze after clear). Widen tracklist duration column and bump narrow saved widths in localStorage. Use StarRating in AlbumHeader, track lists, and playlist rows.
Add Rust command `compute_waveform` that downloads the audio file,
seek-samples 500 evenly-spaced frames with Symphonia (fast path) and
computes peak amplitudes. Percentile-based normalisation (p5→p95)
preserves visible dynamics even for heavily compressed music.
Frontend caches results per track (module-level Map), shows the
pseudo-random fallback immediately and crossfades to the real waveform
over 400 ms (ease-in-out) once the computation finishes.
Also: Settings input-tab reset buttons now use btn-danger styling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Select multiple albums to add them to the offline cache or download
each as a separate ZIP. Action buttons appear inline in the topbar
when at least one album is selected; title shows selection count.
AlbumCard gains selectionMode/selected/onToggleSelect props with a
checkmark overlay and selected outline.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- set_decorations(false) on Linux at startup via #[cfg(target_os = "linux")]
- New TitleBar component: drag region + minimize/maximize/close buttons
- Currently playing track (▶/⏸ + artist – title) shown in the center
- app-shell grid gains a 32px titlebar row when data-titlebar is set
- IS_LINUX utility constant via navigator.platform
- macOS and Windows are completely unaffected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New sidebar item (TrendingUp icon, /most-played route) with:
- Top Artists section: derived by grouping frequent albums by artistId
and summing playCount — no extra API calls needed. Shows up to 10
artists as a responsive grid with avatar, rank and total play count.
- Top Albums section: paginated list using getAlbumList('frequent'),
50 albums per batch with a "Load more" button. Each row shows rank,
cover thumbnail (44px), album name, artist (navigates to artist
page), year and play count.
- Sort toggle (most plays first / fewest plays first).
Also adds playCount?: number to SubsonicAlbum — Navidrome returns this
field in getAlbumList2?type=frequent but it was missing from the type.
Closes#86
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracts the most vibrant pixel from an 8×8 downscaled album cover via
the Canvas API and applies it as --dynamic-fs-accent on the .fs-player
root element. All accent-colored FS player elements use
var(--dynamic-fs-accent, var(--accent)) as fallback so the theme accent
is restored automatically when the FS player closes.
Elements updated:
- .fs-track-title (color + text-shadow)
- .fs-btn.active, .fs-btn-heart.active/:hover
- .fs-btn-play and :hover (background + box-shadow)
- .fs-seekbar-played (background + box-shadow)
- .fs-mesh-blob-a/-b (radial-gradient via color-mix at 14%/8% opacity)
- .fs-art-wrap box-shadow glow (color-mix at 70% opacity)
- MicVocal lyrics-toggle inline style
Color safety: WCAG 4.5:1 contrast against near-black FS background
enforced by progressively lightening in HSL space (ensureContrast).
Pure math functions unit-tested with vitest (28 tests, 0 deps beyond
vitest itself).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds library scoping per server: users can select one Navidrome music
folder or 'All' from a new sidebar dropdown. Choice is persisted per
server ID in localStorage. A musicLibraryFilterVersion counter triggers
refetches across browsing pages when the scope changes.
- getMusicFolders() + libraryFilterParams() in subsonic.ts
- New fields in authStore: musicFolders, musicLibraryFilterByServer,
musicLibraryFilterVersion, setMusicFolders, setMusicLibraryFilter
- Sidebar dropdown via createPortal; collapses to icon in narrow mode
- API scoping applied to: getAlbumList2, getRandomSongs, getArtists,
getStarred2, search3
- Full i18n coverage (en, de, fr, nl, zh, nb, ru)
Review fixes applied (co-authored):
- Sidebar: title={} → data-tooltip + data-tooltip-pos='right'
- authStore: setMusicFolders merged into single set() call
- Locales: removed dead libraryScopeHint key from all 7 files
- Genres.tsx: removed musicLibraryFilterVersion dep (getGenres unscoped)
Co-authored-by: cucadmuh <cucadmuh@users.noreply.github.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Sidebar.tsx: title={} → data-tooltip + data-tooltip-pos="right" on
scope subtitle (CLAUDE.md rule: never use native title= for tooltips)
- authStore.ts: merge two set() calls in setMusicFolders into one atomic
update — avoids double render on folder list arrival
- locales (all 7): remove dead libraryScopeHint key (defined but never
rendered in any component)
- Genres.tsx: remove musicLibraryFilterVersion from effect deps —
getGenres is intentionally not folder-scoped, re-fetching on filter
change was a no-op waste
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove tauri-plugin-updater, relaunch_after_update command, generate-update-
manifest.js, the generate-manifest CI job, and all signing/upload steps for
.sig files and latest.json. The updater plugin, its pubkey and endpoint config
in tauri.conf.json, and the updater:default capability are all gone.
Replace AppUpdater.tsx with a lightweight component that:
- Fetches https://api.github.com/repos/Psychotoxical/psysonic/releases/latest
after a 4 s delay (no install, no download, no progress bar)
- Shows a dismissible toast with two buttons:
GitHub → releases/latest
Website → https://psysonic.psychotoxic.eu/#downloads
i18n: remove updaterInstall/Downloading/Installing/Download/ExperimentalHint
keys from all 7 locales; add updaterWebsite; update updaterVersion wording.
CI: remove generate-manifest job and all .sig signing/upload steps from
build-macos-windows. Also remove TAURI_SIGNING_PRIVATE_KEY env vars (no
longer needed). Release now just builds and uploads the installable binaries.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TooltipPortal clears the overlay when mousedown hits any [data-tooltip] anchor so click-open controls (e.g. library scope) do not leave a stale hover tooltip. The library scope trigger omits data-tooltip while the dropdown is open.
Apply musicFolderId across Subsonic requests, bump a filter version so library views reload, and add a fixed-position sidebar dropdown (with capped height when there are many folders).