Commit Graph

460 Commits

Author SHA1 Message Date
cucadmuh db645ca72f fix(queue): resolve queue rows on scroll so off-window items stop showing '…' (#1236) 2026-07-05 14:07:16 +03:00
cucadmuh 32a21ec2c3 fix(playlist): batch playlist writes past the GET URL limit (#1227) (#1235)
* fix(playlist): batch playlist writes past the GET URL limit (#1227)

Adding tracks failed past ~341 songs because the write path re-sent the
entire song list as createPlaylist.view?songId=<all> query params on a GET,
blowing past the server's ~8 KiB URL limit. Writes now append incrementally
via updatePlaylist.view?songIdToAdd=<batch> (and songIndexToRemove for
clears/removals) in 150-id batches, so there is no practical size cap. A
per-server in-memory membership cache removes the full getPlaylist refetch
on every dedup, fixing the "slow add on big playlists" report.

Layering detangle (keeps the new cache from adding dep-cruiser cycles):
- lib/api/subsonicPlaylists.ts is pure of the store again; cache invalidation
  on write failure moved to the feature callers' catch blocks.
- membership cache extracted to the core layer (src/store/playlistMembershipStore.ts)
  so offline/orbit/contextMenu/playlist read it directly instead of routing
  through the @/features/playlist barrel.
- severed the offline -> playlist-barrel edge (pinnedOfflineSync name fallback
  through the live playlist list was dead: nameless callers are all gated by
  isSourcePinnedOffline, where offline meta already carries the name).
- confirmAddAllDuplicates moved into the playlist feature; contextMenu submenus
  import the add/merge helpers via the playlist barrel, not deep paths.

dep-cruiser baseline regenerated: 742 -> 714 (net -28, all no-circular). The
churn in the baseline is path-shift of the frozen playerStore SCC (cover/
playback/orbit), not new coupling; the new playlist modules have zero violations.

* docs(changelog): record #1235 playlist URL-limit fix (changelog + credits)

* fix(playlist): seed membership cache from full list, not library-scoped view (#1235 review)

F1: runPlaylistLoad seeded the dedup membership cache from the
library-scope-filtered songs, so out-of-scope members looked "new" and
addTracksToPlaylistWithDedup/collectMergeSongIds could re-add them as
duplicates. Cache now holds the full unfiltered server list while the UI
still shows the filtered view; add a regression test.

Also documents the two accepted trade-offs flagged in review:
- F2: >batch updatePlaylist clears then appends non-atomically (URL-limit
  workaround); a mid-step failure truncates server state, cache invalidation
  lets the client re-read truth.
- F3: dedup read-modify-append isn't atomic across the await; rare missed
  dedup on concurrent adds, self-heals on next load.
2026-07-05 01:24:04 +03:00
cucadmuh 5e075b2c36 fix(connection): ignore spurious navigator.onLine offline hint in Tauri (#1234) 2026-07-04 20:56:31 +03:00
cucadmuh bf0e73ce6c feat(artists): album vs track credit mode browse (#1209) (#1232) 2026-07-04 17:39:58 +03:00
Psychotoxical 77ab95170d refactor(ipc): complete tauri-specta typed-IPC cutover, contract guards, and layering cleanup (#1230) 2026-07-04 17:00:31 +03:00
cucadmuh fdbb9deac6 fix(playback): ReplayGain prefetch, gapless UI sync, library peak column (#1231) 2026-07-04 16:00:03 +03:00
Alexander Kirichev 9183c3d657 Added Bulgarian translation (#1228) 2026-07-03 14:59:31 +03:00
Psychotoxical 114ad4633f docs(changelog): record #1225 frontend restructure (changelog + credits) (#1226)
Retroactive changelog + credits for the already-merged feature-folder restructure (PR #1225). Adds a 1.50.0 Changed entry (internal restructure, no user-facing change; additional architecture credited to cucadmuh) and a contributions line under Psychotoxical. cucadmuh is intentionally not added to the settings credits list.
2026-07-01 17:01:52 +02:00
Psychotoxical e1ff4385d6 feat(settings): add "Square Corners" appearance toggle (#1215)
* feat(settings): add "Square Corners" appearance toggle

A Display toggle in Settings → Appearance → Visual Options that overrides
the active theme to render cards and cover art with square (non-rounded)
corners, for a sharper, boxy look.

Covered surfaces: grid cards (album/playlist/artist/song/because) and
their covers, detail-header heros (album/playlist/tracks), the Now
Playing / Radio view, the fullscreen player, the cover lightbox, the
queue panel cover, and the mini player.

Persisted in themeStore and driven by an `html[data-square-corners]`
attribute set in App.tsx — applied in both the main and mini-player
webviews (the mini player rehydrates the theme store on the shared
`storage` event). Strings added for all 12 locales.

* docs(changelog): add Square Corners to 1.50.0 CHANGELOG and What's New
2026-06-29 06:48:26 +02:00
cucadmuh 6b1a898151 docs(release): sync 1.49.0 CHANGELOG, What's New, and credits (#1211)
Set the 1.49.0 release date, fix Added PR order for Polish (#1185),
refresh WHATS_NEW highlights for missing user-facing fixes, and credit
ImAsra for WinGet release automation (#1077).
2026-06-29 02:33:40 +03:00
Psychotoxical fcdb58e0d8 fix(sidebar): reorder sidebar nav items by id to stop drag drift (#1206)
* fix(sidebar): reorder nav items by id instead of positional index

The Settings sidebar customizer rendered library rows with one filter and
reordered them with another, so a hidden/gated item (luckyMix when AudioMuse
is unavailable) made the rendered list shorter than the reorder list and drag
indices landed one slot off — rows jumped back on drop. Replace the index-based
reorder with an id-based primitive shared by the customizer and the in-sidebar
long-press DnD, so a render filter can no longer desync the move. Unknown,
cross-section, conserved, or no-op drops are rejected.

* docs(changelog): note sidebar reorder fix (#1206)
2026-06-28 15:42:35 +02:00
Psychotoxical 184501744b fix(cover): restore full-resolution album and artist covers (#1205)
* fix(cover): build cover tiers from the full-resolution source

Derive the larger tiers from the decoded download instead of re-reading the just-written smaller tier (resize never upscales, so 512/800 were stored at the small resolution). Resolve the full-res 2000 tier exactly so it is actually downloaded and cached. Bump the cache layout stamp to drop already-poisoned tiers.

* fix(cover): open the cover lightbox at full resolution

Require the exact requested tier for the full-res helper so a smaller warmed tier no longer pins the lightbox to a downscaled image. Race the 2000 fetch against a 500ms opening window, show the 800 tier meanwhile, and persist 2000 for the next open. Adds an opening animation (reduced-motion aware) and tests.

* docs(changelog): note full-resolution cover fix (#1205)

* fix(cover): keep full-res peek exact in peek_batch and the grid seeder

Review follow-up: ensure-path peek alone left a hole — cover_cache_peek_batch still laddered a 2000 request down to a smaller tier, and the in-memory grid seeder wrote that smaller path under the 2000 key, so Hero/fullscreen/lightbox surfaces (which peek 2000 before ensure) still showed a downscaled cover. Share one exact-2000 rule (peek_plain_cover_tier) across ensure and peek_batch, and never seed the full-res key from a smaller tier file.
2026-06-28 04:23:12 +02:00
cucadmuh 281e86fd3b fix(queue): persist timeline play history across queue replace (#1096) (#1204)
* fix(queue): persist timeline play history across queue replace (#1096)

Add a session-scoped play-history buffer (with play_session cold bootstrap)
and timeline UI that shows history + current + upcoming without mutating
the canonical queue or Subsonic sync.

* fix(queue): pin timeline current to top and replay history in-place

Timeline scroll matches queue mode (current at top). History clicks insert
after the playing track instead of replacing the queue, and replayed tracks
stay visible in the history strip.

* docs: add CHANGELOG and credits for timeline play history (PR #1204)

* fix(queue): resolve cross-server cover art for timeline history

Include album/cover ids in play_session bootstrap rows, prefetch history
refs through the queue resolver per server, and resolve before replay so
Now Playing artwork works for inactive-server tracks.

* fix(now-playing): stop playbackReport on cross-server track switch

Send stopped to the previous server's playbackReport session when the
playback server changes (queue click, history replay, etc.) so Who is
listening clears on the server that was showing the prior track.

* fix(queue): close timeline history review gaps for PR #1204

Defer play_session bootstrap until the library index is ready with retry
while timeline mode is active; resolve history and queue rows by serverId
+ trackId for mixed-server queues; add tests for bootstrap defer and ref lookup.

* chore(queue): remove dead timeline scroll guard in QueueList

Timeline scroll is handled in the virtual-rows effect; the legacy branch
is queue/playlist only.

* fix(queue): address PR review nits for timeline play history

Use authoritative row.ref.serverId for history clicks before resolver fill;
simplify empty bootstrap seed; tighten completion types; unify recent_plays SQL.

* fix(queue): immutable session history append for useSyncExternalStore

Replace in-place push with a fresh array so getSnapshot returns a new
reference and React re-renders on live appends without a playerStore update.
2026-06-28 02:08:02 +00:00
Psychotoxical 604cdd54d6 feat(updater): show the WinGet update command in the Windows update modal (#1202)
* feat(updater): show WinGet update command in the Windows update modal

* docs(changelog): WinGet update command in the Windows update dialog
2026-06-27 15:56:54 +02:00
Psychotoxical 6d4d82d6a3 fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue (#1201)
* fix(playlist): stop the detail page reloading on Play/Shuffle/Enqueue

* docs(changelog): playlist play no longer reloads the page
2026-06-27 15:24:28 +02:00
Psychotoxical 7e6a2100e5 fix(updater): hold Windows update notice during WinGet moderation window (#1200)
* fix(updater): hold Windows update notice during WinGet moderation window

* docs(changelog): Windows update notice WinGet moderation delay
2026-06-27 14:50:37 +02:00
Psychotoxical ed02ba4e1d feat(titlebar): theme-colored window bar on macOS (#1199)
* feat(titlebar): theme-colored window bar on macOS

macOS showed the grey native title bar that ignores the app theme
(#1198). Enable titleBarStyle: Overlay so the webview reaches the top
edge and the native traffic lights float over the existing in-page
title bar, which already paints with the active theme.

Reuses the Linux title-bar grid: the bar is shown on macOS too (hidden
in native fullscreen), without the custom window buttons since the
traffic lights stay native. The GTK resize grips are now Linux-only.
Renamed the stylesheet to drop the misleading linux-only name.

* docs(changelog): macOS themed title bar

* fix(titlebar): crisp macOS bar text — drop transform layer and shadow

* fix(titlebar): drop now-playing text from the macOS bar
2026-06-27 14:17:55 +02:00
Psychotoxical 86ae462ad6 feat(hero): album-artist backdrop, configurable per-surface sources, and prefetch (#1193)
* refactor(cover): extract shared pickArtistBackdrop priority helper

* feat(hero): show the album artist's fanart as the mainstage hero backdrop

* feat(settings): configurable per-surface artist backdrop sources

Each artist-backdrop surface (mainstage hero, artist-detail header, fullscreen player) gets its own enable toggle + an ordered, individually-toggleable source list, configured under External Artwork Scraper on the Integrations tab (shown when the scraper is on). Reorder via the shared useDragSource/psy-drop drag infra plus keyboard-accessible up/down buttons; each source has its own on/off.

The shared chooser pickArtistBackdrop is generalised to resolveBackdrop + backdropFromConfig (ordered candidate list with the same pending/miss/centred-framing semantics), so all three surfaces resolve identically. themeStore persist bumped to v2; defaults reproduce today's order, so there is no visible change without user action.

Gating decoupled: the three surfaces are gated solely by their own per-surface flag, not by enableCoverArtBackground (which stays scoped to album/playlist-header cover blur). Reorder maths extracted to a pure, unit-tested module. i18n en + de (other locales TODO before PR).

Tests: resolver (10) + reorder (7).

* feat(cover): make the ensure queue surface-aware for artist backdrops

coverEnsureQueued now threads optional CoverEnsureOpts through to the Rust ensure and weaves the external surface into the in-flight key, so the fanart and banner surfaces of one artist no longer collide on one download chain. External surfaces also bypass the disk-src memory short-circuit (their {tier}-{surface}.webp never seeds those caches, and the canonical cover must not read as a hit). New thin ensureArtistBackdropQueued wrapper. Backward-compatible: plain covers append nothing to the key; the 5 queue tests stay green.

* fix(cover): reset the artist external-image hook synchronously on artist change

The hook reset src in an effect (one render late), so for the render between an artist change and that effect a consumer read the *previous* artist's resolved image. The mainstage hero then froze (and cached into per-album memory) a neighbouring slide's banner. Reset synchronously via the React adjust-state-on-prop-change pattern. Also removes brief stale-image flashes on the artist-detail header and fullscreen player.

* feat(hero): prefetch artist backdrops and show-ready-now / upgrade-on-re-entry

warmHomeMainstageCovers now prefetches each hero slide's artist backdrop (banner/fanart) at static slide-index priorities (idx1=high, idx0=low, rest=middle; no reprioritise on navigation), then predecodes every slide already on disk. useHeroBackdrop shows the best source ready at entry (Navidrome on a cold first visit, the prefetched/cached external one on re-entry) and freezes that source choice for the visit so nothing swaps mid-dwell; the url is derived live from the frozen choice, with a per-album disk memory for re-entry. HeroBg now crossfades only after the image bytes load (onLoad/complete gate + onError + fallback). Inert when the scraper is off. Tests: per-album memory (5).

* docs(changelog): configurable artist backdrops + mainstage hero (PR #1193)

CHANGELOG Added entry, settingsCredits line, and the What's New Artist-artwork highlight extended to cover the mainstage hero backdrop and per-place source config.

* docs(changelog): fold mainstage hero + per-place backdrops into the fanart entry (PR #1193)

Merge the configurable-backdrops changes into the existing 'Artist artwork from fanart.tv' block (now PR #1137 and #1193) instead of a separate entry.

* fix(hero): revert HeroBg load-gate that blanked the app (Maximum update depth)

The byte-load gate I added drove the crossfade reveal from an inline img ref + onLoad that re-fires on every render and scheduled a setTimeout each call; frequent re-renders (playback, marquee) stacked nested updates until React threw 'Maximum update depth exceeded' — and with no ErrorBoundary the whole window blanked. Reverted HeroBg to the proven timer-based crossfade. The hero only ever receives ready/predecoded urls, so the gate was cosmetic.

* fix(hero): gate the HeroBg crossfade on image load to stop the slide flicker

The bare 20 ms reveal faded a layer in before its bytes were ready (notably the Navidrome raw url), so a slide change flickered. Now an Image() preloader in the [url] effect reveals the layer on load (or a cached complete check), with a fallback. Everything is scheduled once per url — no per-render <img> ref/onLoad — so unlike the reverted gate it can't stack nested updates.

* feat(i18n): translate the backdrop-source settings into the remaining 10 locales

Adds the per-surface backdrop config strings (es, fr, nl, zh, nb, ru, ro, ja, hu, pl) and extends externalArtworkDesc to mention the mainstage hero where the key exists (ja has none → falls back to en).
2026-06-27 00:42:32 +02:00
Psychotoxical 014d57c53c feat(app): top-level error boundary so a render error no longer blanks the app (#1194)
* feat(app): add a top-level error boundary so a render error no longer blanks the app

Until now any thrown error during render took the whole window down with no recovery (issue #382). A class ErrorBoundary around the authenticated shell catches it and shows a recoverable fallback (Try again / Reload app) while playback keeps going. Hook-free + English-only with literal CSS var() fallbacks so it renders even when i18n/theme/state is what broke.

* docs(changelog): error boundary prevents the app blanking on a render error (PR #1194)
2026-06-26 20:51:42 +02:00
kilyabin afe5b377e0 feat(i18n): Russian locale — missing strings (#1181) 2026-06-25 21:22:45 +00:00
Hive Mind eb42a32315 feat(i18n): add Polish (pl) translation (#1185)
* Translated settings. Added polish option to other languages and in the fronted UI itself. Translated settings to polish

* Corrections to the settings. Translated help page

* Translated orbit. Made corrections to help and settings

* playlists translated

* deviceSync translated

* search translated

* Statistics translated

* musicNetwork translated

* common translated

* player translated

* queue translated

* albumDetails translated

* connection and random mix translated

* artistDefails translated

* albums translated

* smartPlaylists translated

* nowPlaying translated. smartPlaylists corrections

* sidebar translated

* contextMenu translated

* translated radio

* translated sharePaste

* favorites translated

* nowPlayingInfo translated

* translated home and login

* translated artists, genres, licences, migration, mostPlayed, songInfo and tracks

* translated changelog, composerDetail, composers, entityRating, folderBrowser, hero, losslessAlbums, luckyMix, miniPlayer, randomAlbums, randomLanding, tray and whatsNew

* chore(release): CHANGELOG and credits for Polish locale (PR #1185)

* chore(release): mention Polish in 1.49.0 What's New highlights

* settings updated with new strings. Deleted old unecessary ones

* translated missing translations

* Fixed typos

* translated missing strings

* Added missing translations that appeared in #1186 and #1189
2026-06-25 21:32:52 +03:00
cucadmuh cea814e993 fix(connection): strengthen sidebar recovery after reconnect (#1160) (#1190) 2026-06-25 19:52:40 +03:00
Psychotoxical 00512df207 feat(playlist): sort playlist tracks by date added (#1191)
* feat(sort-dropdown): support a right-aligned popover via align prop

* feat(playlist): sort playlist tracks by date added

* docs(changelog): playlist sort by date added (#1191)

* docs(credits): playlist sort by date added (#1191)
2026-06-25 18:29:35 +02:00
Psychotoxical d49424e95b feat(settings): add a Compact buttons appearance toggle (#1189)
* feat(ui): prototype compact hero and toolbar buttons (Large/Small appearance setting)

* feat(settings): rename action-button size toggle to "Compact buttons"

Promote the hero-button prototype to a real, app-wide setting.

- rename heroButtonSize → buttonSize, data-hero-buttons → data-button-size,
  hero-action-bar/hero-btn-label → compact-action-bar/compact-btn-label
- relabel "Hero buttons" → "Compact buttons" and broaden the description
  (action + toolbar buttons across detail pages and browse views) in all 11 locales
- move the feature CSS out of cover-lightbox.css into its own compact-buttons.css
- add tests: themeStore buttonSize toggle, SelectionToggleButton

* docs(changelog): compact buttons appearance toggle (#1189)

* docs(credits): compact buttons appearance toggle (#1189)
2026-06-25 14:49:09 +02:00
Psychotoxical 8b89596fcf feat(album): show all genres in album details, linkable to genre pages (#1186)
* feat(album): show all genres in album details, linkable to genre pages

The album header listed only a single genre. It now renders every genre
from the album's OpenSubsonic genres[] (falling back to splitting the
legacy genre string), each linking to its genre page via the existing
genre route.

* docs(changelog): multiple genres in album details (#1186)

* refactor(genres): extract genreColor into a shared util

* feat(album): union album and track genres behind a +N cursor menu

* feat(album): read album detail from the library index when ready

* docs(changelog): expand the album genres entry

* feat(album): genre pill row with keyboard-navigable popover

* test(offline): cover index-first resolveAlbum path with mocked libraryIsReady

* refactor(styles): move album-detail genre styles out of cover-lightbox.css
2026-06-25 13:16:24 +02:00
cucadmuh a88d5f3181 fix(autodj): last-track tail and queue-end idle pull rewind (#1183) 2026-06-25 00:31:12 +03:00
cucadmuh 4e8f16b8ae docs: refresh WHATS_NEW and CHANGELOG for 1.49.0 (#1180) 2026-06-25 00:04:16 +03:00
cucadmuh ec98fcc4ff fix(genres): hide empty genres after library resync (#1176) 2026-06-24 23:07:22 +03:00
Psychotoxical 1031590742 fix(settings): align AutoDJ and Hi-Res sub-options with the Normalization design (#1175)
* fix(settings): box the AutoDJ and Hi-Res sub-options like Normalization

The AutoDJ overlap-cap and the Hi-Res blend-rate options sat bare in their
sections instead of in the bordered sub-card the Normalization block uses for
its per-mode controls. Wrap both (and the crossfade-seconds slider, for
consistency within Track transitions) in the shared settings-norm-block card,
and drop the redundant SettingsGroup wrapper from the Hi-Res block — the Audio
tab already wraps that section in a group, so the block added a second border.

* docs(changelog): audio sub-section design fix; move recent entries to the end of Fixed

The #1172 and #1174 entries were prepended to the Fixed section instead of
appended; move them to the end where new entries belong, and add the #1175
audio settings entry.
2026-06-24 21:53:04 +02:00
Psychotoxical 9bbe69e7e7 fix: context menu Play Now playback and resize behaviour (#1174)
* fix(playlist): play the playlist from the context menu Play Now action

The Playlists-page context menu 'Play Now' only navigated to the playlist
detail page instead of starting playback. It now loads the playlist's songs
and plays them via the shared playPlaylistAll action (same path as the detail
page 'Play All' button), with a guard against load failures.

* fix(ui): close the context menu on window resize

The context menu is absolutely positioned at fixed coordinates, so resizing
the window left it stranded and drifting off-screen. Whether a resize closed
it was inconsistent across setups (it stayed open on some Windows and Linux
environments). Always close it on resize so the behaviour is the same
everywhere.

* docs(changelog): context menu Play Now and resize fixes (#1174)

* docs(changelog): credit the reporter for the context menu fixes
2026-06-24 21:19:04 +02:00
cucadmuh 58e6efc68c feat(autodj): configurable overlap cap (auto or 2–30 s) (#1173) 2026-06-24 22:00:35 +03:00
Psychotoxical c3e6be537c fix(artist): show fanart in the artist-detail hero, not the Navidrome cover (#1172)
* fix(cover): report a genuine miss for external artist surfaces

When external artwork is enabled, the fanart/banner ensure fell through to
the Navidrome cover and returned it as a hit on a fanart.tv miss. That made
'this artist has no banner' indistinguishable from a real hit, so the
artist-detail hero short-circuited on the ND cover and never reached the
fanart it actually had. Return hit=false on an external-surface miss instead;
the Navidrome fallback is the caller's job, since each surface has its own
chain (hero: banner->fanart->ND, fullscreen player: fanart->ND).

* fix(artist): hero background banner->fanart->Navidrome with raised focal point

The hero header background now steps through banner -> fanart -> Navidrome
artist cover, resolving the ND cover the same way the fullscreen player does.
Previously it had no Navidrome stage at all and, combined with the backend
masking a banner miss as a hit, showed the ND cover where the fanart belonged.

Also raise the focal point (background-position: center 30%) for the portrait-ish
fanart/ND images so the band's heads stay in frame on wide (2K+) viewports,
where 'cover' scales them up and overflows vertically. The wide banner strip
keeps the shared center. Done via a scoped inline style; .album-detail-bg stays
untouched for the album/playlist headers that share it.

* docs(changelog): artist header external-background fallback fix (#1172)
2026-06-24 20:41:16 +02:00
cucadmuh 09f24beb32 feat(audio): hi-res transition blend rate for crossfade, AutoDJ, and gapless (#1171) 2026-06-24 21:04:42 +03:00
cucadmuh de36f79e46 chore(ci): ESLint workflow and path-aware ci-ok merge gate (#1170) 2026-06-24 18:42:59 +03:00
Psychotoxical 53a4bf9330 fix(macos): pad dock icon to Apple icon grid (#1169)
* fix(macos): pad dock icon to Apple icon grid

The macOS .icns artwork filled the canvas edge-to-edge, so the dock icon
rendered larger than native apps. Replace icon.icns with a build whose artwork
is scaled to an ~824px body centred on a 1024px transparent canvas (Apple's
icon grid). macOS-only asset; Windows .ico and Linux PNGs unchanged. Refs #1166.

* fix(macos): skip the icns app-icon in dev to avoid a launch crash

Tauri's dev-only macOS path sets the app icon from icon.icns via
NSImage::initWithData(...).expect(...) (RunEvent::Ready); the padded icns
makes that return nil and abort at launch — release builds are unaffected.
Add a dev config override (tauri.dev.conf.json) that drops icon.icns from
bundle.icon so the dev app icon falls back to a PNG; tauri:dev passes it via
--config. Production bundling keeps the padded icon.icns. Refs #1166.

* docs(changelog): add entry for macOS dock icon padding (PR #1169)
2026-06-24 16:00:15 +02:00
Psychotoxical 7c724a642f chore(eslint): add ESLint toolchain and clean src to strict 0/0 (#1165)
* chore(eslint): add eslint toolchain and configs

* fix(eslint): resolve gradual-config errors (rules-of-hooks, no-empty, prefer-const, …)

* chore(eslint): clear unused vars in config, contexts, app and test

* chore(eslint): clear unused vars in api and music-network

* chore(eslint): clear unused vars in utils

* chore(eslint): clear unused vars in store

* chore(eslint): clear unused vars in cover and hooks

* chore(eslint): clear unused vars in components

* chore(eslint): clear unused vars in pages

* chore(eslint): remove explicit any in src

* chore(eslint): align react-hooks exhaustive-deps

* chore(eslint): zero gradual config on src

* chore(eslint): strict hook rules in store and utils

* chore(eslint): strict hook rules in hooks and cover

* chore(eslint): strict hook rules in components

* chore(eslint): strict hook rules in pages and contexts

* chore(eslint): document scripts ignore in eslint config

* chore(eslint): add lint script and zero strict findings

* chore(eslint): address review round 1 (gradual 0/0, per-site disable reasons)

* chore(eslint): tighten two set-state disable comments (review round 2 LOW)

* chore(nix): sync npmDepsHash with package-lock.json

* docs(changelog): add Under the Hood entry for ESLint setup (PR #1165)

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-24 01:33:34 +02:00
Psychotoxical c7d76af790 feat(themes): add follow-system mode to the theme scheduler (#1163)
* feat(themes): add follow-system mode to the theme scheduler

The theme scheduler can now switch the day/night theme pair by the OS
light/dark preference instead of a clock schedule. A segmented control
picks the trigger; in system mode the time inputs are hidden and the two
theme pickers read as Light/Dark.

The OS theme is read via the native Tauri window theme API (theme() +
onThemeChanged) rather than the Web prefers-color-scheme media query,
which is unreliable through WebKitGTK on Linux. Live updates land
instantly where the platform forwards them; on Linux setups that don't,
a hint notes the change applies after an app restart.

* docs(changelog): add follow-system theme mode entry (#1163)

CHANGELOG Added entry, contributor credit and What's New highlight for the
theme scheduler's new system-theme mode.
2026-06-23 12:13:26 +02:00
Psychotoxical 78a177b1bc fix(orbit): outbox lost-update recovery and session hardening (#1159)
* fix(orbit): harden the guest outbox — recover lost suggestions, avoid a duplicate outbox

Two outbox robustness issues on the poll-based, non-atomic playlist
transport:

- Lost-update: a track a guest appends to its outbox can be wiped by a
  concurrent host sweep-clear before the host recorded it, leaving the
  suggestion lost AND stuck on "waiting on host" forever (it only clears
  once it reaches the host's play queue). The playlist read-modify-write
  can't be made atomic, so recover: re-send a pending suggestion the host
  hasn't recorded (absent from state.queue) past a grace window — the host
  dedupes by (user, trackId), so it's idempotent — and give up + toast past
  a 45s window so the row stops hanging. New pendingResend planner module
  with unit tests; suggest + tick share ensureTrackInOutbox.

- Duplicate outbox: a transient getPlaylists failure at join was swallowed
  to [], so the existing-outbox check missed and a second outbox was
  created. Distinguish "lookup failed" from "genuinely absent" and retry
  once before falling back to create.

New i18n key for the give-up toast across all locales.

* fix(orbit): reject non-http(s) schemes in share invites

normalizeShareServerUrl only prepends http:// when the server string doesn't
already start with "http", so an invite carrying a scheme like httpx:// or
https-phish:// slips through unchanged. Reject anything whose parsed protocol
isn't http(s) at parse time, before the known-server match. Add round-trip
tests covering the accepted and rejected schemes.

* fix(orbit): read exit-modal role fresh to avoid a stale keydown closure

The exit modal's Enter/Escape handler binds once per open (deps [isOpen]) and
closed over the role prop, so a role change while the modal was up would act
on a stale value. Read role from the store inside the action instead, and drop
the now-unused reactive subscription.

* docs(changelog): consolidate Orbit fixes into one block, add 1159
2026-06-22 17:32:53 +02:00
Psychotoxical 9b03949f34 feat(orbit): sync the host's track-transition settings to guests (#1158)
* feat(orbit): mirror host track-transition settings into the session

Each client previously used its own local crossfade / gapless / AutoDJ
settings, so guests blended tracks differently from the host and drifted
out of sync — the exact drift the Catch-Up button exists to paper over.

Add an optional OrbitSettings.transitions (crossfade enabled/secs/trim,
AutoDJ smooth-skip, gapless), additive on the wire so older sessions
simply omit it. The host seeds its own prefs at start and refreshes them
every tick, so a mid-session change propagates. Guests snapshot their own
prefs on join, adopt the host's for the session (idempotent — no setState
churn / audio-sync re-fire when unchanged), and restore their own on
leave. Applying via authStore.setState reuses the existing authSyncListener
path to the Rust engine, so no new IPC. Without the Hot cache a guest's
AutoDJ blend degrades gracefully via the existing byte-preload fallback.

Add a bridge module with unit tests for read/apply/save/restore.

* feat(orbit): lock guest transition controls during a session

Since a guest now mirrors the host's track-transition settings (re-applied
every read tick), their own controls would visibly fight the sync. Disable
them while a guest is in a live session and explain why.

Settings -> Audio -> Track transitions greys out the mode picker, seconds
slider and smooth-skip toggle with a "controlled by the Orbit host" note;
the queue toolbar's Gapless / Crossfade / AutoDJ quick-toggles get the same
disabled state and tooltip. Host controls are untouched. New i18n key added
across all locales.

* docs(changelog): add Orbit transition sync (1158)
2026-06-22 16:44:20 +02:00
Psychotoxical a4fbd45d5d fix(orbit): host-side session guards — settings default, maxUsers, push serialisation (#1157)
* fix(orbit): unify session-settings default to the canonical preset

updateOrbitSettings merged patches onto a hand-rolled
{ autoApprove: true, autoShuffle: true } fallback when a legacy session's
blob predated the settings field. That literal disagreed with
ORBIT_DEFAULT_SETTINGS (autoApprove: false), so toggling autoShuffle on
such a session silently flipped autoApprove on, landing guest suggestions
without host approval. Use ORBIT_DEFAULT_SETTINGS as the single source of
truth for the fallback. Add a behavioural test for the settings-less,
preserve-existing, and not-hosting paths.

* fix(orbit): enforce maxUsers host-side in the participant rebuild

The join gate only sees a one-tick-old state blob, so two guests can both
pass the participants.length < maxUsers check and join past the cap at
once; maxUsers was advisory. The host is the single writer of the
canonical state, so enforce the cap there: admit at most maxUsers guests
in the participant rebuild, earliest joiners winning (an established
participant is never displaced by a newcomer) with a deterministic
username tie-break for same-tick joins. Suggestions from an over-cap guest
are ignored, consistent with their absence from the participant list.
Reorders the fold so the admitted set gates the queue-additions loop. Add
tests for the cap, the no-displacement rule, and the ignored over-cap
suggestion.

* fix(orbit): serialise host state pushes to stop overlapping ticks dropping writes

pushState does several awaited round-trips (sweep -> resolve -> write) and
is fired from three sources: the mount, the 2.5 s timer, and a play/pause
subscription. With no in-flight lock these can overlap and race
last-writer-wins: a slow run that already swept and cleared an outbox can
lose its write to a faster run, dropping those suggestions from both the
outbox and the published state. The lastPushedAtRef was only ever written,
never used as a guard.

Add makeCoalescedRunner: a pure helper that serialises an async task and
coalesces any mid-flight request into a single rerun, so no trigger is
lost. The host hook wraps pushState in it; the dead ref is removed. Add a
unit test for the serialise + coalesce semantics.

* docs(changelog): add Orbit host-side session guards (1157)
2026-06-22 15:25:39 +02:00
cucadmuh 15cecb5d7d feat(server): custom HTTP headers for reverse-proxy gates (#1095) (#1156) 2026-06-22 16:25:28 +03:00
Psychotoxical 2c9b2eeb46 fix(orbit): cleanup regex, state-blob budget, radio top-up lockout (#1155)
* fix(orbit): stop cleanup sweep from deleting live sessions on other devices

The orphan-cleanup regex anchored the trailing `__` inside the optional
`_from_…` group, so the canonical session playlist name
(`__psyorbit_<sid>__`) never matched. It then fell into the
"unrecognised → prune" branch, which deletes unconditionally and bypasses
the orphan-TTL grace that exists precisely to protect a session running on
the user's other device. Opening Psysonic on a second device wiped the
first device's live session out from under its guests.

Move the trailing `__` outside the optional group so both the session and
outbox names match; the existing sid/TTL/ended logic then applies as
intended. Add a behavioural test covering keep-fresh / prune-stale /
prune-ended / skip-current / outbox / corrupt / foreign-owner cases.

* fix(orbit): keep host publishing when the state blob grows past budget

OrbitState.queue (the suggestion/attribution history) was append-only at
the sweep-fold step and never bounded. On a long session it grew until the
serialised blob exceeded ORBIT_STATE_MAX_BYTES; serialiseOrbitState then
threw, writeOrbitState's caller swallowed the error and retried the same
over-budget state every tick, so the host silently stopped publishing and
guests froze into a host-timeout.

Two layers:
- Cap queue at ORBIT_QUEUE_HISTORY_LIMIT in applyOutboxSnapshotsToState,
  evicting oldest-by-addedAt (robust to the periodic queue shuffle). The
  dropped tracks have long since played, so their attribution/dedupe entry
  is dead weight.
- Add serialiseOrbitStateForWire: a budget-aware serialiser that sheds
  oldest history, then the play-queue tail, on a copy before giving up.
  writeOrbitState now uses it, so a transient over-budget tick degrades the
  published blob instead of taking the host offline. Local store state is
  untouched.

Tested: history cap eviction order, wire-trim byte budget + oldest-first
drop, play-queue fallback, and within-budget passthrough.

* fix(orbit): lock out the proactive radio top-up during a session

The ≤2-remaining radio top-up in runNext lacked the isInOrbitSession()
guard that its infinite-queue sibling has at both entry and resolution.
A guest who joined with residual radioAdded tracks (before the first
syncToHost replaces the queue) could fire it, appending up to 10 unrelated
radio tracks and trimming queue history — drifting the guest off the host's
playlist.

Add the guard at entry and re-check inside the resolution .then(), mirroring
the infinite-queue branch; the existing finally() still clears the fetching
flag. The queue-exhausted radio fallback already returns early in Orbit, so
only this mid-queue path was unguarded.

* docs(changelog): add Orbit cleanup / state-budget / radio fixes (#1155)

* docs(changelog): move Orbit fixes to bottom of Fixed section (#1155)
2026-06-22 13:33:09 +02:00
Psychotoxical bd43ea5240 fix(playlists): wrap header action buttons instead of overflowing (#1153)
* fix(playlists): wrap header action buttons instead of overflowing

The Playlists header action row was a fixed flex row, so at narrow
widths (e.g. with the queue panel open) the buttons overflowed and
were clipped off-screen. Add flex-wrap so they wrap, left-aligned,
matching the Albums toolbar behaviour.

* docs(changelog): add 1.49.0 entry for Playlists header button wrap (#1153)
2026-06-22 03:05:38 +02:00
cucadmuh 9323f5fee6 fix(albums): All Albums compilation and favorites filters (#1143) (#1151) 2026-06-22 02:54:41 +03:00
Psychotoxical 93166c774a feat(i18n): add Hungarian (hu) translation (#1149)
* feat(i18n): add Hungarian (hu) translation files

* feat(i18n): register Hungarian locale and language option

* docs(changelog): Hungarian translation entry and credit (#1149)

* fix(i18n): list Romanian in the Login language picker
2026-06-22 00:56:37 +02:00
Psychotoxical ec8cfbc1c8 fix(eq): show the active AutoEQ profile name in the preset picker (#1147)
* fix(eq): surface the active AutoEQ profile name in the preset picker

* i18n(settings): add AutoEQ preset group label (10 locales)

* docs(changelog): AutoEQ active-profile picker fix (#1147)
2026-06-21 22:58:33 +02:00
Psychotoxical 887c940e1b feat(eq): remember EQ profile per audio output device (#1146)
* feat(eq): add per-device EQ snapshot state to eqStore

* feat(eq): wire per-device EQ sync into audio listener setup

* feat(settings): add 'remember EQ per device' toggle to audio output section

* i18n(settings): add 'remember EQ per device' strings (10 locales)

* test(eq): cover per-device EQ snapshot store + device sync orchestration

* docs(changelog): per-device EQ entry, credit and what's new (#1146)

* i18n(settings): note per-device EQ scope (system default + explicit switch) in toggle description

* test(eq): cover system-default bucket and device-reset (null) path
2026-06-21 22:50:06 +02:00
cucadmuh d9969ed76f fix(library): Navidrome artist letter buckets + safer index open/swap (#1144) (#1145) 2026-06-21 20:55:35 +03:00
cucadmuh 986550c854 fix(favorites): bulk add-to-playlist and play/enqueue selected (#1140) 2026-06-21 14:29:05 +03:00
Psychotoxical 30ccaf51a8 fix(updater): rebuild the update modal on a reusable Modal (#1142)
* feat(ui): add reusable Modal component

Portal + dimmed backdrop (no backdrop-filter — on WebKitGTK the blur bleeds
onto modal content on some GPU stacks), Escape/backdrop/X to close, header
(icon/title/subtitle) + scrollable body + footer slot. First consumer is the
update modal; the other hand-rolled modals migrate onto it over time.

* fix(updater): rebuild the update modal on the reusable Modal

Fixes a user report (Manjaro / AMD / X11): blurry modal text (the eq-popup
backdrop-filter bled onto the content), the version arrow sitting between the
two header rows, and the unclear Skip / Remind buttons. Now uses the no-blur
Modal shell; the header icon aligns with the title; Skip is a clear button and
Remind me later is the accent action when there is no in-app install. Drops the
now-dead update-modal shell CSS.

* docs(changelog): update notification popup fix (PR #1142)
2026-06-21 00:48:26 +02:00