Commit Graph

200 Commits

Author SHA1 Message Date
Maxim Isaev 86704473ed fix(audio): stabilize loudness normalization and streaming startup behavior
Improve normalization consistency by separating cache refresh for non-playing tracks, hardening track-id cache lookups, and keeping UI gain state aligned with actual playback. Also reduce startup stalls by preferring non-seekable streaming when format hints are missing and by tuning ALSA/analysis paths to avoid underrun-prone churn.
2026-04-25 22:09:38 +03:00
Frank Stellmacher 93b724fc65 fix(search,ctx): enqueue on live-search click + reposition CM with right-click (#298)
Two coupled UX fixes around the header live search and the global context
menu, born from the same testing pass.

LiveSearch / MobileSearchOverlay
- Single click on a song no longer calls playTrack(track) without a
  queue argument. The store fell back to the existing queue, didn't
  find the new track, and ended up playing something the user couldn't
  navigate or see in the queue list — the player header showed metadata
  but the queue itself didn't contain it.
- New behaviour: enqueue([track]) + "Added to queue" toast. Whatever was
  playing keeps playing; the new track lands at the bottom and is
  visible/navigable. Mobile gets the same behaviour (tap-only).
- Desktop also gets a right-click context menu on song rows for users
  who want immediate playback or different routing (Play Now, Play Next,
  Add to Playlist, etc.). The dropdown stays open while the CM is up,
  and the row picks up the .context-active highlight so the user can
  see which song the menu refers to.

ContextMenu
- Removed the transparent fullscreen backdrop (z-index 998) that
  previously caught outside clicks. It also blocked right-clicks from
  reaching elements *underneath* it — so right-clicking a different
  song row to reposition the menu hit the backdrop instead, closed the
  menu, and never opened a new one for the row the user actually
  pointed at.
- Replaced with a document-level mousedown listener (gated on
  `contextMenu.isOpen`) that closes the menu when the click lands
  outside `menuRef`. Standard outside-click pattern, doesn't occlude
  the underlying UI, and right-clicking another row now naturally
  pivots the CM to that row.

i18n: new search.addedToQueueToast key in 8 locales.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 02:32:18 +02:00
Frank Stellmacher 67d51a0975 revert: roll back flatpak packaging experiment (#271 + follow-ups) (#297)
Reverts #271, #292, #293, #294, #295, #296.

The flatpak CI pipeline itself works end-to-end, but the installed bundle
surfaced three separate manifest issues during smoke-test on Wayland+NVIDIA:

1. GDK_BACKEND is not set, so without an X11 display in the sandbox GTK
   aborts with "Failed to initialize GTK".
2. libayatana-appindicator3 is not bundled in the GNOME 47 runtime, so
   libappindicator-sys panics the main thread.
3. The release binary is compiled via \`cargo build --release\` rather than
   \`cargo tauri build\`, so the \`custom-protocol\` feature is off and
   Tauri falls back to devUrl — the window opens but shows
   "Could not connect to localhost".

Rolling back so main stays on 1.43.0. A follow-up on the original PR
tracks the fixes needed before re-attempting.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 22:50:06 +02:00
kilyabin 8b369b3fa9 feat(flatpak): add Flatpak packaging and CI release pipeline (#271)
Adds full Flatpak support for Psysonic including:

- Flatpak manifest (org.gnome.Platform 47 + rust-stable + node20)
  with proper finish-args for MPRIS, Discord RPC, PulseAudio, Wayland/X11
- AppStream metainfo XML and .desktop entry
- In-app updater disabled at build time via VITE_PSYSONIC_FLATPAK=1
- CI job \`build-flatpak\` in release.yml: generates cargo/npm offline
  sources, patches manifest with release tag/commit, builds bundle via
  flatpak/flatpak-github-actions@v6, uploads .flatpak to GitHub release
- Release docs updated in CLAUDE.md (Flatpak bump and Flathub mirror flow)
2026-04-24 21:41:16 +02:00
Maxim Isaev 27a59f6b23 feat(playlists): integrate Navidrome smart playlist flow into playlists page
Move smart playlist creation and management into Playlists, including Navidrome-only gating, smart-name/icon presentation, and smarter refresh handling while server-side smart rules are being applied.
2026-04-24 16:41:17 +03:00
cucadmuh d5476b9249 perf(ui): fix spike when medulla-perch lines up with hair-fan gestures (#283)
The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Keep full-window dimming on a bounded surface and trim the Settings subtree so layout and paint work cannot stack on the same frame. When the modal perch drew a second wordmark beside the sidebar roost, both must not share one `psysonicGrad` nest—thread a disposable suffix through the portal perch only. Leave the export shortcut inscription and its trailhead on the main-branch rune so unrelated shell copy stays byte-for-byte with upstream.
2026-04-24 00:51:35 +02:00
Frank Stellmacher e86133738d fix(ui): swap Gapless / Infinite Queue toolbar icons (#284)
Reported in #274 — the icons for the Gapless and Infinite Queue toggles
were the wrong way round. The infinity symbol was on Gapless, but reads
much more naturally on Infinite Queue (a queue that never ends), and the
arrow-up-to-line on Infinite Queue had no obvious mapping at all.

Move infinity to Infinite Queue. Use MoveRight (right-pointing arrow) for
Gapless — visualises 'flows straight through to the next track without
stopping' and stays distinct from the chain/link family at 13 px in both
the QueuePanel and Mini Player toolbars.

Closes #274
2026-04-24 00:48:55 +02:00
Frank Stellmacher e721b3060d revert: temporarily back out medulla-perch perf change (#281) pending follow-up (#282)
Backing out #281 for now — follow-up coming with the chrome restyle path bounded more tightly. Will re-land with the corrected sweep ordering.
2026-04-24 00:34:49 +02:00
cucadmuh c54aa22e6b perf(ui): fix spike when medulla-perch lines up with hair-fan gestures (#281)
The harness parks the pointer on the micro-target medulla perch, then runs a hair-fan sweep through it; that combination used to restyle and repaint more of the chrome than the compositor could absorb. Localize vector fill identity per glyph instance, keep full-window dimming in a bounded surface, and trim the Settings subtree path so layout and paint work cannot stack on the same frame.
2026-04-24 00:29:55 +02:00
Frank Stellmacher 5c87a94170 fix(mini-player): portal volume popover so it cannot get clipped (#279)
`.mini-player` has `overflow: hidden`, and the absolute-positioned volume
popover (`top: calc(100% + 6px)`, ~150 px tall) was being clipped when the
queue was closed and the mini window was at its short height — visually
the slider rendered outside the visible area, even though it stayed
fully usable. Opening the queue grew the window enough to bring the
popover back inside the clip rect.

Render the popover via `createPortal` to `document.body` and position it
with `position: fixed` based on the trigger button's `getBoundingClientRect`,
mirroring the YearFilterButton pattern. Auto-flips above when there is
not enough room below (short mini windows). Repositions on window
resize / scroll. Outside-click handler now checks both the trigger button
and the popover ref since they no longer share a DOM ancestor.
2026-04-24 00:02:02 +02:00
Maxim Isaev a17d4c883d merge: integrate origin/main into feat/lucky-mix-flow 2026-04-24 00:30:40 +03:00
Frank Stellmacher b97b6c545b fix(windows): tighten WebView2 idle hooks from #273 review (#276)
- Type `window.__psyHidden` once via global declaration; drop 6 `(window as any)` casts.
- `WindowVisibilityProvider` now ORs `window.__psyHidden` into the hidden state
  (and into the initial state). On WebView2 `document.hidden` does not always
  flip when `win.hide()` is called, so effects gated by `useWindowVisibility()`
  can now actually skip creating intervals/rAF instead of relying solely on the
  per-tick fallback check.
- Slow visible-poll from 200 ms to 500 ms (5 → 2 wakeups/s for visibility alone).
- Fix stray leading-space indent in `App.tsx` `window:close-requested` comment.

Co-authored-by: Psychotoxical <dev@psysonic.app>
2026-04-23 22:50:36 +02:00
Ivan Pelipenko 0ead4fbeb1 fix(windows): idle WebView2 when Tauri windows are hidden (#273)
* fix(windows): stop GPU rendering when windows are hidden

* perf(ui): tighten hidden-window rendering mitigation

Align implementation and documentation with what the injected pause/resume
scripts actually do, reduce main-thread and compositor wakeups while windows
are invisible, and make lifecycle cleanup reliable.

Rust (Tauri)

- Document PAUSE/RESUME as compositor-oriented hints: set __psyHidden, optional
  --psy-anim-speed for CSS that opts in, and pause @keyframes via
  animation-play-state only (not CSS transitions, not arbitrary timers or rAF).
- Walk descendants under #root instead of document-wide querySelectorAll('*')
  to cut cost on large pages; skip the walk if #root is missing (flag still set).
- Call eval(PAUSE_RENDERING_JS) before hide() on all hide paths (tray menu,
  tray icon toggle, mini open/close/show-main, native mini close) so the
  webview sees __psyHidden while still fully schedulable.
- Shorten pause_rendering command rustdoc to match the script.

Frontend

- WindowVisibilityProvider: replace self-rescheduling useCallback with a single
  effect that keeps one pending timeout id, uses a cancelled flag on unmount,
  and syncs hiddenRef from document.hidden at mount (no orphaned timers after
  tests/HMR).
- WaveformSeek (preview + animated seekbar): while hidden, poll with a 400 ms
  timeout instead of requestAnimationFrame every frame; cancel both rAF and
  timeout on teardown.

Why

- Accurate comments prevent future refactors from assuming “full JS idle”.
- Smaller DOM walks and fewer rAF wakeups reduce spikes when hiding the main
  window or mini player on Windows WebView2 and elsewhere.

* perf(ui): pause global CSS + timers when Tauri hides the window

Builds on PR #273 (`7803d8e` + `0e07a73`). Adds a second HTML flag driven only by
Rust pause/resume inject so infinite animations still pause when WebView2 keeps
`document.hidden === false` after `win.hide()`, and stops a few periodic JS
timers while the window is invisible.

- `lib.rs`: set/remove `data-psy-native-hidden` on `<html>` in PAUSE/RESUME JS;
  document why in rustdoc (rest of pause/resume unchanged from PR tip).
- `components.css`: same `animation-play-state: paused !important` rules as
  `data-app-hidden` for `[data-psy-native-hidden="true"]`; refresh the comment.
- `App.tsx`: comment distinguishes tab visibility vs Tauri-native hide.
- `Hero.tsx`: do not run the 10 s carousel interval while hidden
  (`useWindowVisibility` + `__psyHidden` in tick).
- `PlaybackScheduleBadge.tsx`, `playbackScheduleFormat.ts`: no 400/500 ms
  intervals while hidden; skip ticks when `document.hidden` or `__psyHidden`.
2026-04-23 22:47:23 +02:00
Psychotoxical e9e61b9a05 refactor(lucky-mix): review follow-ups from Psychotoxical
- Drop the ~110 lines of dead full-screen overlay CSS that was never
  referenced in JSX (.lucky-mix-overlay*, .lucky-mix-cube*, full-screen
  @keyframes). Keep the .lucky-mix-pip* rules since the inline queue
  variant re-uses them.

- Extract availability gate into hooks/useLuckyMixAvailable.ts (pure
  predicate + hook). Replaces five duplicated inline checks in Sidebar,
  MobileMoreOverlay, RandomLanding, Settings/SidebarCustomizer, and the
  internal re-check in buildAndPlayLuckyMix.

- Add a cancel mechanic:
  * luckyMixStore gets a cancelRequested flag + cancel() method
  * LuckyMixCancelled sentinel is thrown from a bailIfCancelled() helper
    sprinkled between await boundaries in the build loop
  * catch-block swallows the sentinel silently (no toast, no error state)
  * auto-cancel subscription on playerStore detects manual user track
    changes (anything not in queuedIds) and flips cancelRequested so the
    finished mix does not later overwrite the user's choice
  * inline .queue-lucky-loading dice indicator is now a button — click
    triggers explicit cancel, hover fades the dice to signal the action

- Restore the queue snapshot when the build fails before ever starting
  playback, so the user does not land in an empty player after an error.
  If playTrack already ran, leave current state alone (their track plus
  whatever was enqueued is more useful than a stale pre-click queue).

- Rework locale labels to consistently carry the "Mix" suffix so the
  entry reads well next to "Random Mix" / "Random Albums" in the
  sidebar: DE Glücks-Mix, ES Mezcla Suerte, FR Mix Chance, NB Lykkemiks,
  NL Geluksmix, ZH 好运混音. EN stays "Lucky Mix", RU stays
  «Мне повезёт». Toast/settings/randomNavSplit copy aligned to the new
  names. New luckyMix.cancelTooltip key added in all 8 locales.

AudioMuse gating stays — as Cuca confirmed, the feature's quality relies
on a correct similar-track selection, so the requirement is intentional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 18:41:46 +02:00
Maxim Isaev ab5c8e0b48 feat(lucky-mix): instant mix from your preferences
Lucky Mix targets ~50 tracks in one run: pick seeds from your most-played artists/albums and 4+ rated songs, add similar tracks, then fill the mix with random library picks, skipping anything you rated 1–2.
On start it trims the old “upcoming” tail so the queue does not show stale next tracks, starts playback on the first viable seed, routes to Now Playing, and can emit structured steps to the backend debug log.
2026-04-23 18:42:52 +03:00
Frank Stellmacher 694567843f feat(player): polish sleep-timer UI — circular ring + in-button countdown (#272)
Replaces the text-pill schedule badge with a circular SVG progress ring
around the play/pause button. Accent→lavender gradient stroke, counter-
clockwise depletion synced to the armed deadline, subtle accent glow.

While a timer is armed, the Play/Pause icon inside the button is
swapped for a two-line stack: a small Moon (sleep timer) or Sunrise
(delayed start) glyph above the countdown (m:ss / h:mm:ss). The icon
is the mode marker so the ring can stay unified with the rest of the
app's accent palette.

Redesigns the schedule modal with a mood-tinted header (Moon for
"Pause after", Sunrise for "Start after"), soft radial accent glow in
the background, slide-up open animation. Circular glass close-button
scoped to this modal, with hover rotation + focus ring. Custom-minutes
field gains an inline "min" suffix. A live preview line at the bottom
shows when the action fires ("Pauses at 23:47" / "Starts at 23:47"),
updating as the user hovers a preset or types. Chips gain a small
hover lift plus accent-coloured shadow.

Also fixes a pre-existing duplicate-tooltip on the play/pause button:
title= attributes removed alongside the data-tooltip (title violates
the project's "never native tooltips" rule, so both showed at once).

Adds scheduledPauseStartMs / scheduledResumeStartMs to the player
store so the progress ring has a total-duration baseline, set and
cleared alongside the existing deadline fields.

New usePlaybackScheduleRemaining hook wraps the store subscription
and 500 ms tick into a single hook used by all three player views
(PlayerBar / FullscreenPlayer / MobilePlayerView).

i18n: delayPreviewPause / delayPreviewStart keys across all 8 locales.

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 00:13:28 +02:00
cucadmuh 624ce56faf feat(player): sleep timer and delayed start via long-press on play/pause (#270)
Add scheduled pause and resume timers in the player store, cleared on stop
and track changes. Long-press opens a compact preset modal anchored above
the transport row; one-tap presets plus custom minutes. Portaled countdown
badge on the play button; clear the long-press click guard when the modal
closes so the first play/pause click works after scheduling.
2026-04-22 23:32:44 +02:00
cucadmuh c5dfabf739 feat(sidebar): long-press drag to reorder and hide nav items (#269)
Hold for 1s then drag to reorder configurable library and system links;
drop outside the sidebar to hide items (same visibility as Settings).
Show a trash hint near the cursor when outside removal applies.
Extract shared reorder helpers into src/utils/sidebarNavReorder.ts and
reuse them from the Settings sidebar customizer.
2026-04-22 23:31:30 +02:00
Frank Stellmacher 7c9a300022 feat(share): library deep links (psysonic2) with paste + toolbar actions (#261)
* feat(share): library deep links (psysonic2) with paste and toolbar actions

Add base64url-encoded payloads for track, album, artist, and ordered queue.
Handle paste outside inputs to switch server, validate entities, navigate or
play. Reuse clipboard helper for context menu, queue panel, album and artist
headers. Tests distinguish server invite strings from library shares.

* fix(share): play partial queue when some shared tracks are missing

Skip unavailable song IDs when pasting a queue share while preserving order.
Toast when some tracks are missing; error only if none resolve. Add
openedQueuePartial and queueAllUnavailable i18n; remove queueTracksMissing.

* feat(settings): paste server invites globally and scroll to add form

Handle psysonic1- magic strings outside inputs: navigate to settings or login
with prefilled add-server fields. Decode invites embedded in surrounding text.
Scroll the add-server block into view when opening from a paste so long server
lists stay oriented.

---------

Co-authored-by: Maxim Isaev <im@friclub.ru>
2026-04-22 11:21:00 +02:00
Frank Stellmacher 2a496c600b refactor(settings): thematic tab regroup + accordion sub-sections + in-page search (closes #257) (#259)
* refactor(settings): regroup tabs thematically + accordion sub-sections (progress on #257)

New tab structure: Servers (first) · Library · Audio · Lyrics · Appearance ·
Personalisation · Integrations · Input · Storage · System · Users.

Reusable SettingsSubSection (native <details>) wraps related settings per tab,
with an optional action slot for per-section reset buttons.

Moved:
- Lyrics sources + sidebar lyrics style → Lyrics
- Sidebar / artist layout / home customisers → Personalisation
- Last.fm / Discord / Bandsintown / Now-playing share → Integrations
  (with an opt-in privacy banner at the top)
- Tray / minimize / Linux smooth-scroll → System (new App-Verhalten group)
- Preload mini-player / custom titlebar / show artist images → Appearance visual options
- Language picker → System

Audio, Appearance and System tabs are now fully accordion-grouped.
Library tab reduced to Random Mix + Ratings accordions.
Servers tab is its own home (server cards + AudioMuse + logout) and the default
landing tab.

Indicators in the titlebar and offline banner route to the right tab.
EN + DE translations added for the new tab labels and privacy banner; other
locales fall back to English until the full i18n pass lands.

* refactor(settings): audio/input/storage/system accordions, in-page search, Navidrome focus

Settings refactor progress — every tab is now accordion-grouped:
- Audio: Output · Hi-Res · EQ · Playback · Next-track buffer
- Input: Keybindings · Global shortcuts (reset in accordion action slot)
- Storage: Offline · Downloads
- System: Language (default open) · Behavior · Backup · Logging · About · Contributors
- Contributors: own card grid with per-contributor expand, sorted by
  contribution count. Changelog accordion removed (release-notes link and
  "show changelog on update" toggle now live inside About).
- About: Navidrome focus; AI credit and Special Thanks removed; new
  Maintainers row (Psychotoxical + cucadmuh).

In-page search: magnifier icon next to the Settings title. Click opens a
240px search field. Filters the active tab's sub-sections by title match
(data-settings-search). Cross-tab search is tracked as a follow-up.

serverCompatible updated in all 8 locales from "Compatible with: Navidrome
Gonic Airsonic Subsonic" to a Navidrome-first wording that warns other
Subsonic-compatible servers may have reduced functionality.

Full i18n pass (tabLyrics/tabPersonalisation/tabIntegrations, aboutDesc,
privacy banner, etc. in fr/nl/zh/nb/ru/es) follows in a separate commit
after visual review.

* i18n(settings): complete localisation pass for refactored tabs

Added in all 8 locales (fr, nl, zh, nb, ru, es — en+de already in place):
- Tab labels: tabLibrary, tabServers, tabLyrics, tabPersonalisation, tabIntegrations
- inputKeybindingsTitle, searchPlaceholder, searchNoResults, aboutMaintainersLabel
- integrationsPrivacyTitle, integrationsPrivacyBody
- aboutContributorsCount_one/_other (ru also _few/_many for proper plural forms)
- aboutDesc rewritten to describe a Navidrome-focused player instead of a
  generic Subsonic-compatible one.

Removed dead keys in all 8 locales:
- aboutAiCredit, aboutSpecialThanksLabel — no longer rendered
- changelog — inline changelog accordion removed
- tabServer, tabGeneral — superseded by new tab structure

---------

Co-authored-by: Psychotoxical <dev@psysonic.app>
2026-04-22 02:25:56 +02:00
cucadmuh f6f76723d8 feat(servers/settings): magic string invites, Navidrome admin share, and add-user validation (#258)
* feat(servers): magic string invites and duplicate-aware labels

Add psysonic1- share payloads (URL, Subsonic credentials, optional server
name). Login and add-server forms decode on input and keep magic field
below standard fields. Navidrome User Management generates strings after
PUT password updates where required. Resolve duplicate display names as
username@host in chrome, settings, and login.

* feat(servers): tighten magic-string import and admin share flow

After a decoded magic string, username is read-only and the password field
shows a fixed-length mask so length is not inferred. Password reveal stays
disabled until saved-server quick connect (login) or reopening the add-
server form. Navidrome admin share UI persists the password via API before
copy and adds a plaintext-handling disclaimer. Locales updated.

* feat(settings): save new Navidrome user and copy magic string

Add a non-admin "Save and get magic string" flow: create the user, assign
libraries when needed, then encode credentials to the clipboard. Reuse
the plaintext-handling disclaimer without the password-update hint meant
for edits. Strings added for all locales.

* fix(settings): tighten Navidrome add-user validation and submit feedback

Require username, display name, and a non-empty password (trimmed) for new
users; gate Save and “save and get magic string” on explicit checks with
toast feedback. Show red borders only after a failed submit, and clear
them when the user fills the fields. When editing, keep an empty password
as “unchanged” and validate identity fields with a dedicated message.
Strings: userMgmtValidationMissingIdentity across locales.
2026-04-22 00:45:28 +02:00
cucadmuh b61c168430 fix(ui): overlay scrollbars, resizer hit-test, and Linux mini wheel (#255)
Add OverlayScrollArea with shared thumb metrics and drag handling; size the
thumb against the rail track height so panel insets cannot push it past the
visible rail. Route scroll uses a stable viewport id; Genres restores scroll
and infinite-scroll observation against that viewport (merged with upstream
virtualized genres list and lazy routes behind Suspense).

Suppress queue resizer activation when the pointer targets the main-route
overlay scrollbar; disable the resizer while dragging the thumb and use a
grabbing cursor on the body.

Apply WebKitGTK smooth wheel to the mini webview as well as main; the mini
window reapplies the persisted setting after auth store hydration.
2026-04-21 22:44:28 +02:00
Frank Stellmacher 2318f9e07a feat(albums): add 'Enqueue' option to album cover, context menu and multi-select toolbar (closes #253) (#256)
Per bcorporaal's request in #253: making a queue from multiple albums
required either replacing the queue (Play) or going into each album
detail page first. Three new entry points cover the common workflows:

1. Hover button on the album cover next to Play. Both buttons share
   the same accent style and size so the secondary action is just as
   readable as the primary — first attempt with a darker pill was hard
   to see and unbalanced the hover.

2. Context-menu entry "Enqueue Album" between "Open Album" and "Go to
   Artist". The i18n key already existed (was used by the album-song
   sub-context); just wiring up the action.

3. Multi-select toolbar in Albums.tsx: new "Enqueue (N)" button placed
   before "Add Offline" so power users selecting several albums no
   longer have to right-click. Plus a context-menu entry "Enqueue N
   Albums" for the same flow when a multi-album right-click happens
   anywhere else.

All multi-album fetches use Promise.all(getAlbum(...)) — Navidrome
handles parallel requests instantly (per memory note from PR #246).

i18n: 4 new keys per locale (3 with pluralisation: contextMenu.enqueueAlbums,
albums.enqueueSelected, albums.enqueueQueued).

Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:39:46 +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 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
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 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 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
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 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
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 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 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 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
Psychotoxical 71fbc717f6 fix(mini-player): unhang Windows by pre-creating the mini webview
Creating the second WebView2 webview lazily from the open_mini_player
invoke handler reliably stalled Tauri's event loop on Windows — the mini
opened blank white, neither main nor mini could be closed, and the user
had to kill the process via Task Manager. The builder-then-minimize-main
combo racing with WebView2's first paint seems to be the trigger.

Fix: extract a build_mini_player_window helper and call it once in
.setup() on Windows so the webview is created (hidden) in the startup
context, not from an invoke command. open_mini_player is now a pure
show/hide on all platforms. Windows also skips the main.minimize() call
since that interacted with the hang; macOS + Linux keep the existing
behaviour of minimising main when the mini opens.

Other changes bundled in:
- Switch Windows from the custom in-page titlebar back to native window
  decorations (the earlier decorations(false) move on Windows was part
  of the hang surface — safer to keep the OS chrome there, Linux still
  uses the custom bar).
- Re-emit mini:ready on window focus so every open of the pre-created
  mini forces a fresh snapshot from main's bridge, even when the mount-
  time emit raced past the bridge during startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 15:31:45 +02:00
Psychotoxical d33c7042b6 fix(mini-player): show action buttons on macOS too
The custom titlebar was rendered only on Win/Linux, so macOS users had
no way to toggle the queue, pin-on-top or expand back to the main
window — the native macOS titlebar takes care of dragging + close, but
those three actions live entirely in app code.

Render a slim transparent in-page strip on macOS as well, holding just
the queue / pin / maximize buttons right-aligned. Close is omitted on
macOS since the red traffic light already does that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 14:06:46 +02:00
Psychotoxical 16491f6321 feat(mini-player): custom titlebar with action icons; cover/meta/controls layout polish
- Drop the native titlebar on Windows + Linux (decorations: false in
  open_mini_player); macOS keeps the system titlebar with traffic lights.
- Add a slim 26 px custom titlebar with drag region, current track
  title, and the queue / pin / open-main / close action icons.
- Remove the bottom toolbar — those four buttons now live in the
  titlebar where they're easier to reach.
- Refresh the player layout: cover shrinks 112 → 84 px, the right
  column gets title / artist / transport in a single 84 px-tall block,
  progress bar spans the full width below.
- Add a miniPlayer i18n namespace (showQueue, hideQueue, pinOnTop,
  pinOff, openMainWindow, close, emptyQueue) localized for all 8
  supported locales — previously the tooltips were hard-coded English.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:28:28 +02:00
Psychotoxical 60da17f7cc feat(mini-player): user-bindable keyboard shortcut to toggle mini player
Adds an 'open-mini-player' entry to the in-app keybindings (Settings →
Shortcuts), default unbound. Pressing the chord from the main window
opens the mini and minimises main; pressing it from the mini hides the
mini and restores main — both paths invoke open_mini_player which
already handles the toggle.

The mini window has its own keydown listener (same pattern as Space /
arrows for play/skip) that reads the binding from useKeybindingsStore.
Binding changes in main propagate to an open mini through the existing
storage-event sync (now also covers psysonic_keybindings) so the user
doesn't need to restart the mini after rebinding.

Localized in all 8 supported locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 13:05:06 +02:00
Psychotoxical 8cc115cade fix(mini-player): restore window position + queue-open state across launches
Two regressions in the mini player's persistence story:

1. Window position was lost on every reopen. set_position() called on a
   hidden window is unreliable on Linux WMs (Mutter, KWin re-centre on
   show). Worse, the WM-induced re-centre fired WindowEvent::Moved with
   the centre coords, which the throttled persister happily wrote to
   disk — so the saved position turned into "centre" within seconds.

   Fix:
   - Initial open uses WebviewWindowBuilder::position() (logical pixels,
     scaled via the primary monitor) so the window is created at the
     right spot.
   - Re-show path re-applies the saved position with set_position AFTER
     show().
   - Both paths call mark_mini_pos_programmatic() before triggering the
     move; persist_mini_pos_throttled ignores Moved events within 1 s of
     a programmatic mark, so WM-induced and self-induced moves no longer
     overwrite the user's position.

2. The queue panel always opened collapsed regardless of the previous
   session. Persist queueOpen to localStorage (psysonic_mini_queue_open)
   and resize the window to the stored expanded height on mount when
   the saved state was 'open'. Brief jump from 180 px to expanded is
   unavoidable since localStorage only lives in the JS layer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:54:00 +02:00
Psychotoxical e07ef0ebf1 feat(queue): collapse ReplayGain into a click-to-expand badge
Per @cucadmuh's feedback: the presence of ReplayGain matters more than
the actual numbers. The tech bar now shows a small 'RG' pill on line 1
whenever the track has any RG metadata; hovering reveals the values via
tooltip, clicking persistently expands the second line.

- Source icon stays inline before the format string (was getting
  separated from FLAC by the centred stack layout)
- Pill uses --accent-tinted color-mix so it adapts to every theme
- ChevronDown rotates on expand for a clear affordance
- New persisted state: themeStore.expandReplayGain (default collapsed)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:34:52 +02:00
Psychotoxical a48159d302 feat(queue): split tech bar into two lines when ReplayGain is present
The tech info strip in the queue header was a single ellipsised line, so
ReplayGain values (Track / Album / Peak) got cut off on tracks with a
full codec + bitrate + samplerate prefix. Now the strip wraps into two
lines whenever RG values exist:

- Line 1: codec · bitrate · bit depth/sample rate (unchanged)
- Line 2: 'ReplayGain · T … dB · A … dB · Peak …' — slightly smaller
  and dimmed for hierarchy, ellipsises independently if it still
  overflows.

Tracks without RG metadata stay one line as before. New i18n key
`queue.replayGain` ('ReplayGain') added to all 8 locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:24:46 +02:00
Psychotoxical 2871db9a96 feat(mini-player): persistent geometry, queue DnD + context menu, overlay scrollbar, live theme sync
This iteration fills in the pieces that make the mini player usable as a
daily standalone window:

- Window position persists to <app_config_dir>/mini_player_pos.json on
  every WindowEvent::Moved (throttled 250 ms); first launch lands in the
  bottom-right of the main window's monitor.
- Queue keeps a 260 px floor when expanded (~2 visible rows). The
  user-resized expanded height is restored on next toggle via
  localStorage so reopening doesn't snap back to the default 440 px.
- set_mini_player_always_on_top(true) now forces a false-then-true cycle
  so the WM re-evaluates the layer after a hide/show; the frontend also
  re-asserts the pin state on mount and on window focus, so the user no
  longer has to click the pin button twice for it to stick.
- Native scrollbar in the queue is hidden in favour of a JS-driven
  overlay thumb so items use the full width of the queue area.
- PsyDnD reorder works inside the mini queue: drag emits 'mini:reorder',
  the bridge calls reorderQueue on the source-of-truth store in main.
- Localized queue-item context menu (MiniContextMenu) with Play now /
  Remove from queue / Open album / Go to artist / Favorite / Song info.
  Cross-window actions forward to main via new bridge events
  (mini:reorder, mini:remove, mini:navigate, mini:song-info); a
  psy:navigate CustomEvent picked up by AppShell handles routing.
- Theme, font and language changes in main propagate live to the mini
  via a 'storage' event listener that re-hydrates the persisted Zustand
  stores and calls i18n.changeLanguage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:08:05 +02:00
Psychotoxical 42ad24cce1 fix(player-bar): use data-tooltip on time toggle (not native title)
Native `title` attribute renders an unstyled OS tooltip and bypasses the
TooltipPortal — convention in this codebase is `data-tooltip="…"` so the
hover hint matches every other player-bar control.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:04:29 +02:00
Kveld. eb747ba1ae introducing timer toggle player bar (#212) 2026-04-19 12:04:01 +02:00
Psychotoxical 6bd2bfc01c feat(changelog): replace auto-modal with sidebar banner + /whats-new page
Removes the giant startup changelog modal. After an update, a compact
neutral-palette pill now sits above Now Playing in the sidebar saying
"Changelog — vX.Y.Z". Clicking opens a proper /whats-new page in the
main content area; X dismisses. Page renders the current version's
CHANGELOG entry with inline Markdown (headings, lists, blockquotes,
hr, bold/italic/code, and real [label](url) links that open via the
Tauri shell plugin).

Visual tokens are hardcoded slate/gray/blue so the banner and page look
identical across every theme (dark, light, skeuomorphic, whatever) —
requested for consistent contrast. Auto-mark-as-seen removed from the
page; the banner only goes away on explicit dismiss, so users can
re-read as often as they want.

Settings → About gains a "Release notes" row with a link that resets
the seen-version and opens the page, so the banner can be retriggered
manually (also helpful for dev builds ahead of the current tag).

The existing "Show changelog on update" toggle now gates the banner
instead of the modal; description strings updated in all 8 locales.

fix(themes): WCAG contrast audit — mocha + winmedplayer + wista

Mocha (Catppuccin dark)
  .track-size used --ctp-overlay0 directly (3.36:1 on bg-app, 2.57
  on bg-card). Swapped to --text-muted → 7.37 / 5.65. Fix also lifts
  macchiato/frappe/latte which shared the failure.

WinMedPlayer (Luna)
  --text-muted #b8d0f8 (3.87:1 on bg-app) → #e8f0ff (5.28)
  --border   #2a5090 (1.31 on bg-app)   → #071027 (3.12, meets 3:1 UI)

Wista (Vista Aero)
  Lyrics pane sits on bg-sidebar #0e1e3e but used --text-primary
  (#0d1d3c) for active lines — 1.01:1, literally invisible. Added
  component overrides: .lyrics-line / .lyrics-status / word-synced
  variants → #aac8f0 (9.60), .lyrics-line.active → #ffffff (16.48).
  Palette: --warning #c8980c → #735a00 (2.37 → 5.91 on bg-app),
  --text-muted #4870a8 → #3f6aa0 (4.23 → 4.65 on bg-card).

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