Commit Graph

598 Commits

Author SHA1 Message Date
Frank Stellmacher aa0776e811 perf(bundle): lazy-load rarely-visited pages + split vendor chunks (#249)
Cold-start bundle was a single 1.88 MB / 577 KB-gzipped chunk that
parsed every page in the app before first paint, including pages a
typical session never opens.

Two-part fix:

1. Lazy-load 10 rarely-visited pages via React.lazy() — each becomes
   its own chunk that only downloads when the route is hit:
     Settings, Statistics, Help, WhatsNew, DeviceSync, OfflineLibrary,
     LabelAlbums, AdvancedSearch, FolderBrowser, InternetRadio.
   Whole <Routes> tree is wrapped in <Suspense fallback={null}> — no
   visible loading state, the browser cache hits on subsequent visits.

2. Vite manualChunks splits dependencies that change rarely from app
   code: react/react-dom/react-router, the @tauri-apps/* family, and
   i18next. Tauri auto-updater pulls smaller deltas when only app
   code changed (vendor chunks stay byte-identical and cached).

Build output:
  Before: 1.88 MB / 577 KB gz monolithic
  After cold start: ~450 KB gz (index 370 + react 54 + tauri 6 + i18n 20)
                    = 22% smaller initial download
  Plus 11 lazy chunks. Notably WhatsNew's bundled CHANGELOG (215 KB /
  76 KB gz) is now off the cold-start path entirely.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:38:07 +02:00
Frank Stellmacher 88cd09c0be perf(lyrics): persist resolved lyrics to IndexedDB across app restarts (#248)
The existing module-level Map<songId, CachedLyrics> only survived
within a session. After every app restart the lyrics fetch chain
(server → lrclib → netease) ran from scratch for every track the user
played, even ones whose lyrics were already resolved 5 minutes ago
before the previous session ended.

Add an IndexedDB layer that's only consulted on RAM miss:

- L1 (RAM): unchanged, sync, hit on tab switch / track repeat
- L2 (IndexedDB, async): hit after restart, hydrates RAM
- Network: unchanged fallback chain

Key format `${serverId}:${songId}` so the same songId on two different
Subsonic servers cannot collide.

TTLs: 90 days for resolved lyrics (they rarely change once shipped),
7 days for `notFound` entries (gives the user / admin a chance to add
lyrics later without an indefinite negative cache).

YouLyPlus mode skips L2 — a persisted entry from the standard pipeline
wouldn't have word-level sync, which is the whole point of lyricsplus.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:38:02 +02:00
Frank Stellmacher a76616b342 perf(albums): bump infinite-scroll prefetch margin from 200px to 1500px (#247)
The IntersectionObserver sentinel triggered the next page only when it
came within 200px of the viewport — by the time the user had scrolled
that far, they were already at the very edge waiting for the request.

Bumping rootMargin to 1500px (~1.5 screens at typical heights) prefetches
ahead of the scroll instead of behind it, so the next batch of albums is
visibly already there when the user reaches them.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:37:57 +02:00
Frank Stellmacher 1ec27f9aff perf(device-sync): parallelise getAlbum calls when syncing an artist source (#246)
fetchTracksForSource() iterated over an artist's albums with sequential
`await getAlbum(...)` inside a for-loop. A 50-album artist sync stalled
for ~7 seconds of round-trips before any device write started.

Switch to Promise.all(albums.map(...)). Same pattern ArtistDetail.tsx
already uses for "Play All" without issues — Navidrome handles parallel
getAlbum requests fine.

Per-album errors fall back to an empty song list rather than aborting
the whole sync.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:37:52 +02:00
Frank Stellmacher 9da1d643f7 perf(search): use CachedImage for result thumbs to stop re-render download storms (#245)
The sidebar LiveSearch and mobile search overlay rendered cover thumbs via
raw <img src={buildCoverArtUrl(...)}> instead of going through the
IndexedDB-backed CachedImage path used everywhere else in the app.

buildCoverArtUrl() mints a fresh URL on every call (random salt + a new
MD5 token per Subsonic spec), so the browser cache was permanently
defeated for these thumbs — every re-render of the dropdown produced
new URLs and the browser dispatched HTTP requests for every "new"
image.

While typing in the search field this multiplied: each keystroke
triggered a re-render with fresh URLs for every visible cover, and
after the 300 ms debounce a fresh search() result triggered another
wave. Five visible album thumbs × five keystrokes ≈ 50 redundant cover
fetches in 1.5 s, manifesting as periodic typing delays.

Switch all three call sites to CachedImage with the stable
coverArtCacheKey() — same pattern CLAUDE.md explicitly recommends:

    buildCoverArtUrl() generates a new ephemeral URL every call —
    browser cache is useless. Use coverArtCacheKey(id, size) +
    CachedImage for <img> tags.

Confirmed locally: typing in the search field is noticeably smoother.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:37:47 +02:00
Frank Stellmacher 06140a490b feat(queue): add Now-Playing Info tab with artist bio, song credits and Bandsintown tour dates (#244)
* feat(queue): add Now-Playing Info tab with artist bio, song credits and Bandsintown tour dates

A third tab in the right-side queue panel surfaces context for the
currently playing track:

- Artist card: image + biography from Subsonic getArtistInfo (Last.fm
  "Read more on Last.fm" anchor stripped), with read-more toggle that
  only appears when the bio actually overflows the 4-line clamp.
- Song info: contributor credits from OpenSubsonic contributors[]
  rendered stacked (name prominent, role muted). Section is hidden
  entirely on servers without contributor support, and rows that just
  re-state the main artist under role "artist" are filtered out.
- On tour: optional Bandsintown integration (opt-in, off by default).
  HTTP fetch + JSON parsing happen entirely on the Rust side; the
  frontend wrapper deduplicates concurrent calls and caches results in
  RAM for the session. Limited to 5 events with a "Show N more" toggle.
  When the toggle is off, the section becomes an in-place opt-in card
  with a privacy info-tooltip explaining what data is sent — same
  tooltip is also exposed on the matching toggle in Settings.

Caching: artist info and song detail are memoised by stable IDs across
component remounts, so jumping between tracks of the same album/artist
does not refire the network calls.

Implementation notes:
- Bandsintown app_id "js_app_id" — arbitrary strings (e.g. "psysonic")
  now return HTTP 403 from rest.bandsintown.com; js_app_id is the ID
  Bandsintown's own embeddable widget uses and is broadly accepted.
- Tour items use negative left/right margins so the date badge stays
  visually aligned with the section title while the hover background
  extends slightly past the section edges.
- New i18n namespace nowPlayingInfo across all 8 locales (en, de, fr,
  nl, zh, nb, ru, es) including pluralised "Show N more" forms.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(now-playing-info): switch nested flex-columns to block layout to stop content-dependent drift

Repeated reports of the Tour and Song-info sections rendering at
inconsistent x-positions (sometimes left of the section title,
sometimes centred, sometimes correctly aligned) — varying by artist,
sidebar width, and even whether "Show more" was clicked.

Root cause: nested flex-direction: column containers
(.np-info → .np-info-section → .np-info-tour / .np-info-credits) where
the cross-axis (horizontal) sizing on WebKitGTK occasionally inherits
the intrinsic min-content of the longest child. With one
"Naherholungsgebiet/Freizeitgelände Lago Alfredo"-style venue name
that overflows the sidebar, the parent ul gets pushed past 100% width
and the entire layout drifts. min-width: 0 on every level didn't help
because cross-axis stretch in flex-column ignores it for content-driven
overflow.

Fix: collapse all the section/list containers to display: block. Block
layout is content-agnostic — children always render at 100% parent
width, left-aligned, deterministically. Spacing between siblings now
uses the lobotomy selector (`> * + * { margin-top }`).

Only the tour item itself stays flex (badge ↔ meta horizontal layout),
and that one still has overflow: hidden + flex: 1 1 0 + min-width: 0
on the meta column to truncate venue/place text with ellipsis.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:13:18 +02:00
Frank Stellmacher 76ed6eca55 feat(replaygain): add Auto mode that picks track vs album gain from queue context (#242)
Auto mode is the new default for ReplayGain. It picks album gain when an
adjacent queue neighbour shares the same albumId (so "Play All" on an
album, or any contiguous album block, gets album gain), otherwise track
gain. Falls back to track gain when album gain is missing.

- authStore: replayGainMode widened to 'track' | 'album' | 'auto', default 'auto'
- playerStore: new resolveReplayGainDb(track, prev, next, enabled, mode) helper
  replaces five inline ternaries (audio_play hot path, gapless preload,
  cold-resume success + fallback, live update_replay_gain)
- Settings: third "Auto" button + contextual hint when active
- i18n: replayGainAuto + replayGainAutoDesc added to all 8 locales

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:12:19 +02:00
Frank Stellmacher fb02b62a54 fix(titlebar): hide traffic-light glyphs until hover (#243)
* fix(titlebar): hide traffic-light glyphs until hover

The lucide Minus/Square/X icons at 9×9 with stroke-width 2.5 looked
chunky at all times — especially the Square for maximize. Match real
macOS behaviour: glyphs fade in only when the controls group is hovered
(or a button is keyboard-focused).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(titlebar): drop traffic-light glyphs entirely, add coloured hover glow

Following review feedback: even on-hover the lucide glyphs at 9×9
weren't pleasant. Remove them outright and signal hover with a soft
coloured glow matching each button (red/yellow/green) plus a subtle
brightness lift. Keyboard focus uses a white outer ring for accessibility.

- TitleBar.tsx: remove lucide imports + icon children, add aria-label
- layout.css: drop svg sizing rules, add per-variant box-shadow hover,
  add focus-visible ring, transition box-shadow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 19:11:24 +02:00
cucadmuh bac0afe6ae fix(subsonic): sync Rust HTTP UA with main webview UA (#235)
Use the main window WebView user agent as the runtime source for Rust-side HTTP clients and refresh the audio engine client when the UA changes. This keeps backend and WebView requests aligned while preserving a simple startup sync flow.
2026-04-21 19:04:04 +02:00
Frank Stellmacher 3b3833007b feat(logging): add runtime log levels and debug log export (#241)
Add a System setting for Off/Normal/Debug logging, apply readable local timestamps to backend logs, and enable exporting buffered runtime logs to a file when debug mode is active.

Co-authored-by: Maxim Isaev <im@friclub.ru>
2026-04-21 12:12:54 +02:00
Frank Stellmacher fa21379dbb fix(player): prevent streaming seek UI freezes and progress snapbacks (#236)
* fix(player): prevent streaming seek UI freezes and progress snapbacks

Move blocking seek work off the command path with a bounded timeout, switch fullscreen/mobile seek bars to commit-on-release, and stabilize fallback progress state so rapid early seeks on streamed tracks do not freeze the UI or jump progress to zero.

* fix(player): avoid second-seek lockups and progress flicker

Add a short lock timeout for audio seek state access to prevent UI stalls when rapid seeks overlap, and keep waveform progress stable right after seek commit to eliminate brief visual snapbacks.

* fix(player): harden seek recovery and reduce stream log noise

Keep seek fallback stable during delayed backend responses to prevent occasional resets to track start, and remove per-read stream blocking logs so normal playback diagnostics stay readable.

---------

Co-authored-by: Maxim Isaev <im@friclub.ru>
2026-04-21 12:10:26 +02:00
Frank Stellmacher c61bcacd0d feat(mobile): comprehensive mobile UI overhaul (#238)
RandomMix: filters and genre mix panels collapse on mobile
- RandomAlbums / album grids: auto-fill favouring 3 columns at narrow widths
- BottomNav: More button opens a bottom sheet with remaining nav items
- MobileMoreOverlay: new sheet component with backdrop and slide-up animation
- Tracklist: mobile Title / Artist two-line stacked layout (Apple Music style)
- ArtistDetail: centred header (photo → name → buttons), icon-only buttons
  except Play All, collapsible Similar Artists (5 default), 2-column album
  grid, avatar glow derived from dominant cover colour
- Settings: fixed overflow in ReplayGain, Crossfade, mix rating filters,
  theme scheduler, and Next Track Buffering sections

Co-authored-by: kilyabin <65072190+kilyabin@users.noreply.github.com>
2026-04-21 12:10:02 +02:00
Frank Stellmacher f73cca669b edit(FullScreenPlayer): Possible halos in the background have been removed (#239)
Co-authored-by: kilyabin <65072190+kilyabin@users.noreply.github.com>
2026-04-21 12:09:38 +02:00
Frank Stellmacher a1427202a9 docs(privacy): add YouLyPlus lyrics backend note (#240)
* edit(PRIVACY): added note about YouLy+ lyrics backend

* fix(PRIVACY): fixed link

---------

Co-authored-by: kilyabin <65072190+kilyabin@users.noreply.github.com>
2026-04-21 12:08:52 +02:00
Frank Stellmacher ae92c452b6 chore(aur): bump pkgver to 1.43.0 (#229)
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:08:38 +02:00
Frank Stellmacher 1ce199ee4b fix(ci): drop --auto from nix-lock PR merge (#227)
GitHub rejects \`gh pr merge --auto\` when the PR has no required
status checks and no required reviews — the error is "Pull request
is in clean status (enablePullRequestAutoMerge)". Our ruleset
requires neither, so auto-merge has nothing to wait for. Switch
to a direct merge instead. If we ever add required checks, --auto
can come back.

Validated against the v1.43.0 test release: PR #226 was created
by the workflow but the auto-merge call failed; merging it
manually with the same flags minus --auto succeeded immediately.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 00:00:37 +02:00
github-actions[bot] cb86bd7602 chore(nix): refresh lock + npmDepsHash for v1.43.0 (#226)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-04-20 23:59:12 +02:00
Frank Stellmacher 77598e10e9 chore(release): v1.43.0 (#225)
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:34:05 +02:00
Frank Stellmacher e7bfe276c3 ci(release): route nix lock refresh through PR + auto-merge (#224)
After enabling branch protection rulesets on main, the existing
direct push from the verify-nix job (`git push origin HEAD:main`)
is rejected. Push the refresh commit to a per-release branch
(`chore/nix-lock-refresh-v<VERSION>`) instead, open a PR, and
enable auto-merge so the change lands as soon as repo conditions
allow (0 required reviews + 0 required status checks → instant).

Adds `pull-requests: write` to job permissions so GITHUB_TOKEN can
create + merge the PR.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 23:16:25 +02:00
Frank Stellmacher f510223d2d feat(settings): compact spacing pass + row hover affordance (#223)
Tightens the Settings panels globally: section margin-bottom 32→20px,
card padding 20→12px, divider margin 12→8px, section header h2
16→15px. Adds a subtle accent-tinted hover background on every
.settings-toggle-row (extended to card edges via negative
horizontal margin). Vertical row padding kept at 2px so the diet
isn't undone by the hover affordance.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:58:19 +02:00
Psychotoxical 2d3be10c8f feat(users): show last-access timestamp per user
Maps Navidrome's lastAccessAt onto NdUser and renders it in the
User Management row as a localized relative time (Intl.RelativeTimeFormat
with the current i18n language). Tooltip carries the absolute
timestamp. Navidrome returns 0001-01-01 for never-accessed users —
detected and shown as the localized "Never" label.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:26:58 +02:00
Frank Stellmacher 459c9f688d feat(users): per-user library assignment + themed confirm modal (#222)
Adds Navidrome library assignment to the User Management settings
panel: GET /api/library + PUT /api/user/{id}/library wired through
new Rust commands. UserForm gets a checkbox picker (hidden for
admins, who auto-receive all libraries server-side) with inline
validation. User rows compacted to a single clickable line with
hover highlight; native confirm() replaced by a portal-based
ConfirmModal (theme-aware, ESC/Enter, vertically centered).

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 22:18:10 +02:00
Psychotoxical 6d3c50264a fix(mini): minimize main on open + cap width on non-tiling WMs
Restore the main-window minimize/unminimize behavior when opening the
mini player on Windows — the original WebView2 stall no longer applies
since the mini window is pre-created at startup. Also cap the mini
window at 400 px width so horizontal drag can't stretch the layout
across a whole monitor; skipped on tiling WMs (Hyprland, Sway, i3)
where the compositor manages sizing itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 21:13:57 +02:00
Psychotoxical 9f21edee80 feat(mini): queue-style meta block, action toolbar, vertical volume slider
Restructure the MiniPlayer to mirror the main window's queue panel layout:

UI changes:
- Meta block: cover + title/artist/album/year (year added to MiniTrackInfo)
- Action toolbar styled like .queue-round-btn (30px round, accent fill when
  active), in order: volume, shuffle | gapless, crossfade, infinite | queue
- Volume button opens a thin 5px vertical strip slider that drops down (click
  / drag / wheel to adjust). Right-click on the volume button mutes.
- Controls + progress moved to the bottom as a true footer (margin-top: auto),
  so they stay anchored even with the queue expanded
- Queue toggle moved out of the titlebar into the action bar (logically lives
  with the other queue/playback toggles)
- Window size bumped to 340x260 collapsed / 340x500 expanded for the new layout

Bridge changes (miniPlayerBridge.ts):
- MiniSyncPayload extended with volume, gaplessEnabled, crossfadeEnabled,
  infiniteQueueEnabled
- Bridge now subscribes to authStore in addition to playerStore so toolbar
  toggle states propagate cross-window
- New events: mini:set-volume, mini:shuffle, mini:set-gapless, mini:set-crossfade,
  mini:set-infinite-queue
- Bridge enforces gapless ↔ crossfade mutual exclusion (per CLAUDE.md gotcha)
  so the mini doesn't need to know about both states to act

Misc:
- Belt-and-suspenders user-select: none on .mini-player-shell * to kill
  Ctrl+A / mouse-drag selection that WebKit/WebView2 occasionally let through
2026-04-20 18:51:31 +02:00
Kveld. a7eb0eda72 fix padding (#221) 2026-04-20 17:41:48 +02:00
Kveld. 9687f90b54 fix: artist top songs continue playback (#220)
* fix: continue playback after top songs finish on artist page

* comments to english
2026-04-20 17:41:44 +02:00
Psychotoxical ccf6e6c886 fix(seekbar): commit seek on mouseup + ignore progress ticks while dragging
Ports the WaveformSeek UX changes from PR #219 (original fix by @cucadmuh)
onto test/seekable-streaming:

- Split previewFraction (during drag) / commitSeek (on mouseup) so audio_seek
  fires once per gesture instead of on every mousemove.
- Subscribe-effect skips external progress updates while isDragging — prevents
  cursor flicker from streaming/recovery ticks fighting the dragged position.
- Clear seekTarget in seek() catch branch so a failed seek doesn't permanently
  block progress updates.

Skips the byte-restart fast-path from #219: would defeat RangedHttpSource's
direct seek by restarting the stream after 450ms.

Co-Authored-By: cucadmuh <cucadmuh@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 17:28:20 +02:00
Psychotoxical 5aac4d4ed0 feat(audio): seekable streaming via RangedHttpSource + instant local playback
Reworks the manual streaming-start path so playback is seekable from the
first frame and hot-cache hits start without a multi-hundred-MB pre-read.

Why
- The legacy AudioStreamReader was a non-seekable ring buffer. Forward
  seeks during streaming let Symphonia consume the buffer through to EOF
  (next song), and backward seeks were silently broken.
- fetch_data() loaded `psysonic-local://` files via tokio::fs::read,
  blocking playback for seconds on hot-cache or offline files (e.g. a
  100+ MB hi-res FLAC) and producing ALSA underruns when the new sink
  swap arrived too late.

What changed
- New RangedHttpSource MediaSource: pre-allocates a Vec<u8> in track
  size, background ranged_download_task fills linearly from offset 0
  with HTTP Range reconnects. is_seekable=true; reads block on data,
  seeks update the cursor only. Picked when the server response carries
  Accept-Ranges: bytes + Content-Length.
- New LocalFileSource MediaSource: thin std::fs::File wrapper used for
  every `psysonic-local://` URL — Symphonia probes the first ~64 KB and
  starts playback before the rest is read.
- audio_play wires both paths through a generic PlayInput::SeekableMedia
  variant so the build_streaming_source pipeline stays single-shape.
- current_is_seekable AtomicBool on AudioEngine: audio_seek short-circuits
  with `not seekable` for the legacy fallback so the frontend restart-
  fallback (playerStore seek catch) engages instead of letting a forward
  seek consume the ring buffer.
- Dev-only [stream] / [seek] eprintln logs (cfg(debug_assertions)) so
  source selection, codec resolution (codec name + lossless flag + bit
  depth + rate + channels), download progress and seek targets are
  visible in the terminal during testing.
- content_type_to_hint now covers wav/wave/opus; URL-derived format
  hints strip the query string first and only accept known audio
  extensions (Subsonic stream URLs have no extension — would otherwise
  latch onto query-param fragments).

Compatibility
- Servers without Accept-Ranges fall back to the existing AudioStreamReader
  path; seek is rejected up-front so the existing restart-fallback runs.
- Radio + audio_chain_preload paths untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 01:27:31 +02:00
Psychotoxical ccff7be499 chore(settings): drop Alpha badge on AudioMuse toggle
Also removes the now-unused `hotCacheAlphaBadge` i18n key from all
8 locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:53:10 +02:00
Psychotoxical 99fc62e505 fix(themes/jayfin): WCAG AA contrast fixes for nav + primary buttons
Active nav-link: raise background to 22% accent + switch text/icon to
white — lifts from 3.30:1 to >10:1 (was AA-small fail). Left purple bar
preserves the Jellyfin brand cue.

Primary buttons (btn-primary, player-btn-primary, hero-play-btn,
album-card-details-btn, queue-round-btn.active): force font-weight 700.
White on #AA5CC3 measures 4.08:1 — bold glyphs compensate visually so
button labels remain clearly legible without shifting the brand color.

Not touched: accent-as-text on bg-card (4.09:1, needs separate
--accent-text token), hover tint #be70d8, white on danger red
(cross-theme issue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:45:14 +02:00
Psychotoxical dc819f3a2c feat(settings): admin-gated User Management tab via Navidrome REST API
Adds a dedicated Settings tab for managing Navidrome users (list, create,
edit, delete). Only visible to admins — gated by `/auth/login` probe on
mount. Uses Navidrome's native /api/user endpoints instead of Subsonic
(getUsers.view etc. return only the caller on Navidrome). All HTTP calls
go through new Rust commands to bypass WebView CORS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 23:35:14 +02:00
Psychotoxical 69dd374e80 chore(credits): add PRs #214, #215, #216, #217 to Settings → About
kilyabin: #214 (ease-out lyrics scroll + plain-lyrics bottom fade),
#215 (single-line YouLy+ source strings).
kveld9: #216 (opt-in floating player bar), #217 (Linux GPU-vendor
auto-detection for the WebKitGTK DMA-BUF renderer).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:15:21 +02:00
Psychotoxical 36caf3a14c refactor(linux): simplify GPU-vendor detection for DMA-BUF toggle
Follow-up on #217. Blind A/B test on NVIDIA + proprietary confirmed the
toggle helps, but the detection path had a few rough edges.

- Drop lspci and glxinfo fallbacks: the /proc/driver/nvidia/version +
  sysfs scan catch every realistic case, and spawning subprocesses in
  main() added startup latency for no win (glxinfo also isn't always
  installed — mesa-utils isn't a hard dep).
- Scan all /sys/class/drm/card* entries via read_dir instead of
  hardcoded card0/card1 — hybrid laptops and systems where only card1
  exists were previously missed.
- Remove 0x1022 from the AMD list: that's AMD's host/chipset vendor ID,
  not GPU. Only 0x1002 (ATI/Radeon) belongs here.
- Unknown vendor → leave WEBKIT_DISABLE_DMABUF_RENDERER unset instead
  of forcing =1. VMs, ARM SBCs and anything exotic keep the WebKitGTK
  default and do not get regressed by a guess.
- Only set the env var for the NVIDIA case. Intel/AMD on Mesa already
  default to DMA-BUF enabled in current WebKitGTK, so the explicit =0
  was redundant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:08:59 +02:00
Frank Stellmacher d145a7758e Merge pull request #217 from kveld9/feat/linux-gpu-dmabuf-autodetect
feat(linux): Auto-detect GPU vendor and configure DMA-BUF renderer
2026-04-19 22:07:39 +02:00
kveld9 fc0b8fbcab introducing WEBKIT_DISABLE_DMABUF_RENDERER 2026-04-19 15:40:29 -03:00
Psychotoxical 5a51c2c713 feat(floating-bar): liquid-glass look on macOS and Windows
Inspired by kveld9's PR #211 proposal. Adapted to stay within WebView2
and WKWebView GPU budgets — 8px blur instead of 28, theme-token colours
via color-mix instead of hardcoded white, no saturate/brightness/
contrast stack, no transitions, no !important.

Linux keeps the solid floating bar from PR #216 on every compositing
mode. Platform is detected once on mount via a data-platform attribute
on <html>.

Co-Authored-By: kveld9 <179108235+kveld9@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 20:00:10 +02:00
Psychotoxical fab1dbf863 perf(floating-bar): subscribe themeStore via selector
Both AppShell and PlayerBar read floatingPlayerBar via destructured
useThemeStore() without a selector, so every unrelated theme change
(accent, font, scheduler state, …) would force a re-render. Switch to
s => s.floatingPlayerBar so these components only re-render when the
toggle actually flips.

Follow-up to PR #216.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:44:17 +02:00
Frank Stellmacher 8a0810ffb4 Merge pull request #216 from kveld9/feat/floating-bar-toggle-v2
feat: floating player bar with toggle
2026-04-19 19:43:14 +02:00
Frank Stellmacher 6e34dc8160 Merge pull request #215 from kilyabin/am-lyrics-improvements
fix(lyrics): sidebar lyrics strings with YouLy+ source shows in one line
2026-04-19 19:37:57 +02:00
Psychotoxical 2aa88d26d4 chore(lyrics): rename springScroll.ts to easeScroll.ts
After PR #214 replaced SpringScroller with EaseScroller, the filename
no longer matched the exported class. Rename for clarity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 19:35:51 +02:00
kilyabin 22fe0fff74 fix(lyrics): sidebar lyrics strings with YouLy+ source shows in one line 2026-04-19 21:24:58 +04:00
kveld9 12bebdde06 some fixes 2026-04-19 14:23:14 -03:00
Frank Stellmacher 50c886fd6f Merge pull request #214 from kilyabin/am-lyrics-improvements
refactor(fs-lyrics): smoother line scrolling + bottom fade for plain lyrics
2026-04-19 19:20:02 +02:00
kilyabin 65413c954e feat(fs-lyrics): fade bottom edge of plain lyrics scroll viewport
Synced lyrics always fill the screen due to auto-scroll, so the
existing fsa-fade-bottom overlay naturally fades content below
the active line. Plain (unsynced) lyrics start from the top and
may not fill the viewport, leaving no content for that overlay
to fade.

Add mask-image to .fsa-lyrics-container when rendering plain
lyrics (--plain modifier class). The mask operates on the visible
scroll viewport rather than the content, so the bottom fade is
always present regardless of scroll position.
2026-04-19 21:00:46 +04:00
kilyabin 059900e85e refactor(lyrics): replace spring scroller with cubic ease-out animator
The spring model (velocity × damping per rAF frame) produced an
abrupt-looking lurch on each line change — especially noticeable at
the stiffness/damping values used (0.1 / 0.78).

Replace SpringScroller with EaseScroller: a fixed-duration (650 ms)
cubic ease-out animation that starts from the current scroll position
and decelerates smoothly to the target. Restarting mid-flight is clean —
the next call captures the container's current scrollTop as the new
start point, so fast line changes never skip or jerk.

Applied to both the fullscreen Apple-style lyrics and the sidebar
LyricsPane. User-scroll cancellation (stop() + 4 s resume) is
preserved unchanged.
2026-04-19 20:42:36 +04:00
Psychotoxical dea9d9b5a4 fix(settings): lyrics-sources DnD survives mode toggle
The psy-drop listener was attached in a useEffect keyed only on
setLyricsSources, so when the container was unmounted by the
{lyricsMode === 'standard' && …} wrapper (switching to YouLyPlus and
back), the listener stayed bound to the old DOM node and drag-to-reorder
silently did nothing.

Switch the container ref to a callback-ref stored in state, so the
effect re-runs on mount/unmount and always listens on the current DOM
node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:23:34 +02:00
Psychotoxical d191404f2d Revert "perf(fs-player): cut CPU under WebKitGTK software compositing"
This reverts commit 77edfaa867.

Restores the full Apple Music-style lyrics visuals from PR #205 on Linux:
rAF spring scroller, per-line scale transforms, text-shadows, title glow,
album-art/play-button outer glows, mesh-blob radial gradients, and the
FS-art opacity crossfade. PR #205's author noticed the Linux codepath
lost the intended feel — the CPU tradeoff is accepted.

The trigger-ref fix from 25537f27 for the lyrics menu stays in place.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 18:07:13 +02:00
kveld9 ffffe268ab introducing floating bar with toggle 2026-04-19 13:01:31 -03:00
Psychotoxical 81ed6db9d1 style(folder-browser): auto-contrast text on selected row
Replace hardcoded #fff on `.folder-col-row.selected` with a relative
OKLCH expression that picks black or white based on the accent's
lightness. Keeps rows readable on bright-accent themes (Muma, pastel
sets) without per-theme overrides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:44:08 +02:00
Psychotoxical 6878867e64 style(titlebar): theme-independent traffic-lights + song pill
Window controls use fixed macOS-style colored dots (red/yellow/green)
so they stay visible on every theme (broke on Middle-Earth etc. where
text tokens collapsed against the sidebar bg). Song indicator becomes
a dark translucent pill with light text + text-shadow for universal
contrast. Removed the "Psysonic" label — the sidebar logo already
covers it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:37:35 +02:00