* 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).
* fix(player): freeze main seekbar during track preview
Preview pauses the main sink in Rust while isPlaying stays true in the
store, so WaveformSeek's interpolation rAF must not advance progress.
* fix(audio): recover output after sleep and stalled streams
Add platform-specific post-sleep recovery hooks for Windows and Linux, and add a watchdog that reopens the output stream when playback is active but sample progress stalls, so audio can recover without restarting the app.
* fix(ui): remove card hover lift and smooth artwork zoom
Remove vertical hover translation from album and artist cards, and move image fade transition out of inline styles so cover zoom uses CSS timing consistently.
* fix(player): prevent seekbar jump after preview ends
Reset interpolation anchor timing when preview freeze state changes so the main seekbar does not momentarily jump forward before resyncing.
* fix(audio): reduce false watchdog recoveries and add diagnostics
Arm stalled-output recovery only after long poll gaps that suggest sleep/resume, and add detailed watcher logs for arm/clear/trigger paths to diagnose unintended stream reopens.
* chore(ui): drop card GPU hints and clarify macOS sleep scope
Remove translateZ and will-change hints from album and artist cover images to avoid per-card compositing overhead on software-composited Linux paths, and document why post-sleep recovery hooks currently target only Windows and Linux.
* docs(audio): document intentional Win32 callback pointer lifetime
Add inline rationale for the two Box::into_raw pointers in Windows suspend/resume registration so future maintenance does not treat the process-lifetime pointers as accidental leaks.
* docs(changelog): summarize playback stability updates for PR #476
Add a high-level changelog entry for preview seekbar fixes, sleep/wake audio recovery hooks and watchdog diagnostics, and card-hover stability adjustments from PR #476.
* docs(contributors): add cucadmuh entry for PR #476
Logs the post-sleep audio recovery, preview-seekbar fixes and card
hover stability work in the Settings → System → Contributors list.
* feat(settings): keep current active server when adding a new one
Adding a server from Settings no longer auto-switches the active server.
The new entry appears in the server list and is immediately usable, but
playback context, queue, and library view stay on the server the user
was already on.
The previous setLoggedIn(true) call was redundant — Settings is behind
RequireAuth, so isLoggedIn is necessarily already true at this point.
Login flow is unchanged: signing in on /login still selects that server,
which is the explicit intent of that screen.
* docs: changelog + contributors for PR #475
Adds the v1.46.0 "Changed" entry and the Psychotoxical contributors
line for the no-auto-switch-on-add-server behaviour.
* perf(ui): defer off-screen Artist grid paint + rAF overlay scrollbar
Apply content-visibility to artist tiles in .album-grid-wrap (aligned with
album cards). Coalesce OverlayScrollArea thumb updates to one requestAnimationFrame per scroll burst.
* perf(css): content-visibility on horizontal artist rails
Apply the same off-screen deferral as album cards to `.album-grid .artist-card`
(ArtistRow, Favorites, etc.).
* feat(perf-probe): add FPS overlay toggle and tidy probe modal
Add optional rAF-based FPS readout controlled by a persisted probe flag.
Remove the separate keyboard shortcut. Collapse all phase sections by default.
* perf(fps-overlay): subscribe only to showFpsOverlay flag
Add usePerfProbeFlag so the overlay does not re-render when other probe
toggles change. Track the animation frame id with a loop-local variable.
* feat(covers): race sibling downscale vs fetch, search thumb priorities
Run getCoverArt and client downscale in parallel when another size of the
same cover is cached; first successful result wins and aborts the other path.
Await both branches so inflight bookkeeping does not detach early.
Extend the cover cache size roster so provisional siblings resolve for sizes
used in the UI (e.g. 400/600/800, 48/96).
CachedImage: fetchQueueBias for live/mobile search (artist thumbnails ahead of
albums in fetch-slot ordering); configurable observeRootMargin with a wider
default to prepare priority slightly before elements enter view.
Mobile search adds round artist-thumb styling; add shared cover blob downscale
helper.
* perf(image-cache): batch sibling IDB reads and guard cover size registry
Use one read transaction when probing IndexedDB for sibling cover keys.
Extract COVER_ART_REGISTERED_SIZES and add Vitest coverage so every literal
coverArtCacheKey(_, size) in src stays aligned with sibling invalidation.
Honor AbortSignal during JPEG encode in downscaleCoverBlob.
PR #419 replaced the small (10px) Play icon next to the active queue
title with an animated 3-bar eq-bars block prefixed before the row.
Restore the original Play-icon-in-title behaviour and drop the unused
isStorePlaying selector.
* Enhance CachedImage and ArtistDetail components with improved image caching and priority handling
- Refactor CachedImage to utilize a priority system for image loading based on viewport visibility, improving performance during scrolling.
- Update useCachedUrl to accept an optional getPriority function for better cache management.
- Optimize ArtistDetail and Artists components by using useMemo for cover art URLs, reducing redundant calculations and improving rendering efficiency.
- Adjust image loading logic in CachedImage to ensure smoother transitions and avoid unnecessary fetch requests.
* perf(ui): unblock IDB cover art, stabilize mainstage rails and virtual lists
Let IndexedDB reads bypass the network concurrency slot so cached thumbnails
paint without queueing behind remote fetches; debounce disk eviction during
heavy scrolling.
Fix mainstage horizontal rails: dedupe album/song ids for React keys, widen
artwork budget overscan, avoid resetting the budget on list append, and raise
Home initial artwork budgets. CachedImage treats already-decoded images as
loaded; rail cards load cover images eagerly.
Refresh dynamic color extraction and extend virtual scrolling / scroll roots on
Albums, Artists, Playlists, and related surfaces.
Remove local agent-only commit instructions from the repository tree.
* perf(virtual): viewport-based overscan for main scroll lists
Drive TanStack Virtual overscan from measured scroll height so each list
renders about one screen of extra rows above and below the viewport for
snappier scrolling on Albums, Artists (list mode), and Tracks virtual song list.
Introduce useResizeClientHeight helpers (ID + ref) for ResizeObserver-based
clientHeight tracking.
* docs(changelog): note PR #468 UI cover cache, rails, and virtual lists
Add a coarse summary under 1.46.0 Changed for cover-art pipeline,
mainstage rails, viewport-based overscan, and library/chrome polish.
* 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
* 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
* 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>
* chore: upgrade dependencies and migrate playback to rodio 0.22
Bump npm and Rust crates; adapt symphonia decoding, ringbuf 0.5, lofty tags,
and discord-rich-presence usage. Use native rodio Player/MixerDeviceSink and
cpal device descriptions; drop the unused cpal patch. Align Vite 8 build
targets and chunking; remove redundant dynamic imports and fix hot-cache debug
logging imports.
* perf(build): lazy-load routes and restore default chunk warnings
Lazy-load all routed pages with React.lazy to shrink the main bundle; wrap root
Routes in Suspense for lazy Login. Drop chunkSizeWarningLimit override so Vite
uses the default 500 kB threshold.
* fix(windows): tray double-click without spurious menu; clean unused import
Disable tray menu on left mouse-up on Windows so a double-click to hide the
main window does not immediately reopen the context menu (tray-icon default
menu_on_left_click). Gate std::fs in app_api/core behind cfg(linux) for
/proc-only code so Windows builds stay warning-free.
* fix(sidebar): preserve new-releases read state under storage cap
When merging seen album ids, keep the current newest sample first so the
500-id localStorage limit does not truncate freshly marked reads and bring
back the unread badge.
* fix(audio): hot-cache replay, analysis no-op skips, playback source UI
Retain stream_completed_cache across audio_stop so end-of-queue replay can
use RAM promote or disk hot file instead of re-ranging HTTP.
Add cpu_seed_redundant_for_track gate before file/bytes seeds and local-file
spawn; emit analysis:waveform-updated only on Upserted. Ranged/legacy promote
checks generation after await before filling the slot.
Frontend: promote on same-track and cold resume; set currentPlaybackSource on
resume, queue undo restore, and gapless track switch so cache/stream icons stay
accurate. Import tauri::Manager for try_state in audio_play.
* fix(ts): narrow activeServerId for hot-cache promote calls
promoteCompletedStreamToHotCache expects a string; bind non-null server ids
in repeat-one, playTrack prev/same-track, and cold resume paths so tauri
production build (tsc) succeeds.
* fix(player): handle same-track hot-cache promote promise chain
Add .catch for promoteCompletedStreamToHotCache → runPlayTrackBody so sync
throws and unexpected rejections do not surface as unhandled in DevTools;
reset defer-hot-cache prefetch and isPlaying on failure.
* chore(nix): sync npmDepsHash with package-lock.json
* chore(release): finalize 1.46.0 CHANGELOG with PR #463 links
Document the release with full GitHub PR #463 on every subsection so
entries stay attributable if sections are reordered. Fix ContextMenu
lines where dynamic imports were accidentally merged onto one line.
* docs(contributors): credit cucadmuh for #463
The track-change reset wiped lineRefs/wordRefs in its effect body, which
ran *after* the commit phase that just populated them on remount. The
tracker effect then saw an empty refs array, no-op'd the DOM update, but
still advanced prevActive — so every following progress tick early-returned
on `prev.line === lineIdx` and the highlight froze.
Track the previous track id in a ref and skip the reset on initial mount,
so refs only get cleared on an actual track change.
* feat(linux): optional native GDK for Nix gdk-session
Introduce PSYSONIC_ALLOW_NATIVE_GDK so main skips the default GDK_BACKEND=x11
pin when the Nix gdk-session wrapper sets the flag. Remove GDK_BACKEND from
the npm tauri:dev script so it does not override nix develop defaults.
* fix(ui): portal server switch menu above sidebar
Main column stacks below the sidebar (layout z-index), so an in-tree dropdown
could never win over the left nav. Render the menu via createPortal to
document.body with fixed coordinates, matching the library scope picker.
* feat(perf): add mainstage probe controls and cut WebKit repaint load
Add a dedicated performance probe surface for mainstage/home toggles and wire Linux CPU diagnostics to isolate expensive UI paths. Tune waveform drawing and Home artwork clipping/windowing so visible content loads immediately while reducing WebKit compositor pressure during playback.
* fix(perf): stop hero rotation when section is off-screen
Gate hero auto-rotation and backdrop crossfade by real viewport visibility using the actual scrolling ancestor. This prevents periodic 10-second CPU spikes from hidden hero updates while preserving normal behavior when the hero is visible.
* fix(perf): isolate player progress updates from mainstage diagnostics
Add probe toggles for PlayerBar waveform and live progress UI updates to confirm playback progress churn as the main CPU driver.
Restore Home artwork quality defaults and keep visual-degradation modes opt-in via debug flags only.
* fix(hero): resume background and autoplay after viewport return
Re-check hero visibility on focus/visibility changes and add a short recovery poll while off-screen so missed scroll/RAF events cannot leave hero animation paused.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(perf): decouple playback progress from mainstage compositing pressure
Throttle audio progress delivery and route live seekbar timing through a lightweight progress channel to cut focus-time WebKit CPU spikes.
Add focused diagnostics in Performance Probe and restore hero/waveform behavior so visuals remain stable while profiling.
* fix(debug): open performance probe with Ctrl+Shift+D
Replace logo-triggered opening with a keyboard shortcut and keep logo purely decorative to avoid accidental probe activation.
* docs(changelog): document experiment/performance probe and playback work
Add an [Unreleased] section for the performance probe, throttled audio
progress IPC, snapshot-based live UI updates, WaveformSeek scheduling
over the same canvas bar renderer, Hero/Home rail fixes, and Linux/Nix
GDK dev ergonomics.
* perf(linux): add WebKit probe, throttle progress IPC, snapshot playback UI
Ship Performance Probe (Ctrl+Shift+D), Rust-throttled audio:progress, a
playback progress snapshot channel with coarse Zustand timeline commits,
Linux /proc CPU readout for the probe, Hero and Home rail artwork fixes,
Tracks SongRail windowing parity, MPRIS cleanup, gated perf counters, and
WaveformSeek paused-seek correctness. Documented in CHANGELOG for PR #452.
* docs(changelog): fold perf work into 1.45.0 and refresh date
Drop the separate 1.45.1 heading; keep PR #452 notes under 1.45.0 Added and
set the section date to 2026-05-04. Restore the safety preface before the
versioned sections.
* docs(changelog): order 1.45.0 Added entries by PR number
Sort the 1.45.0 release notes so subsections follow ascending PR id (390
through 452), with PR #452 last.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
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.
* 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
* 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
* 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
The drag-row-outside-queue-to-remove feature shipped in #420 (by
cucadmuh) had its QueuePanel.tsx wiring wiped when #419 was squash-
merged from a branch that pre-dated #420. The DragDropContext and
MiniPlayer parts of #420 survived; only QueuePanel.tsx was overwritten.
Restored to the state on commit 274ac5b3:
- Import `registerQueueDragHitTest` from DragDropContext
- Subscribe to `removeTrack` from playerStore
- Register the queue rect as a hit-test region on mount
- Document-level psy-drop listener that removes the dragged row when
the drop coordinates fall outside the queue rect
Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
* fix(audio): frame-align gapless-off track-separation silence
The 500 ms silence prepended between tracks when gapless playback is
disabled and the previous track ended naturally was built with
`Zero<f32>::new(ch, sr).take_duration(500ms)`. Rodio's `TakeDuration`
computes its sample count via integer-nanosecond division
(`1_000_000_000 / (sr * ch)`), which truncates: at 44.1 kHz / 2 ch
this emits 44103 samples = 22051.5 frames, half a frame short.
That half-frame leak shifts the next source's L/R parity in the
device frame stream. Multiple users have reported the next track
playing only on the right channel — exactly when gapless is OFF and
the previous track ended naturally (manual skip and album-first-play
bypass the silence prepend, which matches the reproducer report).
Replace with `SamplesBuffer::new(ch, sr, vec![0; frames * ch])`
where `frames = sr / 2`. Frame-aligned by construction, same
audible effect.
* docs(changelog): add #439 mono-channel fix entry
* chore(credits): add #439 to Psychotoxical contributions
* docs(changelog): strip @ from non-contributor mention in #435 entry
Plain-text 'zunoz on Discord' instead of '@zunoz' so GitHub does not
attribute the requester as a contributor on subsequent merges.
The blur-pause selector added in #434 included `.eq-bars .eq-bar`,
which caused the now-playing equalizer indicator in the queue to
freeze whenever the window lost OS focus (alt-tab, hover-focus
WMs, DevTools opening on WebKitGTK). Three small scaleY transforms
cost effectively nothing GPU-wise, so dropping them from the pause
list trades negligible idle GPU for a much less broken-looking UI.
* 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
* 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).
* fix(css): scope data-app-blurred animation pause away from `*`
The `data-app-blurred="true"` rule used a `*` selector to pause every
animation while the window was unfocused. On WebKitGTK + no-compositing
(Linux dev mode) this triggered a stale rendering bug after Vite HMR
reloads — the sidebar and main content stayed invisible until any user
interaction nudged a re-render.
Splitting the rule:
* `data-app-hidden` and `data-psy-native-hidden` keep `*` because the
window is fully invisible to the user in those states.
* `data-app-blurred` now lists only the concrete heaviest infinite
animations (eq bars, marquees, np dot pulse, fullscreen mesh blob /
portrait, generic spin). The other small animations keep running
while blurred — minor GPU cost compared to the broken HMR experience.
The JS-side `__psyBlurred` flag in WaveformSeek is unchanged.
* docs(changelog): add #434 entry to [1.45.0] / Fixed
* 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 5662dafe state for all 8 supported
languages (en, de, fr, nl, zh, nb, ru, es).
* fix(i18n): restore statistics.export* locale strings dropped by #419 squash
Same root cause as the previous commit on this branch: the #419
squash-merge silently dropped 18 statistics.export* keys (introduced
by the Stats-Card-Export PR #425) from en.ts and de.ts. Stats →
"Share Top Albums" modal and the export button tooltip showed raw
keys.
Restoring all 18 keys verbatim from the 5662dafe state in en + de.
Note: PR #425 only ever shipped en + de translations for these keys —
the other 6 locales (fr, nl, zh, nb, ru, es) never had them and are
not in scope for this restore. i18next falls back to en for those, so
users on those languages see the English strings rather than raw keys.
A follow-up to fully translate the export modal across the remaining
locales is tracked separately.
After PR #316 split the 'waveform' seekbar style into 'truewave' /
'pseudowave', users who carried any other unrecognised persisted value
(legacy variants, undefined, tampered strings) ended up with a blank
seekbar — the rehydrate migration matched only the literal 'waveform'
string, and the drawSeekbar dispatcher had no default branch, so an
unknown style drew nothing.
Two-layer fix:
* authStore migration now treats *any* value not in the current
SeekbarStyle union as legacy and resets it to 'truewave'.
* drawSeekbar gains a default case that falls back to the truewave
renderer, so even if a future style mismatch slips through the
store-level guard the user still sees a usable seekbar.
Visible to affected users on next app start (rehydrate runs once per
session). Clicking a style in Settings has always worked around the
issue; this fix removes that workaround.
The squash-merge of PR #419 was performed against an outdated PR base
that predated several main-side refactors and features. The resulting
squash inadvertently re-introduced files that had already been removed
(`src-tauri/src/audio.rs` monolith, `app-icon.png`) and reverted main's
content for ~20 files (`src-tauri/src/lib.rs` decompose, `src/App.tsx`
animation-pause, `src/components/AlbumRow.tsx` headerExtra, etc).
This commit:
* Restores all collateral-damage files to their pre-#419 main state
(5662dafe), including the audio module split, lib decompose,
Cargo.toml `windows` dep, animation-pause-on-blur logic, and
`reducedAnimations` toggle plumbing in WaveformSeek/Settings.
* Keeps the genuine #419 work intact: tri-state duration toggle,
position counter, persistent Now Playing collapse, animated EQ
indicator (in QueuePanel.tsx, 8 locales, authStore, components.css).
* Merges authStore.ts so both `reducedAnimations` (from #426) and
`queueNowPlayingCollapsed` (from #419) coexist.
* Adds the `.eq-bars.paused` CSS rule manually since components.css
needed a 5662dafe base + the single #419 addition.
* Fixes a latent type error in OverlayScrollArea.tsx (`useRef<T>(null)`
is `RefObject<T>` / read-only under current `@types/react`; widened to
`useRef<T | null>(null)` so the existing `wrapRef.current = el`
assignment compiles).
Verified locally with `cargo check` and `npm run build` — both green.
* 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>
* 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 (9cc74a7) and the lib-decompose refactor
(cfeec22):
- `audio/device_watcher.rs` lost the `output_enumeration_includes_pinned`
import. Add it cfg-gated to `not(target_os = "linux")` since it's only
called inside the non-Linux pinned-device fallback path.
- `lib_commands/sync/tray.rs::is_tiling_wm()` is `#[cfg(target_os = "linux")]`,
but its Tauri command wrapper `is_tiling_wm_cmd()` is unconditional.
Add a `#[cfg(not(target_os = "linux"))]` stub returning `false`.
No behavior change on Linux. Restores `cargo build` on Windows + macOS.
* perf(ui): pause cosmetic animations when window loses OS focus
Companion to the WASAPI MMCSS fix in this branch — issue #334
reported audible audio stutter on low-end laptops, partly traced
to WebView2 still compositing CSS animations + waveform canvas at
full rate while the user has alt-tabbed into another app.
Existing `data-app-hidden` / `data-psy-native-hidden` flags pause
animations only when the window is fully hidden (minimized or
behind a tray). When the window is visible-but-unfocused — the
common "music in the background while I work" case — every infinite
keyframe + 60fps rAF loop keeps running.
Add a parallel `data-app-blurred` flag driven by `window.focus`/
`window.blur`, plus matching `__psyBlurred` on the global window
object for imperative checks. The CSS rule that pauses every
`animation-play-state` on hidden gets a third selector for blurred.
WaveformSeek's two 60fps rAF loops (animated styles + settings demo)
extend their existing `document.hidden || __psyHidden` short-circuit
to also include the new blurred flag, falling back to the same
400ms polling path that already exists for hidden.
Verified visually on the dashboard: alt-tab → `<html
data-app-blurred="true">`, mesh blobs in the fullscreen player +
marquee + waveform 60fps loop all pause. Click back into the
window → blob/marquee/waveform resume immediately.
Cross-platform: focus/blur events fire reliably on all targets, so
the optimization applies to Windows, Linux (incl. WebKitGTK with
software compositing where the savings are largest), and macOS.
* chore: remove stale app-icon.png from repo root
* perf(ui): add 'Reduce animations' toggle that caps animated seekbar styles to 30 fps
Off by default. When on, animated seekbar styles (pulsewave, particletrail,
liquidfill, retrotape) skip every other rAF and advance the animation timer by
a doubled delta, so wave speed is unchanged while draws are halved. Aimed at
low-end Windows GPUs where the 60 fps shadow-blur loop drives ~40 % WebView2
GPU usage even when focused.
Toggle lives directly under the seekbar style picker in Settings → Appearance.
i18n in all 8 supported languages.
* 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
* fix(preview): sync audio start, ring animation, and download timeout
Three coupled fixes for the track-preview engine:
1. Audio sync. `Sink::try_seek` was running on a worker thread after
`sink.append(source)`, so the sink began playing position 0 while
the seek was still iterating to the mid-track target. With the
30 s `take_duration` cap counting wall-clock from append, audio
could only become audible ~25% into the preview window. The seek
now runs on the bare source before append, then `take_duration`
wraps it — playback starts at the seek position with the cap
measured from there.
2. Ring animation gating. The CSS progress-ring animation was
bound to `is-previewing` (set on click), so the ring sprinted
ahead of any download/decode/seek warmup and didn't reset
cleanly when switching from one preview to another. Added an
`audioStarted` flag in `previewStore` that flips on
`audio:preview-start` from the engine; CSS animation is now
gated on `audio-started` instead. `is-previewing` still drives
tooltip/icon for instant click feedback. Same SVG is reused for
a 25%-arc rotating loading spinner while waiting for audio,
with a 150 ms delay so cached/short previews don't flash.
3. Download timeout. The shared `audio_http_client` caps at 30 s,
which aborts mid-download on multi-hundred-MB uncompressed
files (e.g. 18-min Hi-Res WAV ~600 MB). The preview engine now
builds a dedicated client with a 5 min timeout for the bytes
fetch. Watchdog still bounds the playback window at 30 s once
the audio actually starts.
Touches `audio/preview.rs`, `previewStore.ts`, `components.css`
plus the eight tracklist/player-bar components that render the
preview button.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): add preview audio sync fix for PR #423
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
* 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').
* 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.
* 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
* feat(themes): add Kanagawa, Atom One, 1984 palettes; group OSS Classics by family
Adds three upstream-faithful theme families to Open Source Classics and
restructures the picker so the section is no longer alphabetically wild.
New themes (9 total):
- Kanagawa (rebelot/kanagawa.nvim): Wave, Dragon, Lotus
- Atom One (Th3Whit3Wolf/one-nvim): Dark, Light
- 1984 (juanmnl/vs-1984): Default, Cyberpunk, Light, Orwell
(Fancy + Unbolded skipped — identical palette to Default, style-only)
Each theme defines the full token set (--bg-*, --accent, --text-*, all
--ctp-*, --waveform-*, --positive/warning/danger, --select-arrow), so
login screen, queue sidebar tabs, and all subpages inherit the palette
without component-level overrides.
Picker restructure:
- ThemeDef gains optional `family?: string`
- Open Source Classics regrouped: 1984, Atom One, Catppuccin, Dracula,
Gruvbox, Kanagawa, Nightfox, Nord
- Family headings rendered inline (grid-column: 1 / -1) when family
changes; new .theme-family-header style in components.css
- Theme scheduler dropdown labels prefixed with family for context
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(release): sync Cargo.lock to 1.45.0-dev
drawCurve read offsetWidth/offsetHeight unconditionally. When the canvas
sits inside a collapsed <details> (SettingsSubSection) on first paint,
both are 0, innerW becomes negative, the points-loop never runs, and
points[0][0] throws. Linux WebKitGTK happens to lay out the details
expanded on first render; macOS WebKit does not, so the same code path
unmounted the entire React tree because there is no ErrorBoundary above
Settings.
Bail early when innerW or innerH is non-positive — the ResizeObserver
already triggers a redraw once the canvas gets real dimensions.
Closes#382.
Whats New and the post-update modal required an exact ## [version] header,
so prerelease package versions (rc, dev) often had no matching section.
Match on major.minor.patch first; if several sections share the core,
prefer a plain X.Y.Z heading when present.
* 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>
Apply mix rating settings to infinite queue candidates and keep fetching random batches when filtered results are below the target size, so top-ups can still reach five tracks when possible.
* feat(player): persist queue panel visibility across restarts
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(audio): drop unused resolved_loudness_gain_db binding
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
* fix(player): Lucky Mix respects mix rating filter and harden rating reads
Wire Lucky Mix through the same Settings → Ratings filter as Random Mix
(enrich + passesMixMinRatings). Prefetch artist/album ratings via
getArtist/getAlbum using both userRating and Navidrome-style rating,
and bump the in-memory prefetch cache key so stale misses are not reused.
Resolve artist entity id from OpenSubsonic artists[] or contributors[]
when top-level artistId is missing; read rating on child song and artist
refs so thresholds apply consistently.
* chore: clarify JSDoc for mix rating enrich vs Lucky Mix filter
Store the main queue panel viewport scrollTop in undo snapshots and
reapply it after undo/redo so Ctrl+Z / Ctrl+Shift+Z restores scroll
position alongside queue state.
Snapshot queue, current track, time, and pause before queue mutations.
Ctrl+Z/Cmd+Z restores prior state; Ctrl+Shift+Z/Cmd+Shift+Z reapplies undone
edits when the redo stack is non-empty; new edits clear redo.
Resync the Rust audio engine only when the current song identity changes
from the pre-apply state so reorder/enqueue-style edits keep live playback.
Register the document capture listener from main.tsx after the window label
is set. Mini player forwards undo/redo via mini:undo-queue and mini:redo-queue.
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>