mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
707a41f615577be3afe0f8091859ce13ad937c7c
369 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6c5465e9c7 |
fix(orbit): hysteresis on Catch Up visibility so the button stays clickable (#527)
* fix(orbit): keep Catch Up button visible long enough to click Reported during testing: the Catch Up button "appears for a moment, then disappears too fast to click". The single-stage debounce only filtered the show direction (require ≥ 3 s sustained over-threshold before showing), but on a real-world high-latency session the genuine drift fluctuates between ~1 s and ~8 s in lockstep with both sides' chunked `currentTime` updates — so the button vanished as soon as drift dipped briefly under 3 s, even though the drift baseline was still 5–8 s. Add a hide-side hysteresis: once visible, the button stays visible until drift has been under a tighter 1 s threshold for ≥ 1 s. Otherwise the noisy 1–3 s drift valleys keep the button up so the user can actually click it. Constants left as locals; if testing wants different floors we can extract. * docs(changelog): add catch-up hysteresis bullet * fix(orbit): show Catch Up when host paused + serialise seek-then-pause Two follow-ons after the hysteresis change: **1. Show Catch Up even when the host is paused.** The visibility gate required `state.isPlaying === true`, but a guest who joined while the host was paused still benefits from Catch Up if their sync to the host's paused position failed (engine drop, brief network blip during initial-sync, manual local seek). Drop the `state.isPlaying` gate — the only signal that should matter is "drift between us and the host's reported state" — which `computeOrbitDriftMs` already computes correctly in the paused case (no time-extrapolation, just `guestPos - hostPos`). **2. Defer the play-state mirror after `player.seek`.** Reported symptom: "I press Catch Up, the player pauses; when I press play, it resumes from the *old* position and the waveform jumps back." Root cause: `player.seek` debounces `audio_seek` via `setTimeout(0)`, while `player.pause` / `player.resume` fire their invokes synchronously. Pause arrives at the engine first, leaving it paused at the pre-Catch-Up position with the seek queued behind. When resume happens later, the engine plays from the old spot. Push the play-state mirror behind a 200 ms `setTimeout` so the seek's invoke lands first. |
||
|
|
27c740b760 |
fix(orbit): kick a fresh playTrack when engine is stuck mid-load (#526)
* fix(orbit): kick a fresh playTrack when engine is stuck mid-load Follow-up to #525. Symptom: occasionally on join, the guest gets no audio until the next host-driven track change, at which point a fresh `playTrack` runs and audio plays normally. Root cause: when the initial `syncToHost` poll hits its 5 s deadline without the engine reporting `isPlaying === true` (slow Navidrome cold-start), the next pull tick takes the cheap "track already loaded" shortcut and calls `applyMirror`. `applyMirror` fires `seek` + `resume` on an engine that is stuck in a "loaded but never started" limbo — seek silently no-ops and `resume` can't kick a track that never began. Guest is silent until something else triggers a fresh `playTrack`. Tighten the shortcut: take the cheap path only when the engine is already in the state the host expects (or playing while host is paused, which is fine to align via pause). Otherwise fall through to a fresh `playTrack` and the existing 5 s ready-poll, which re-initialises the engine and lets audio actually start. * docs(changelog): add engine-limbo follow-up bullet * fix(orbit): re-sync when engine silently fell back to paused The optimistic `isPlaying: true` `playTrack` writes synchronously masks a `audio_play` failure: the post-playTrack poll sees the optimistic flag, fires `applyMirror`, and the outer tick records `lastAppliedRef = { ..., isPlaying: true }` as a successful sync. But if the underlying `invoke('audio_play')` rejects later (network blip / cold-start exhaustion), the catch handler flips the flag back to `false` and schedules `next()`, which short-circuits to `audio_stop` in an Orbit guest — leaving the player silent while `lastAppliedRef` still claims we're playing. None of the divergence-detection branches (track-change / play-pause-flip) match, so the guest never re-syncs. Add a recovery check before the if/else-if chain: when the captured `last` says we applied playing, the engine is currently not playing, and the host is still playing the same track, reset `lastAppliedRef.current = null`. The next iteration of the chain re-runs initial-sync (with the 500 ms fast-poll cadence) which fires a fresh `playTrack`. If `audio_play` succeeds the second time, audio finally starts; if it keeps failing, we loop with no extra harm (audio was already silent). Adds an `engine-recovery` event scope to the diagnostics buffer so future captures can see when this re-sync fired. * docs(changelog): expand engine-recovery bullet to cover both cases |
||
|
|
af1b9661f5 |
fix(orbit): three interlocking guest playback bugs (#525)
* fix(orbit): guest short-circuits queue-exhaustion fallback paths
When a guest's local queue runs out (single-track queue from `syncToHost`
empties on `audio:ended`), the player walks the standard fallback chain
in `next()`: radio top-up → infinite-queue → stop. The infinite-queue
branch builds a 6-track queue and calls `playTrack`, which trips
`orbitBulkGuard` and pops a "Add 6 tracks to the Orbit queue?" modal.
Hitting Cancel leaves playback frozen; "Add them all" injects unrelated
tracks into the host's shared queue.
In an active Orbit guest session the host owns the queue. Skip the
fallback paths entirely and just stop — the next `useOrbitGuest` pull
tick will sync to whatever the host advanced to.
Bonus side-effect: kills the deferred-promise race where a
`buildInfiniteQueueCandidates().then(...)` from a guest's track end
could resolve *after* a Catch Up replaced the queue and pop the modal
a second time against the now-current 1-track queue.
* fix(orbit): treat natural track-end as not-diverged in guest sync
When a guest's track ended naturally before the host advanced, the
divergence-detection branch read `player.isPlaying === false` and
classified it as the user manually paused — so it refused to load the
host's next track. The guest sat silent until they clicked Catch Up.
`handleAudioEnded` keeps `currentTrack` pinned to the just-ended track
and resets `currentTime` to 0, while a real manual pause leaves
`currentTime` somewhere mid-track. Use the 0-position discriminator to
classify natural-end as not-diverged so the host's new track loads.
Confirmed via the captured guest log buffer:
18:43:08.598 [track-change] host: VJkV5… → 6i6RP… BUT guest diverged
(player.isPlaying=false ≠ last.isPlaying=true)
— guest stuck for ~33s until Catch Up was pressed.
* fix(orbit): Catch Up polls until engine is ready before seeking
The 400 ms blind setTimeout in `onCatchUp` was too short for an
HTTP-streamed cold-start on high-latency links. If the audio engine
wasn't ready by then, `seek(fraction)` silently no-oped and playback
started at 0:00, making Catch Up effectively useless on exactly the
slow links where it's needed. Captured log shows a Catch Up bringing
the guest to posSec=30, then 5 s later the guest was at posSec=6
(playback restarted from the head).
Replace with the same poll-until-ready pattern `syncToHost` already
uses: check every 100 ms, fire the seek as soon as the engine reports
playing, fall back to a blind apply at the 4 s deadline.
* docs(changelog): add orbit guest playback fixes entry
* fix(orbit): debounce Catch Up button + match bar item height
Two follow-on UX fixes after PR #525's three primary bugs landed:
1. **Debounce visibility.** Drift is computed from an asymmetric signal:
guest's `currentTime` updates in coarse ~5 s chunks, while host's
position is extrapolated linearly via `(nowMs - posAt)`. Even on a
perfectly-synced session the diff swings ±5 s every tick, so the
button flickered in and out continuously. Show only after drift has
stayed over the 3 s threshold for ≥ 3 s of wall clock — measurement
noise is filtered out, real sustained drift still surfaces in time.
2. **Match neighbour height.** The button was 32 px tall against 26 px
for the other action buttons (.orbit-bar__settings) so every flicker
shifted the entire bar height. Set `height: 26 px` and tighten the
padding/font so the layout is stable regardless of visibility.
* fix(orbit): tighten queue-extension lockout + reliable initial-sync seek
Two follow-on fixes after the 4-bug umbrella:
**1. Local queue-extension paths fully off during Orbit.**
Phase check broadened from `active` to cover `starting` / `joining` /
`active` so a fetch-then-join race can't pop the bulk-add modal *after*
the join. The proactive infinite-queue topper inside `next()` (which
fires when ≤ 2 auto-tracks remain ahead) is now also gated, plus each
async `.then()` callback in the radio + infinite-queue paths re-checks
at resolution time. A `playTrack(... 6-track queue ...)` after the user
joined Orbit was the path that re-triggered the "Add 5 tracks?" modal
on a freshly-joined guest.
**2. `syncToHost` only seeks once the engine reports playing.**
The previous 2 s deadline-fallback applied the seek even when the
engine hadn't started, where the seek silently no-ops and the track
plays from 0:00. Symptom: clicking Catch Up makes the song "jump 50 %
forward" — that's the seek finally landing because the engine is now
ready, the initial-sync seek had already failed silently. New deadline
is 5 s, and on timeout we return `false` so the outer pull tick keeps
`lastAppliedRef` null and the 500 ms fast-poll retries.
* fix(orbit): double-click play button + hide preview during session
Two cucadmuh-flagged gaps:
**1. Double-click on the inline play button now reaches the orbit-add
path.** The album-track row's onDoubleClick already routes to
`addTrackToOrbit` when in Orbit, but the inline play button stopped
propagation on click — so clicking it twice just fired the "double-
click to add" hint toast and never touched the orbit queue. Add an
onDoubleClick on the button itself that delegates to the parent's
`onDoubleClickSong`.
**2. Track preview is suppressed during an Orbit session.** Preview
shares the Rust audio engine with the shared playback, so starting
one as a guest yanks the host's track out from under everyone. A new
`[data-orbit-active]` attribute on `<html>` (set whenever role is
host/guest and phase is starting/joining/active) hides every
preview button via a single CSS rule, and `previewStore.startPreview`
short-circuits as a defensive guard for keyboard shortcuts and any
programmatic callers.
|
||
|
|
a702a5dd5b |
feat(orbit): in-app diagnostics popover with copyable event log (#524)
* feat(orbit): in-app diagnostics popover with copyable event log Multiple users on Discord report Orbit guests stopping after the first song with no errors anywhere — Settings → Debug → Export Logs is too buried for non-technical reporters, and the relevant code branches have no logging at all (silent fail). This adds a one-click "Copy log" path right inside the Orbit session bar. The new Activity-icon button next to Help opens a popover with: - Live mini-display: role, host vs. guest track id + position, drift, age of the host's last state write — all updating once a second. - Scrolling event log textarea fed by an in-memory ring (200 events). - Copy + Clear buttons. Copy formats `[ISO] [scope] body` lines and drops them on the clipboard — paste straight into a Discord report. Instrumentation lands at the previously-silent decision points: - Guest pull tick: full snapshot of host vs. guest state on every read. - Each branch of the divergence detection in `useOrbitGuest.ts` logs which path it took and why (initial / track-change-followed / track-change-diverged / play-pause-flip), making the "stuck after first song" symptom diagnosable from the buffer alone. - Host pushes log track id, isPlaying, queue length, guest count. Events are also bridged to the existing `frontend_debug_log` Tauri command when Settings → Logging is on Debug, so power users still get the same data in `psysonic-logs-*.log` for offline triage. i18n: full `orbit.diag.*` namespace in all eight locales. EN + DE are native; ES / FR / NB / NL / RU / ZH are first-pass and may want a polish from native speakers later. * docs(changelog): add orbit diagnostics popover entry |
||
|
|
874b0c67ae |
fix(context-menu): drop inline z-index that hid menus under floating player (#522)
* fix(context-menu): drop inline z-index that hid menus under floating player
The main context menu wrapper carried an inline `zIndex: 999` that
overrode the `.context-menu { z-index: 10000 }` stylesheet rule. The
floating player bar sits at z-index 1000, so when a menu opened near
the bottom of the screen — long enough or anchored low — the player
bar covered it.
Removing the inline override lets the stylesheet rule (10000) win, so
the menu always paints on top of the floating bar. Submenus inherit
the same stacking context so they follow the parent menu.
Closes #521.
* docs(changelog): add context-menu floating-player z-index fix entry
|
||
|
|
fec513b629 |
fix(home): swap Because-you-listened rail to AlbumRow under 696 px (#520)
* fix(home): swap Because-you-listened rail to AlbumRow under 696 px The hero-style BecauseCards are tuned for full-rail widths (3 cards at 1052 px+, 2 cards at 696-1051 px). Below that the cards stretched full-width with a fixed 160 px cover stuck on the left and centred text floating in a wide empty area — looked like three over-sized banners stacked vertically instead of a compact recommendation rail. A `ResizeObserver` on the rail wrapper now watches the container width and below 696 px renders a standard `AlbumRow` (which is already perf-tuned for narrow rails: artwork budget, viewport windowing, scroll paging). Wide layouts keep the unchanged hero card layout, so the mainstage view at full width is identical to before. * docs(changelog): add Because-you-listened narrow-layout fix entry |
||
|
|
8b781a848d |
refactor(i18n): show language names as endonyms in picker (#514)
* refactor(i18n): show language names as endonyms in picker * docs(changelog): add language-endonyms entry for #514 |
||
|
|
c982362884 |
fix(i18n): OpenDyslexic font supports Cyrillic (#513)
* fix(i18n): OpenDyslexic font supports Cyrillic The OpenDyslexic subtitle in the font picker stated "no RU/ZH support", but the bundled `@fontsource/opendyslexic` 5.x ships Cyrillic glyphs and Russian renders correctly — verified empirically. Only Chinese (CJK) actually falls back to the system font. Updated the subtitle string in all 8 locales accordingly; the Russian locale itself no longer claims unsupported-self. * docs(changelog): correct OpenDyslexic locale-coverage claim (#513) The #507 entry in the unreleased v1.46.0 block claimed `Latin + Latin-extended only` and listed Cyrillic among the system-font fallbacks. Cyrillic glyphs ship in @fontsource/opendyslexic 5.x and render correctly — only Chinese (CJK) actually falls back. Updated the bullet in place to match the corrected subtitle hints from this PR. |
||
|
|
6f50fb6a19 |
feat(player-bar): album context menu on song title right-click (#512)
* feat(player-bar): album context menu on song title right-click Right-clicking the track title in the player bar now opens the same album context menu that album cards use (open, play next, enqueue, go to artist, favorite, rate, share, download, add to playlist). Mirrors the existing left-click behavior on the title, which already navigates to the album. Suppressed for radio and preview, matching the click handler. MarqueeText gains an optional onContextMenu prop; PlayerBar builds a SubsonicAlbum shape from currentTrack on demand. * docs(changelog): add entry for PR #512 (player-bar title context menu) |
||
|
|
dc2068303d |
chore(deps): bump Tauri 2.11.0 → 2.11.1 (GHSA-7gmj-67g7-phm9) (#509)
* chore(deps): bump Tauri 2.11.0 → 2.11.1 (GHSA-7gmj-67g7-phm9) Patches the IPC origin-confusion advisory: Tauri 2.11.0 and below could let a remote-origin page loaded inside the webview invoke local-only IPC commands. Severity medium. Psysonic exposes a number of file-system and credential-bearing IPC commands (download_zip, nd_get_song_path, audio_*), so closing the gate is worth the lockfile-only bump. Cargo.toml is unlocked at "2", so this is purely a Cargo.lock refresh via `cargo update -p tauri --precise 2.11.1`. Full Tauri family bumped together (tauri / tauri-build / tauri-codegen / tauri-macros / tauri-runtime / tauri-runtime-wry / tauri-utils — all matching patch releases). wry, tao and other transitive deps unchanged. phf 0.11.3 fell out of the dependency graph because the new tauri-utils no longer pulls it. Tested locally (Windows): tray hide/restore + single-instance second launch + mini-player toggle + window-state persistence — all working. * docs: changelog entry for PR #509 Logs the Tauri 2.11.1 GHSA security bump in v1.46.0 "## Fixed". |
||
|
|
57fe847d71 |
refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic (#508)
* refactor(settings): collapse all sections, drop font dropdown, surface OpenDyslexic Settings opened on a tab where four or five sub-sections were expanded on first render — audio device, theme list, lyrics sources, sidebar customizer, random-mix copy, offline dir, language picker, keybindings table. The page felt like a wall of controls before the user had even looked for something specific. Removed every `defaultOpen` flag from the SettingsSubSection call sites so each tab now boots with only the section headers visible. Component default was already `false`. ThemePicker auto-expanded the group containing the active theme on mount. Same noise on a screen that already has the longest accordion list in the app, and the blue dot in the group header already tells the user which group holds the active theme. Initial open-group is now `null` — all groups collapsed until the user clicks one. Font picker had a dropdown-style button that toggled a list inside the sub-section, which meant two clicks (open the section, then open the dropdown) for what should be a one-click choice. Removed the button + the `fontPickerOpen` state — opening the Font sub-section now reveals the full list directly and a click sets the font without collapsing anything. OpenDyslexic moved to the top of the list so users with dyslexia don't scroll past 14 sans-serifs to find their option; the rest stays in the original order. * docs: changelog entry for PR #508 Logs the Settings collapse-by-default + font picker cleanup + OpenDyslexic ordering in v1.46.0 "## Changed". |
||
|
|
f520f7951a |
feat(settings): OpenDyslexic font option for dyslexic readers (#507)
* feat(settings): OpenDyslexic font option for dyslexic readers Next step on the accessibility track. The first pass was on the colour side — WCAG contrast audits across every theme and dedicated colour- vision-deficiency variants for the protanopia / deuteranopia / tritan- opia palettes. Typography is the other axis: some users with dyslexia find a font with a heavier weighted baseline and asymmetric glyph shapes (b/d, p/q never mirror, italic forms differentiated rather than slanted-regular) easier to track than a typical sans. Adds OpenDyslexic to the existing Fontsource font picker. SIL OFL licensed, freely redistributable, and the de-facto open-source standard for this use case. Non-variable axis, ships as four discrete weight/style files (regular, bold, italic, bold-italic) — the Settings picker grew an optional `hint` field on font entries so this one row can carry a "dyslexia-friendly · no RU/ZH support" subtitle without bloating the other 14 entries. Latin + Latin-extended only. Cyrillic and CJK locales (RU, ZH) fall back to the system font when this is selected; the subtitle calls out that limitation upfront. i18n: hint string in all 8 locales (settings.fontHintOpenDyslexic). Accessibility is intentional product positioning here — it's an underserved corner of the Subsonic-client ecosystem. * chore(nix): sync npmDepsHash with package-lock.json * docs: changelog entry for PR #507 Logs the OpenDyslexic font option in v1.46.0 "## Added". * docs(settings): contributor entry for PR #507 Adds the OpenDyslexic accessibility bullet to Psychotoxical's contributions list. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
bd56177e2c |
feat(home): Lossless Albums rail + dedicated page + sidebar nav (#506)
* wip(home): Lossless rail + dedicated /lossless-albums page Rail under Home > mostPlayed and a dedicated infinite-scroll page that list albums whose tracks are tagged in lossless containers. Walks Navidrome's native /api/song?_sort=bit_depth&_order=DESC, dedupes by albumId on the way down, stops when the song stream crosses into lossy (bitDepth==0) or the server runs out of rows. _filters has no operators on quality columns, so a sort + walk is the only path; equality on sample_rate / bit_depth probes returned empty (verified). The page paginates through the song-cursor with an in-flight ref so overlapping IntersectionObserver fires don't double-add albums, plus a cancelled flag so React StrictMode's double-mount doesn't apply two parallel result sets in dev. Suffix allowlist excludes ambiguous wrappers — m4a/m4b can be ALAC *or* AAC and Navidrome's response carries an empty codec field, so we can't tell them apart; same story for wma. Allowlist is flac, wav, aiff/aif, dsf/dff, ape, wv, shn, tta — containers that are only lossless. ALAC-in-m4a setups will miss out, acceptable trade-off without a reliable codec field. Status: WIP. Settings home-customizer label and en.ts strings landed, no other locales yet, no quality badge on AlbumCards across the rest of the app, no CHANGELOG. Branch was 'feat/home-hires-rail' during the earlier hi-res-only iteration before the lossless broadening. * wip(home): Lossless page header parity + streaming + sidebar nav Page header now mirrors All Albums: selection mode with three action buttons (Enqueue Selected, Add Offline, Download ZIPs) wired to the same handlers Albums.tsx uses, selection-counter title swap, and the perfFlags.disableMainstageStickyHeader respect path. Filters were intentionally skipped — the rail is sorted by bit_depth, mixing client- side filters with server-driven pagination would produce gaps. Loading feels noticeably faster: ndListLosslessAlbumsPage takes an optional onProgress callback that fires once per internal fetch with the entries discovered in that fetch, so the page can stream new albums into state instead of waiting on the whole loadMore. Page-side budget dropped from 5×200 to 2×100 songs per loadMore (~1 MB worst case vs 5 MB before), since the rail's catch-em-all pass is wrong for infinite-scroll UX. Subtitle under the page title primes the user that this is slower than other album pages because Psysonic walks the song catalog by quality (Navidrome ignores _fields, so per-song responses ship with lyrics + tags + participants whether we want them or not). Sidebar nav entry registered under 'losslessAlbums' with a Gem icon, defaults to visible:false (matches composers / folderBrowser / deviceSync — niche browsing modes). Existing users get the entry appended at the end of their persisted sidebar list automatically via the onRehydrateStorage merge that sidebarStore already runs for new DEFAULT_SIDEBAR_ITEMS. i18n: full coverage across all 8 locales for sidebar.losslessAlbums, home.losslessAlbums, losslessAlbums.empty, losslessAlbums.unsupported and the new losslessAlbums.slowFetchHint subtitle. ru/zh are machine-translation quality, flagged for a polish pass. * feat(home): default Lossless sidebar entry to visible Flips the DEFAULT_SIDEBAR_ITEMS entry for `losslessAlbums` from false to true. Existing installs keep whatever the user has in persisted storage; fresh installs see the entry in the sidebar from the start. Earlier wip commit defaulted it off (matched composers / folderBrowser / deviceSync as a niche browse mode), but the rail + page do show something useful for any library with at least one FLAC/WAV/etc. album, so off-by-default just hid the feature. * docs: changelog entry for PR #506 Logs the Lossless Albums rail + page + sidebar entry in v1.46.0 "## Added". * docs(settings): contributor entry for PR #506 Adds the Lossless Albums rail/page bullet to Psychotoxical's contributions list. |
||
|
|
a41e3a624a |
feat(song-info): show absolute file path on Navidrome via native API (#504)
* feat(song-info): show absolute file path on Navidrome via native API
Subsonic's `getSong.view` returns at most a relative path (or none on
Navidrome), so the Path row in the Song Info modal stayed empty for
most users. Feishin and the Navidrome web client surface the full
server-side path by hitting Navidrome's native `/api/song/{id}` instead.
Added an `nd_get_song_path` Tauri command that logs in to the native
API with the active server's credentials, fetches the song, and returns
the `path` string. Wired it into `SongInfoModal`: the Subsonic
`getSong` call still drives the rest of the dialog, and the native call
runs in parallel only when the active server's identity is
"navidrome". When it returns a path, that absolute path replaces the
relative Subsonic value; native-API failures are silent and the modal
falls back to whatever Subsonic provided.
No token cache yet — the modal is opened occasionally enough that one
fresh login per call is fine.
Closes discussion #479.
* docs: changelog entry for PR #504
Logs the Navidrome native-API absolute file path support for the
Song Info dialog in v1.46.0 "## Added".
* docs: settings contributor entry for PR #504, drop competitor mention
Adds the song-info absolute path bullet to Psychotoxical's contributors
list in Settings, and rewords the existing CHANGELOG entry so neither
text references competing clients.
|
||
|
|
726f3f0ff2 |
fix(radio): queue navigation, dedup, and similar-first variety (#500) (#503)
* fix(radio): queue navigation, dedup, and similar-first variety (#500) After a Radio session ran a while, three things broke: Queue navigation through duplicates. playTrack re-resolved the active queue index by `findIndex(t.id === track.id)`, returning the *first* matching id, so reaching the second occurrence of a track snapped queueIndex back to the earlier slot — highlight jumped and the next advance played the wrong follow-up. Added an optional `targetQueueIndex` to playTrack, threaded through next(), previous(), the audio:ended repeat-one path, queue-row click, and the queue-item context menu. findIndex stays as the fallback for callers that just have a track and a fresh queue. Queue accumulation. enqueueRadio didn't dedupe incoming tracks; the next() top-up deduped against the live queue but trimmed the played tail down to HISTORY_KEEP=5, so a song heard 8 ago was gone from `existingIds` and a later Last.fm/topSongs response could re-add it; and the `.filter(...)` pass admitted intra-batch repeats (top + similar overlap is common) because it read the dedup set before mutating it. A module-level radioSessionSeenIds set, fed by enqueueRadio and both top-up paths and reset on artist change and clearQueue, closes all three: trimmed ids stay in the set, ids about to be replaced (fresh enqueueRadio wiping the pending radio block) are removed first so callers can re-introduce them, and the dedup pass mutates the set inline. Variety. Starting Radio on a track stacked five top tracks of the seed artist before any similar-artist material played. Switched the seed path and both top-up paths to lead with similar songs (other artists) and only fall back to top tracks when similar comes back empty — preserves the "no Last.fm" graceful degradation but stops the seed artist from monopolising the front of the queue. Not affected: gapless audio:track_switched (already index-based, no findIndex), AudioMuse Instant Mix / Lucky Mix (single-element queues or enqueue-only paths), the artist-radio path (no seedTrack — already picks just one top track and fills the rest from similar). Reported by netherguy4. * docs: changelog entry for PR #503 Logs the radio queue navigation/dedup/similar-first fix in v1.46.0 "## Fixed". |
||
|
|
d7ff1d3113 |
fix(preview): keep preview sink volume in sync with player slider (#498) (#502)
* fix(preview): keep preview sink volume in sync with player slider (#498) The Rust preview sink had its volume set once at audio_preview_play and then never updated. audio_set_volume only ramps the main sink, so slider movements during a preview had zero effect on the preview level. With the default loudness normalization (-4.5 dB pre-analysis attenuation) applied at start, even a 100% slider gives 1.0 × 0.596 × MASTER_HEADROOM ≈ 53% — matching the user-visible "fixed at around 50%" symptom. - Add audio_preview_set_volume Rust command that updates the preview sink if one is active (clamp + master headroom mirror the path used in audio_preview_play). - Extract the preview-volume calculation in previewStore into computePreviewVolume() so startPreview and the new sync path share one formula (slider value, plus the LUFS pre-analysis attenuation the engine already applies to the main sink). - Subscribe to playerStore at module level: when volume changes and a preview is active, push the recomputed value to Rust. Auth / normalization tweaks during preview are intentionally not synced — preview is short and that case is rare. Reported by netherguy4. * docs: changelog entry for PR #502 Logs the preview-volume-slider sync fix in v1.46.0 "## Fixed". |
||
|
|
3cabb64dbc |
fix(tray): resume rendering on second-launch restore (#497) (#501)
* fix(tray): resume rendering on second-launch restore (#497) The single-instance plugin callback that handles a second launch (e.g. via desktop / start-menu shortcut while the main window is hidden in the tray) was missing the RESUME_RENDERING_JS injection that the tray- icon restore path already does. When the main window is closed to the tray, PAUSE_RENDERING_JS sets window.__psyHidden = true, marks <html data-psy-native-hidden="true">, and zeroes --psy-anim-speed. A global CSS rule then pauses every animation under <html>. Page wrappers across the app use .animate-fade-in (animation: fadeIn ... both) which starts at opacity: 0 — so when a route mounts after navigation the new wrapper stays frozen at opacity: 0 and the page looks blank. Restoring via the tray icon worked because that path injects RESUME_RENDERING_JS before show(); only the second-launch path was missing it. Mirror the same eval here so both restore paths are consistent. Reported by netherguy4. * docs: changelog entry for PR #501 Logs the tray second-launch restore-rendering fix in v1.46.0 "## Fixed". |
||
|
|
ddb1f29af9 |
refactor(settings): remove redundant Animations 3-state setting (#495)
* refactor(settings): remove redundant Animations 3-state setting under Seekbar Style The `animationMode` setting (Full / Reduced / Static) duplicated work the perf-flag system and OS-level reduced-motion preference already covered: - `perfFlags.disableMarqueeScroll` already kills marquee scrolling on demand, replacing what `static` mode used to gate. - The `data-perf-disable-animations` html-level switch already strips every `*` animation, replacing what `static` mode used to do globally. - `@media (prefers-reduced-motion: reduce)` honours the OS setting for every user that asked for it via system preferences. - The 30 fps cap that `reduced` mode applied to the seekbar wave was better served by per-feature perf toggles cucadmuh added later. Removed: - `AnimationMode` type, `animationMode` field + setter from auth store. - Settings UI block (3 buttons + hint text) under Appearance > Seekbar Style. - `animationMode === 'static'` short-circuit in WaveformSeek's rAF effect; `isReduced` skip-every-other-frame logic; `static`-checks in `drawNow` / `needsDirectDraw`. - `animationMode !== 'static'` guard and `data-anim-mode` attribute in MarqueeText. - `[data-anim-mode="static"]` and `[data-anim-mode="reduced"]` rules in layout.css. - Seven i18n keys (animationMode + 6 variants) across all eight locales. Migration: the persist layer strips `animationMode` (and the legacy `reducedAnimations` boolean predecessor) so anyone who had `'reduced'` or `'static'` selected silently lands on the former `'full'` path on first launch after upgrade. No user-facing prompt — the missing setting just stops existing. cucadmuh's PR #472 (FPS overlay), #476 (preview-freeze main seekbar, sleep-recovery hooks, card-hover removal) and #486 (interpolation anchor reset on resume) are all preserved untouched — they live in separate effects / files and were not driven by `animationMode`. * docs(changelog): add Removed section for animationMode setting (PR #495) * docs(changelog): refine animationMode removal rationale (drop prefers-reduced-motion overstatement) |
||
|
|
d75670ec4b |
feat(home): broaden Because-you-like seed pool + tidy orphan card at 1080p (#493)
* feat(home): mix recently-played + starred into Because-you-like anchor pool Anchor pool was sourced only from getAlbumList(frequent), so the rotation cursor walked the same eight top-played artists no matter how varied the rest of the listening history was. Round-robin merge of mostPlayed, recentlyPlayed and starred (dedup by artistId) means each mount can land on a different listening *mode* — heavy rotation, current focus, or explicit favorites — instead of stepping through the same top-played sequence. Pool size 8 -> 12 to let the cursor visit all three modes before wrapping. Visibility guard widened so the rail still renders when the server has no frequent-play data yet but starred or recent items exist. Zero new API calls — all three lists are already in Home's initial fetch. * fix(home): drop orphan 3rd Because-card in 2-col range, keep all 3 stacked on mobile auto-fit grid wraps to 2 cols between 696-1051px container width, which left the third card alone on a second row at 1080p. Container query hides the 3rd card only inside that 2-col band; on wider screens the full 3-up row stays, on narrow viewports (single column) all three cards stack vertically as expected. * docs(changelog): Because-you-listened seed pool + 1080p layout polish (PR #493) * docs(changelog): fold PR #493 refinements into the existing Because-you-listened entry Drop the separate Changed section entry — the feature is in the same 1.46.0 release window as PR #489, so readers want a single description of the final behaviour, not "added X, then changed X" for the same release. PR reference becomes "PRs #489, #493". |
||
|
|
f82f1be63a |
feat: redesigned community themes (#490)
* redesigned community themes * fixed select arrow obsidian-black & violet-haze * docs: CHANGELOG + Contributors entry for community themes redesign (PR #490) --------- Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
d1ff2fab51 |
feat(home): Because you listened recommendation rail (#489)
* feat(home): "Because you listened" recommendation rail New Home rail (under Recently Added, default on, toggleable in Settings → Personalisation → Home Page) that surfaces 3 albums from artists similar to one of your top-played artists. Anchor rotates per Home mount so a different top-artist seeds the recommendations each visit; within each anchor, both the similar-artist subset and the chosen album per artist are randomised, so the same anchor returns different picks on subsequent visits. Card layout matches the regular Album cards' surface (--bg-card with accent-tinted border + 1px inset top highlight) and gets the same Play / Enqueue hover overlay buttons. Cover and meta scale via CSS only — no infinite animations, no filter/blur/transform, no compositing layers. Grid wraps below 3-up at <400px card width instead of shrinking. API budget: one getArtistInfo2 + 6 parallel getArtist calls per Home mount, both reusing the existing mostPlayed payload to derive the anchor pool (no extra API call to find top artists). All 8 locales seeded. * fix(home): ru plurals + per-server anchor + narrower card grid - ru: add _few / _many for becauseYouLikeTracks (CLDR Russian needs 4 forms — 3 треков was wrong, now 3 трека). - Anchor rotation memory is now per-server. The localStorage key becomes psysonic_because_anchor:<serverId>; switching servers no longer aliases server A's rotation onto server B's pool. - because-card grid minmax(400px, 1fr) -> minmax(340px, 1fr) so two cards fit side by side at typical sidebar-expanded widths instead of collapsing to a single card per row. * docs: CHANGELOG + Contributors entry for Because-you-listened rail (PR #489) * style(home): blurred cover backdrop + centred layout for Because-cards - Each Because-card renders the album cover as a blurred, low-opacity full-bleed background layer behind the existing cover thumb and text. Resolved through useCachedUrl so the cache layer feeds it (same key as the thumbnail) instead of a fresh salted URL on every render. - Card content (cover thumb + text block) now centred horizontally and vertically within the card; the meta line lives in a small pill that sits centred under the artist row. - Text contrast halo and meta-pill background are theme-aware via color-mix on var(--bg-card) / var(--text-primary), so the same rules read on dark and light themes (was hard-coded rgba black before and smudged the type on Latte / Nord Snowstorm). |
||
|
|
5b37ab70f1 |
perf(tracklist): drop now-playing pulse + EQ-bar animations (#488)
* perf(tracklist): replace animated EQ-bar + active-row pulse with static icon The currently-playing track in any tracklist (AlbumDetail, ArtistDetail, PlaylistDetail, Favorites, RandomMix) was rendered with two animations on top of an already-busy DOM: - `.track-row.active` ran `track-pulse` 3s opacity 1 → 0.6 → 1 infinite on the entire row subtree (title button, several Lucide icons, star rating SVGs, hover affordances). Opacity is a compositor property, but on WebKitGTK without compositing — Linux + NVIDIA proprietary + WEBKIT_DISABLE_COMPOSITING_MODE=1 — every animated row falls back to a full software repaint of the subtree per frame. - The "now playing" indicator in the track-number cell was three `<span class="eq-bar">` siblings with `transform: scaleY()` keyframes (`eq-bounce`), each on its own delay/duration. Same composite story: three software-repainted layers per frame, every frame. On AlbumDetail (long tracklist + cover-art header background + the already-running WaveformSeek progress rAF in the player bar) the combined cost held the WebProcess at ~80 % CPU with 1.2 GB RSS for the entire duration of playback. CPU dropped immediately on pause/stop; on Composers / Settings (no track rows) the symptom never appeared. Profiler confirmed continuous Layout & Rendering work synchronised with isPlaying, not the canvas itself. Replace both animations with static visuals: - `.track-row.active` keeps the `--accent-dim` background, drops the pulse animation entirely. - The "now playing" indicator becomes a single Lucide `AudioLines` icon (four vertical bars of different heights — reads as an EQ icon, no animation, one SVG per active row instead of three animated spans). `.eq-bars` className now only sets the accent colour. Cleanup: dead `@keyframes track-pulse`, `@keyframes eq-bounce`, `.eq-bars.paused` rule, plus a duplicate `.eq-bar` block in theme.css (with a `wave` keyframe that was being shadowed by components.css and had no other consumers). No behaviour change beyond removing the animation; the row is still visibly the active one and the icon still marks the playing track. * docs: CHANGELOG entry for tracklist animation perf fix (PR #488) |
||
|
|
59744601d4 |
feat(composer): Browse by Composer page (issue #465) (#487)
* feat(composer): Browse by Composer page (issue #465) New library section listing every artist credited as composer on at least one track, with a detail page showing all works they're credited on in that role. Targeted at classical-music libraries where the "recording artist" tag carries the orchestra and the "composer" tag carries Bach / Mozart / Chopin. Hits Navidrome's native /api/artist?_filters={"role":"composer"} for the listing and /api/album?_filters={"role_composer_id":"…"} for the works grid — Subsonic getArtist only follows AlbumArtist relations and returns 0 albums for composer-only credits, so the native API is the only path that works. Requires Navidrome 0.55+ (uses library_artist.stats role aggregation); on older / pure-Subsonic servers the page shows a one-line capability banner. - Two new Tauri commands: nd_list_artists_by_role + nd_list_albums_by_artist_role, generic over participant role so conductor / lyricist / arranger pages are trivial to add later. - Composers grid: text-only compact tiles (name + participation count pulled from stats[role].albumCount). No avatars — composer libraries carry no useful imagery and the listing endpoint exposes no image URLs anyway. - ComposerDetail: hero with Last.fm bio (via getArtistInfo2) plus the full work grid, with a graceful fallback when the artist has no external info synced. - Sidebar entry default off (Feather icon) — opt-in for the niche classical use case. - nd_retry backoffs widened from [500] to [300, 800, 1800] — helps every nd_* call survive intermittent TLS-handshake-EOF errors that some reverse-proxy setups produce when keep-alive pools churn. - Distinguishes "server can't do this" (HTTP 400/404/422/501) from transient errors so the capability banner only fires when the server actually rejects the request shape; everything else gets a retry button. - i18n in all 8 supported locales. * fix(composer): address review feedback on detail page + role queries - Re-fetch ComposerDetail when music-library scope changes; previously the album grid stayed stale until navigation while the list refreshed. - Thread library_id through nd_list_artists_by_role and nd_list_albums_by_artist_role so role queries respect the active Navidrome library, matching the Subsonic musicFolderId already piped through libraryFilterParams(). - Fix CachedImage cache-key mismatch on ComposerDetail: a Last.fm header image was stored under the Subsonic cover-art key, aliasing cache entries and risking cross-source pollution. - Consolidate the two contradictory composer-imagery comments in Composers.tsx into a single accurate one (the older one referenced an Images toggle that was never implemented). - Align openLink toast duration with ArtistDetail (1500ms -> 2500ms). * fix(composer): keep bio across scope changes, add share, degrade gracefully Three remaining items from the latest review pass on the composer flow. 1. Bio survives a music-library scope change. The previous fix added musicLibraryFilterVersion to the load effect, but that effect also did setInfo(null) while the getArtistInfo effect still depended on [id] alone — so a scope bump on the open page wiped the bio without re-fetching it. Move the info reset into the bio effect (keyed on id) and out of the load effect: the album grid still refreshes on scope change; the Last.fm header image and biography survive untouched, since both are library-independent. 2. Composers join the share pipeline as a first-class entity kind. Extend EntityShareKind with 'composer' (and isEntityKind), branch applySharePastePayload to validate via getArtist (same id pool) and navigate to /composer/:id, and wire a Share button into ComposerDetail. A pasted composer link now opens the composer view instead of the artist view, matching what was copied. i18n added in all 8 locales (sharePaste.composerUnavailable, openedComposer; composerDetail.shareComposer, unknownComposer). 3. Partial server failure no longer hides the works. If getArtist rejects but ndListAlbumsByArtistRole succeeds, the page used to show full "not found" despite having data to display. Switch the not-found gate to require both empty (`!artist && !albums`) and render a degraded header (placeholder name, no Wikipedia / favourite / share / Last.fm image) when only metadata is missing. * fix(composer): right-click share copies a composer link, not an artist link The context menu opened from a composer card / row uses type='artist' because every composer-action (radio, favourite, rating, add-to-playlist) is identical to the artist counterpart — they share an id space and a backend representation. Sharing was the one exception: the "Share Link" entry produced a 'psysonic2-' string with k='artist', so a paste opened /artist/:id even though the user came from /composers. Add an optional shareKindOverride to openContextMenu (default: undefined, preserves existing behaviour) and have the artist-typed branch consult it when calling copyShareLink. Composers.tsx now passes 'composer' on both right-click sites; nothing else changes downstream because the override only affects the share kind. * polish(composer): show Last.fm avatar even without server metadata Two minor follow-ups from the latest review. - ComposerDetail: drop the `&& artist` guard on the header-avatar render path. info?.largeImageUrl can resolve through getArtistInfo(id) without ever needing the SubsonicArtist record, so the previous gate hid a perfectly good Last.fm portrait whenever getArtist failed but the bio fetch succeeded. Replace artist.name with displayName so the alt / aria-label degrade to the localised "Composer" placeholder instead of empty strings. - copyEntityShareLink: doc comment now mentions composer alongside track / album / artist. * fix(composer): derive Last.fm cache key from route id, not from artist record Follow-up to the previous polish: the avatar render path no longer requires `artist` to be populated, but the cache-key gate still did. So when getArtist failed but getArtistInfo returned a Last.fm portrait, the key fell through to coverKey — which is empty without an artist record, re-creating the very aliasing bug the earlier Subsonic-vs-Last.fm fix was meant to close. Switch the Last.fm branch to the route id (same id namespace as the SubsonicArtist record), so the key stays stable whenever Last.fm art is shown, independent of getArtist succeeding. * docs: CHANGELOG + Contributors entry for composer browsing (PR #487) |
||
|
|
e215694301 |
feat(help): rewrite Help page — trimmed Q/A, 10 sections, live search (#485)
* feat(help): rewrite English Q/A entries — trim, consolidate, refresh The Help page had grown to ~50 entries over time, with several that the UI itself answers (double-click to play, click the cover for fullscreen, click the repeat button to cycle, …) and other groups that were better folded into a single answer (rating + Skip-to-1★, Internet Radio basics + supported formats, Device Sync overview + filename template + cross-platform behaviour, …). This pass: - drops obviously-redundant entries (q4, q7, q8, q11, q22, q24, q25, and the trivial Settings → X pointers q12, q13, q15, q32, q42, q43) - consolidates the natural groupings (q5+q31, q37+q38+q39, q53+q54+q55, q34+q35+q47, q26+q27+q28, q12+q41, q56+q57) - adds entries for features that did not exist yet when the previous Q/A list was written: Orbit (Listen Together), Magic Strings sharing, LUFS Smart Loudness Normalization, Mini Player + Floating Player Bar, Smart Playlists, Track Preview, Search and Advanced Search, Statistics, Tracks library hub, Genre tag-cloud browser, Discord Rich Presence, Bandsintown tour dates, Multi-select + Shift-click range selection, Sidebar / Home / Artist Page customization, Sleep Timer, Open Source Licenses Result: 45 focused entries across 10 sections (Getting Started / Playback & Queue / Audio Tools / Library & Discovery / Lyrics / Sharing & Social / Personalization / Power User / Offline & Sync / Integrations & Troubleshooting), each one answering something the UI does not already answer at a glance. * feat(help): restructure into 10 sections with live search Page is now organised into ten focused sections (Getting Started, Playback & Queue, Audio Tools, Library & Discovery, Lyrics, Sharing & Social, Personalization, Power User, Offline & Sync, Integrations & Troubleshooting) each rendered as its own column-friendly accordion group with a Lucide icon. A search input lives in the page header. Typing filters every Q+A pair across all sections by case-insensitive substring; sections that end up empty are hidden, matched items are auto-expanded so the user sees the answer without having to click each result, and a "no results" empty state appears when the query matches nothing. Clearing the input restores the manual accordion behaviour. An × button next to the input clears the query in one click. CSS uses dedicated `.help-search`, `.help-search-icon`, `.help-search-input`, `.help-search-clear` rules instead of leaning on the global `.input` class — the latter brought its own focus-ring styles that doubled with the wrapper border. Focus state highlights the wrapper border to `--accent` via `:focus-within`. * i18n(help): translate the new Help page to 7 locales Updates de, fr, nl, zh, nb, ru, es to match the new English Q/A structure (45 entries across 10 sections, plus the live-search labels: title, searchPlaceholder, noResults). DE / FR / NL / NB / ES were translated directly. RU and ZH are structurally correct but written at machine-translation quality; both could use a pass from the original locale maintainers (@cucadmuh for RU, @jiezhuo for ZH) — none of the wording is load-bearing for the i18n keys, so the page renders correctly today and refinements can land as follow-up touch-ups without coupling. * docs: changelog + contributors for PR #485 Adds the v1.46.0 "Changed" entry and the Psychotoxical contributors line for the Help page rewrite. |
||
|
|
43d75e744b |
feat(selection): Shift+Click range selection on grid pages (#484)
* feat(selection): add useRangeSelection hook with Shift+Click range support
Reusable hook for multi-select state across pages that show grids of
items the user can pick. Tracks the selected ID set, a click anchor,
and exposes a `toggleSelect(id, { shiftKey })` callback:
- Plain click → toggles that item and moves the anchor to it.
- Shift-click on a second item → adds every item between the anchor
and the click target (inclusive) to the selection. The anchor moves
to the shift-clicked item so the next shift-click extends from
there.
Range expansion follows the items array passed to the hook, so the
caller controls the user-visible order (filtered + sorted list, not
the raw upstream array).
Implementation note: the anchor ref is snapshotted *before* the state
updater runs and written *after* it. React 18 strict mode invokes
state updater functions twice in dev to surface side effects, so any
ref mutation inside the updater would taint the second invocation and
the replay would miss the range branch.
* feat(selection): adopt Shift+Click range selection on grid pages
Wires `useRangeSelection` into the four pages that ship a multi-select
mode on top of card grids:
- Albums (passes the filtered/sorted `visibleAlbums` so range follows
the order the user actually sees)
- RandomAlbums
- NewReleases
- Playlists
`AlbumCard.onToggleSelect` is extended to forward `{ shiftKey }`, and
the card's onClick handler reads `e.shiftKey` from the React event
and threads it through. The Playlists grid uses an inline onClick on
the card div and was updated the same way.
User-visible behaviour: in selection mode, click an item then
shift-click a later item — every item between them gets selected.
Existing single-toggle behaviour is unchanged when no shift key is
held.
* docs: changelog entry for PR #484
Logs the Shift+Click range selection on grid pages under
v1.46.0 "## Changed".
|
||
|
|
6c1deeeb7f |
feat(most-played): quick actions, real context menu, prominent plays badge (#482)
* feat(most-played): quick actions, real context menu, prominent plays badge Three UX refinements on Settings → Most Played, in response to user feedback: * **Quick actions on each album row** — Play and Enqueue buttons that reuse the same logic as AlbumCard (Play kicks the existing `playAlbum` fade-out flow; Enqueue fetches the album and appends its songs to the queue). Always visible, not hover-gated. * **Real context menu** on right-click — replaces a hidden direct `playAlbum` action with the standard `openContextMenu(...)` flow used elsewhere in the app, so right-click on an album row now opens the full album context menu (Play / Add to queue / Play next / Add to playlist / Go to artist), and right-click on a Top Artists card opens the artist context menu. * **Plays badge next to the album title** — replaces the small right-aligned plays count that was easy to miss. Each row now shows a localized pill (`11 plays` / `11× gespielt`) right next to the album title, since the play count is the central datum on this page. CSS: new `.mp-album-name-row`, `.mp-album-plays-pill`, `.mp-album-actions` and `.mp-album-action-btn` rules; the unused `.mp-album-plays` block and its right-most grid column were removed. * docs: changelog entry for PR #482 Logs the Most Played quick-actions / real context menu / prominent plays badge changes under v1.46.0 "## Changed". |
||
|
|
ebce53f8a7 |
fix(sidebar): centre Playlists icon and unify hover hitbox in collapsed mode (#481)
* fix(sidebar): centre Playlists icon and unify hover hitbox in collapsed mode The Playlists nav entry had its own special render path with a wrapper div, header-row and `flex: 1` main link to fit the expand-toggle button. Those elements remained active in collapsed mode too, where `padding-right` on the header-row and `flex: 1` on the link made the icon sit off-centre and gave the row a wider hover hitbox than every other collapsed sidebar item. Hoist the `isCollapsed` check above the playlists special-case so that in collapsed mode Playlists renders through the same plain `<NavLink>` branch as Artists / Albums / Favorites / etc. The expanded-mode treatment (wrapper, header-row, expand-toggle, nested playlist list) is unchanged. * docs: changelog entry for PR #481 Logs the collapsed-sidebar Playlists icon centring fix in v1.46.0 "## Fixed". |
||
|
|
b084e96c1f |
fix: prune stale analysis queues and cap loudness backfill window (#480)
* fix(analysis): prune stale backfill jobs and limit prefetch window Drop pending backfill and cpu-seed jobs that are no longer in the active playback queue, and add debug counters for pruned work. Limit loudness backfill scheduling to the current track plus the next five tracks to prevent runaway queue growth in dev sessions. * chore(analysis): remove unused loudness prefetch parameter Drop the now-unused incoming-tracks parameter from the loudness prefetch helper and update internal call sites to match the current queue-window scheduling logic. * docs(changelog): document analysis queue control fix (#480) Add a short 1.46.0 Fixed entry describing stale backfill pruning, the current+5 loudness backfill window cap, and debug prune counters for diagnostics. * docs(contributors): add cucadmuh entry for PR #480 Logs the analysis-queue prune + loudness backfill window cap in the Settings → System → Contributors list. |
||
|
|
dc35f53674 |
feat(artist): group albums by release type on artist page (#471)
* feat(artist): group albums by release type on artist page Uses the releaseType field to group albums/releases into sections like Albums, Compilation, Live, etc. If there's no release type it falls back to normal view * feat(artist): i18n release-type group labels * fix(artist): deterministic release-type group order * refactor(artist): replace inline styles with CSS classes * i18n(artist): translate release-type labels in remaining 7 locales Sayykii's `releaseTypes` namespace was added to en.ts only. Fills in de, fr, nl, zh, nb, ru, es with the same 8 keys (album, ep, single, compilation, live, soundtrack, remix, other) so users on non-English UIs see translated section headers on the artist page instead of the raw title-cased fallback. * docs: changelog + contributors for PR #471 Adds the v1.46.0 "Added" entry and bumps Sayykii's contributors line for the artist-page release-type grouping. --------- Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> |
||
|
|
d48ea819c1 |
fix: stabilize preview seekbar, post-sleep audio recovery, and card hover behavior (#476)
* 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. |
||
|
|
5c8cfb8be3 |
feat(settings): keep current active server when adding a new one (#475)
* 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. |
||
|
|
c3d37546cf |
Feat/search improvements (#470)
* 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. |
||
|
|
9d30285ff1 |
Perf/UI cover cache mainstage (#468)
* 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. |
||
|
|
d33abf565c |
feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch (#466)
* feat(ui): StarFilterButton component + common i18n keys Reusable toggle button for "favorites only" filtering. Three size variants for different toolbar contexts: - default: icon + label (Albums-style) - compact: icon-only with 0.5rem padding (Artists view-mode buttons) - small: icon + label at 12px / 4×14 padding (AdvancedSearch tabs) Adds common.favorites + favoritesTooltipOff/On in all 8 locales. * feat(library): "favorites only" filter on Albums, Artists, AdvancedSearch Client-side filter using the existing useMemo pipelines on each page. Reads starred state from item.starred + playerStore.starredOverrides (O(1) Map lookup, picks up live star toggles without refetch). - Albums: toolbar button (default size) next to compilation filter. - Artists: toolbar button (compact / icon-only) before the Images toggle. - AdvancedSearch: toolbar button (small) next to the result-type tabs; filters all three result categories (artists / albums / songs) and updates the count badges accordingly. Filter state is ephemeral per-page (not persisted) so users don't get surprised by hidden items after a restart. Zero extra server calls. * docs(contributors): credit + changelog entry for #466 |
||
|
|
0fab2849e5 |
feat(queue): preserve Play Next order toggle (#464)
* feat(queue): add preservePlayNextOrder setting + playNext store action - New Track.playNextAdded flag (analogous to autoAdded / radioAdded). Stale flags behind queueIndex are harmless — only forward streak scan. - New playerStore action playNext(tracks): tags incoming tracks and delegates to enqueueAt for unified undo + server sync. - New authStore boolean preservePlayNextOrder (default false). When on, playNext appends behind the existing Play-Next streak (Spotify-style) instead of inserting directly after the current track. * refactor(context-menu): centralise Play Next; add Settings toggle + i18n - Replace 3 inline splice/enqueueAt call sites in ContextMenu with the new playNext action. Side-benefit: the single-song path now goes through enqueueAt and gets undo + queue sync (previously missing). - Settings → Audio → Playback: new toggle below Gapless. - 8 locales: preservePlayNextOrder + preservePlayNextOrderDesc. * docs(contributors): credit + changelog entry for #464 |
||
|
|
e1f2cb4c37 |
feat(discord): add server cover art source (#462)
* feat(discord): add server cover art source
The old Apple Music toggle is replaced with a radio selector which let's you choose
between Apple Music, Server and no image.
It's important to note that the server needs to be publicly accessible.
Translations have been added for all locales
* feat(discord): toggle UI for cover source and tightened defaults
- Replace cover-source radio buttons with three indented sub-toggles
(none / server / apple) under Discord Rich Presence; mutex via
setDiscordCoverSource — turning one on flips the others off.
- Default discordCoverSource is now 'server' for fresh installs
(opt-in friendly: own server, no third-party data leak). Existing
users keep their state via the legacy bool migration.
- Tighten template defaults: details {artist}, state {title}, largeText
unchanged. Existing users keep their persisted values.
* docs(contributors): credit Sayykii + changelog entry for #462
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
|
||
|
|
8d8c1aa8a3 |
Environment upgrade & hot-cache playback (#463)
* 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 |
||
|
|
a6cc2e2ad4 |
perf(linux): WebKit probe, throttled progress IPC, snapshot playback UI (#452)
* 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> |
||
|
|
4fce491974 |
feat(nix): psysonic-gdk-session, devShell target dir, nixos-install refresh (#447)
* feat(nix): psysonic-gdk-session package and local cargo layout - Add psysonic-gdk-session (forceGdkX11=false) to flake packages and apps - Ignore .build-local/ in cleanSource; set CARGO_TARGET_DIR in devShell shellHook - Gitignore: result, .build-local, prod.sh (local helper only) - nixos-install: contributor shell docs use flake devShell only (no shell.nix in tree) * chore(nix): gitignore local dev.sh, shell.nix, prod.sh Document optional local helpers in nixos-install.md; keep flake PR free of non-reproducible shell.nix fetchTarball. * docs(nix): document default vs gdk-session flake packages Explain x11-wrapped default and optional psysonic-gdk-session trade-offs; extend flake description and one-shot run note. * docs: quote flake URLs for zsh in README and nixos-install * docs(changelog): add PR #446 UI bulk ratings and PR #447 Nix flake GDK choice * docs: remove Nix flake block from README; changelog #447 points to nixos-install only |
||
|
|
3b4d54431b |
feat(random-mix): playlist size selector + filter panel layout cleanup (#445)
* feat(random-mix): playlist size selector + filter panel layout cleanup Adds a 5-button playlist-size picker (50/75/100/125/150) at the top of the Random Mix filter panel, persisted via authStore. Clicking a size immediately reruns the current mix (genre-scoped or All Songs) at the new size — no second click on Remix needed. Filter panel layout cleaned up: - Two sub-sections "MIX SETTINGS" and "EXCLUSIONS" with a divider between them so the panel reads cleanly with the new size row. - Larger panel-level headers (FILTERS / GENRE MIX) so the hierarchy panel-title > sub-section is visually unambiguous. - Italic muted note under MIX SETTINGS calling out that large mix sizes may return fewer unique tracks if the server's random pool runs short — sets honest expectations instead of users wondering why a 150 request returned ~126. fetchRandomMixSongsUntilFull now scales batch size, max-batch ceiling and dup-streak budget with target size; when no Settings-level mix filter is active, the first call asks for the full target so a 150 mix can finish in a single round-trip on most libraries. The loop falls through to top up with deduped follow-up calls if the server returns fewer than requested. * docs(changelog): add #445 Random Mix playlist size selector entry * chore(credits): add #445 to Psychotoxical contributions |
||
|
|
1799e90e04 |
feat(tracks): Highly Rated rail + per-card star display (#443)
* feat(tracks): Highly Rated rail + per-card star display Adds a new SongRail above the Random Pick on the Tracks page that surfaces the user's highly-rated tracks (sorted by rating DESC). Auto-hides on non-Navidrome servers and when the library has no rated tracks yet. Reuses the existing SongRail layout, with the standard reroll button forcing a cache bypass. Per-card stars: any SongCard whose `userRating > 0` now shows a small five-star row (filled to the rating value) below the artist line — visible everywhere SongCard is used, not only in the new rail. Read-only display; rating is still done via the row's context menu or the Now Playing star widget. Cache layer in `ndListSongs`: opt-in `cacheMs` parameter (skipped by VirtualSongList; used only by the Highly Rated rail with a 60 s TTL). Cleared on `setRating` mutation so a freshly-rated track shows up on the next page revisit, and on server switch alongside the existing token cache. The reroll button explicitly invalidates before refetching, so a manual refresh always hits the network. * docs(changelog): add #443 Tracks Highly Rated rail entry * chore(credits): add #443 to Psychotoxical contributions |
||
|
|
98ff73d17a |
feat(perf): 3-state animation mode (Full / Reduced / Static) (#441)
* feat(perf): 3-state animation mode (Full / Reduced / Static) Replaces the boolean `reducedAnimations` toggle with a three-way `animationMode` setting, suggested by Viktor Petrovich after the Windows audio fix (PR #426) shipped and confirmed a measurable GPU drop: - `full` (default): native frame rate, marquee scrolls normally - `reduced`: 30 fps cap on the animated seekbar wave; player marquee runs at half speed - `static`: rAF loop disabled; the seekbar repaints from the ~2 Hz audio:progress heartbeat. Player title/artist truncate with ellipsis instead of scrolling. Migration in `onRehydrateStorage` maps legacy `reducedAnimations: true` to `'reduced'`, anything else to `'full'`. Static is opt-in only. Settings UI follows the ReplayGain Auto/Track/Album pattern with a contextual hint that explains what each mode does. i18n: 5 new keys across 8 locales, 2 legacy keys removed. * docs(changelog): add #441 3-state animation mode entry * chore(credits): add #441 to Psychotoxical contributions |
||
|
|
dcec30166a |
fix(audio): frame-align gapless-off track-separation silence (#439)
* 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. |
||
|
|
4483552c94 |
fix(i18n): backfill shortcut labels for #435 in 7 locales (#436)
* fix(i18n): backfill 10 settings.shortcut* keys for #435 PR #435 added 10 new settings.shortcut* labels to en.ts (start search, advanced search, toggle sidebar, mute, equalizer, repeat, open now playing, lyrics, favorite current track, open help) but left the other 7 locales without translations — i18next would fall back to English at runtime. Adding translations for de, fr, nl, zh, nb, ru, es in the same position as en.ts (right after shortcutOpenMiniPlayer), styled after the existing shortcut* entries in each locale. * docs(changelog): add #435 shortcuts action-registry entry * chore(credits): add #435 to cucadmuh's contributions |
||
|
|
402e288a24 |
fix(css): scope data-app-blurred animation pause away from * (#434)
* 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 |
||
|
|
c28cc58185 |
docs(changelog): restore #420/#423/#425/#426 entries dropped by #429 (#430)
PR #429 took CHANGELOG.md from HEAD (post-#419 squash) instead of merging it from the pre-squash main state, so the four entries that had landed via #428 backfill (#426) and the squashes of #420/#423/#425 were silently dropped from [1.45.0]. This restores all four to the same positions they originally had, keeping the existing #419 entry intact. |
||
|
|
18b4a982ef |
feat: queue-ux-improvements (#419)
* feat(queue): add ETA display, equalizer indicator and collapsible now playing
* deleted endsAt and showDuration strings, changed eta update to 30s
* feat(queue): ETA tooltip, persistent Now Playing collapse, EQ bar pause, remove redundant Play icon
* feat(queue): fold ETA into existing total/remaining toggle as third mode
The standalone ETA span next to the track counter is removed; instead the
clickable duration label in the queue header now rotates through three
modes per click: total → remaining → eta → total. Counter (N/M) stays
where it was.
ETA mode keeps the live-feel treatment from the original PR (accent
colour while playing, muted at 50% opacity when paused). The other two
modes use plain accent.
i18n: queue.etaTooltip removed (no longer a separate descriptive label),
queue.showEta added as the action tooltip ('Show estimated end time')
in all 8 locales — matches the showRemaining / showTotal pattern.
* docs(changelog): add #419 queue UX improvements entry
Adds the [1.45.0] / Added entry for this PR's queue panel refinements
(position counter, tri-state duration toggle including ETA, collapsible
Now Playing section, animated EQ indicator).
---------
Co-authored-by: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
|
||
|
|
5662dafe97 |
docs(changelog): backfill #426 Windows stutter fix entry (#428)
PR #426 was merged earlier today without a CHANGELOG entry; adding it under [1.45.0] / Fixed retroactively. From now on the entry should ship inside the PR itself rather than as a follow-up commit on main. |
||
|
|
7064ca500e |
feat(stats): shareable Top-Albums card export (#425)
* feat(stats): add shareable Top-Albums card export
Adds a shareable PNG export of the user's most-played albums,
available from the Statistics page under the "Most Played Albums"
section header.
Layout:
* 3 formats — Story (1080×1920, 9:16), Square (1080×1080), Twitter
Card (1200×675, 16:9). Story and Square stack a centered URL footer;
Twitter packs logo + label + URL into a single header row.
* 3 grid sizes — 3×3 (default), 4×4, 5×5. Modal fetches up to 25
frequent albums on open so larger grids work even when the entry
surface only loaded a handful.
* Cover tiles render the rank + play count in a thin black info strip
at the bottom of the cover (rank left, "N Plays" right).
* Header label is hardcoded English ("Top Albums") so a shared image
remains legible to followers who don't share the user's UI language.
Caller can override via `opts.meta`.
Source path is local-only — `getAlbumList(type='frequent')` from
Subsonic. Last.fm is intentionally not used.
Implementation:
* `src/utils/exportAlbumCard.ts` — pure Canvas-API renderer. Reads
theme accent / bg from the document's CSS variables so the export
matches the active theme. Cover art is loaded through the existing
`getCachedBlob` IndexedDB cache, so repeated exports are cheap.
Wordmark is rendered from the `PsysonicLogo` React component via
`renderToStaticMarkup`, with both gradient stops baked to a single
accent color for crisp single-tone branding.
* `src/components/StatsExportModal.tsx` — UI: format + grid pickers,
live downscaled preview, native save dialog via
`@tauri-apps/plugin-dialog` + `plugin-fs`.
* `src/components/AlbumRow.tsx` — adds optional `headerExtra` slot so
the share button sits next to the existing scroll-nav arrows.
* i18n keys added under `statistics.*` (`en`, `de`) — other locales
fall back to English.
* docs(changelog): add Top-Albums card export for PR #425
|
||
|
|
297c9f1125 |
fix(preview): sync audio start, ring animation, and download timeout (#423)
* 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> |