New /tracks route with three sections:
- Hero "Track of the moment" — random pick with play / enqueue / reroll
- Random Pick rail — 18 song cards, rerollable; hero song deduped
- Browse all tracks — virtualized list (@tanstack/react-virtual),
paginated 50 at a time
Browse uses Navidrome's native /api/song?_sort=title&_order=ASC for
proper A-Z order (no Subsonic equivalent), with automatic fallback
to search3 on non-Navidrome servers. Search input drives search3
with 300ms debounce. Bearer token cached module-level, re-auth on 401.
Play button on rows + cards calls a new enqueueAndPlay() helper that
appends to the existing queue (skip if duplicate) and jumps to the
song — different from playSongNow which replaces the queue. Enqueue
button stays opaque (no hover-only).
i18n keys for sidebar.tracks + tracks.* namespace in all 8 locales.
New AudioLines sidebar icon. Sidebar entry inserted between
"All Albums" and "Build a Mix".
Performance: cover thumbnails dropped (uniform layout instead),
RAF-throttled scroll prefetch, hover transforms removed from cards
(WebKitGTK compositing-friendly).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Open smart playlist editing from playlist cards, load rules via Navidrome single-playlist API with safer fallbacks, and keep edit visibility aligned with ownership rules.
Also add/clean smart playlist locale keys across all supported languages and preserve smarter autogenerated naming behavior.
Show edit/delete actions on hover in playlist cards, support opening metadata edit directly from the grid, and render smart playlist covers from active-library tracks. Playlist detail now filters displayed songs to the selected library instead of hiding whole playlists.
Plain lines blur together once a card has more than a handful of
contributions; a simple accent-colored • in front of each row makes the
list scannable without changing the tone.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
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.
`.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.
* 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`.
- 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>
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.
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>
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.
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.
Block capture-phase dragstart in the webview so stray native HTML5 drags
(e.g. SVG reorder grips) do not create a stuck translucent GTK surface on
Wayland compositors.
Harden PsyDnD lifecycle when mouseup never reaches the webview: blur,
visibilitychange, pointerup/cancel in capture, and Escape cancel the
floating label without dispatching a drop.
Add -webkit-user-drag:none on sidebar customizer grips.
Each dashboard card (Album, Top Songs, Credits, Artist, Discography,
On Tour) is now a widget the user grabs and drops anywhere — reorder
within a column or move across columns. Layout persists per-install
via the new nowPlayingLayoutStore (Zustand + localStorage, same shape
as sidebarStore with an onRehydrateStorage guard for unknown IDs).
Drag uses psyDnD (useDragSource / psy-drop event, the mouse-event
based system from DragDropContext) — HTML5 native DnD is a no-go on
WebKitGTK Linux where it always shows a forbidden cursor. The whole
card is the drag handle; the column listens via a document-level
mousemove for the drop indicator and via a global psy-drop listener
that reads the latest hovered column from a ref, so drops below the
last card still land correctly regardless of column height.
Visibility is toggled from a layout menu in the top-right of the
dashboard — two sections (visible / hidden) plus a reset-to-defaults
button. No hide buttons on the cards themselves (keeps the card UI
uncluttered). An equal-height grid (align-items: stretch + min-height
on columns) keeps the drop zone active for the full vertical span of
either column.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the flat Now Playing page with a two-column dashboard: hero
(cover, metadata, badges, Last.fm stats + love button, release-age
tagline), and a grid of themed cards (Album tracklist with sliding
window, most-played-by-artist, credits, about-the-artist with Last.fm
bio fallback, compact discography contact-sheet with expand, upcoming
tours via Bandsintown). Adds lastfmGetTrackInfo + lastfmGetArtistStats
for global listener counts + userplaycount, plus Last.fm love/unlove
wired to the new hero button. Module-level TTL caches per entity keep
same-artist track switches instant. Artist-image guard filters the
well-known Last.fm no-image placeholder so the card collapses cleanly
when no real image exists.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up on #263. Refines the cross-tab search into a proper
command-palette-ish flow and polishes the UX around it.
Results-only mode:
- When a query is active, the tab content is hidden entirely; the
results list is the only thing shown. Unified list (was: in-place
current-tab filter + "In other tabs" panel underneath), sorted with
current-tab matches first and then by score.
- Removed the DOM-based hide/show logic and its CSS helper class, the
dead searchOtherTabs i18n key, and the searchHits/otherTabHits state.
Keyboard navigation inside the search field:
- ↓/↑ moves selection through the result list (clamped, no wrap).
- Enter activates the selected item (same flow as a click — clears
query, switches tab, scrolls + opens + flashes the target).
- Mouse hover syncs with the keyboard selection so the two input modes
don't fight each other.
- Selected item gets accent-border + tinted background; scrollIntoView
with block:'nearest' keeps it visible when the list is long.
Ctrl/Cmd+F (Settings page only):
- Window keydown listener registered on mount, removed on unmount →
the shortcut is live only while Settings is rendered.
- Opens the search input, focuses + selects it even if already open.
- preventDefault blocks the native WebKit find bar.
- Placeholder now hints at the shortcut: "… (Ctrl+F)" / "… (⌘F)" on
macOS, no new i18n keys needed.
Target flash animation:
- After navigating from a result, the target SettingsSubSection gets a
1.4s accent-colored border + glow pulse so the user immediately sees
which entry the result pointed at. Reflow trick lets it retrigger
when clicking the same result twice.
Misc:
- "Next Track Buffering" → "Buffering" in all 8 locales + the section
comment in Settings.tsx. The SETTINGS_INDEX keeps "next track" as a
keyword so old search habits still find it.
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cross-tab settings search:
- Static SETTINGS_INDEX covering every sub-section of every tab, with
free-form keywords per entry so "scrobble" finds Last.fm, "replay gain"
finds Playback, etc.
- matchScore() does substring-first ranking, falls back to a char-in-order
fuzzy match so typos still yield hits.
- Active tab keeps its in-place hide/show filter; matches from other tabs
render at the top as a clickable "In other tabs" list (tab badge +
sub-section title). Clicking clears the query, switches tab, and scrolls
+ opens the target sub-section.
- All 8 locales updated: new searchOtherTabs key, searchNoResults reworded
(old "try another tab" hint is now handled by the list itself).
Settings restructure polish:
- "Next Track Buffering" moved from Audio to the Storage tab — sits
between Offline Library and Downloads.
- Storage tab label renamed across 8 locales: "Storage & Downloads" →
"Offline & Cache" (equivalents per locale). nb locale was missing
tabStorage entirely; added. Tab id stays 'storage' to avoid breaking
persisted state.
- Library tab: Random Mix sub-section now opens by default (previously
both sub-sections started collapsed) and is renamed "Random Mix
Blacklist" in all 8 locales — the section is entirely a blacklist
config, the old title was misleading.
- Lyrics tab:
* Standard / YouLyPlus rows use the same toggle style as the "Show
lyrics as static text" row and stack vertically. Order swapped so
YouLyPlus is first.
* Provider list (Server / LRCLIB / Netease) moved to appear directly
under the Standard toggle as its sub-item, indented, instead of after
the static-text toggle.
* Classic / Apple Music-like rows likewise rebuilt as vertical toggle
rows.
- One pre-existing typo carried in from #261 fixed along the way: the
openAddServerInvite handler in Settings referenced the renamed tab id
'server' (pre-#259) — now 'servers'. [Note: already landed in PR #262,
keeping this commit clean of that.]
Co-authored-by: Psychotoxical <dev@psysonic.app>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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>
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.
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>
* 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>
* 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>
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>
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>
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>
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
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>
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>
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.
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>
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>
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>
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>
- 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>
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>
The player bar occasionally renders fully black for one frame on Linux
(WebKitGTK with software compositing) when an unrelated layer elsewhere
in the page invalidates. `contain: layout paint` makes the bar its own
paint boundary so it can no longer be pulled into a surrounding dirty
rect.
No-op on Wayland-with-GPU and on Chromium-based webviews (Windows,
macOS) — those don't exhibit the flash to begin with.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>