Commit Graph

14 Commits

Author SHA1 Message Date
cucadmuh ea6ac49885 feat(offline): Offline Library shows cached albums from all servers (#719)
* feat(offline): show cached albums from all servers in Offline Library

List every offline album regardless of active server; load cover art per
source server and switch before play/enqueue. Sidebar, mobile nav, and
disconnect auto-nav use any cached content; multi-server cards show a label.

* docs(changelog): link offline library PR #719

* chore(pr-719): address review — CHANGELOG in Added, helper tests

Move release note to ## Added per team changelog policy; cover
offlineAlbumCoverArt and ensureServerForOfflineAlbum in unit tests.
2026-05-15 17:20:19 +03:00
Frank Stellmacher 651a3f276a feat(settings): global Advanced Mode toggle + playlist page layout (#556) (#720)
Adds a per-element visibility toggle for the playlist detail page (Add
Songs, Import CSV, Download ZIP, Cache Offline, Suggestions) and reworks
the way uncommon options are surfaced: instead of a per-tab collapsible
group, a global "Advanced" toggle in the Settings header reveals all
`advanced` sub-sections across every tab and marks each one with a small
badge. Sets the pattern up so any future advanced option lives in its
natural tab, gated by the same switch.

- New `advancedSettingsEnabled` boolean on `authStore`
  (UiAppearance slice, persisted with the rest of the store).
- `SettingsSubSection` gains an `advanced?: boolean` prop. Hidden when
  the toggle is off; renders an "Advanced" pill in the header when on.
- Settings header gets a Toggle-Switch next to the search lupe.
- `PersonalisationTab` flattens — Sidebar + Home stay always visible;
  Artist sections, Queue Toolbar, and the new Playlist layout get
  `advanced` and disappear by default. `PersonalisationAdvancedGroup`
  component + CSS removed.
- New `playlistLayoutStore` (Zustand + persist, items[{id,visible}] +
  rehydrate sanitize) following the queueToolbarStore pattern.
- `PlaylistHero` and `PlaylistSuggestions` gate the four toolbar buttons
  and the suggestions rail on the store directly.
- One-time migration in MainApp on mount: if the user had opened the
  old per-tab Advanced group (`psysonic_personalisation_advanced_open
  === 'true'`) OR already customised any of the three sub-sections,
  Advanced Mode auto-enables on first launch. Idempotent via a
  localStorage flag; legacy key removed afterwards.
- New i18n keys `settings.advancedMode`, `settings.advancedModeTooltip`,
  `settings.advancedBadge`, `settings.playlistLayout*` in all 9 locales.

Reuses kveld9's design from PR #556; not merged because the locale split
landed afterwards. Credited under the existing kveld9 entry in
settingsCredits.ts.

Co-authored-by: Kveld. <kveld912@proton.me>
2026-05-15 15:55:11 +02:00
cucadmuh 45e0e1206f fix(playback): pin queue playback to source server when browsing another library (#717)
* fix(playback): pin queue streams, cover art, and library links to queue server

When the active server changes while a queue from another server is playing,
keep streams and UI on queueServerId; switch back for artist/album links and
queue or player-bar context menus.

* fix(playback): switch to queue server when opening Now Playing

Ensure active server matches queueServerId before Subsonic fetches on the
Now Playing page, mobile player route, and queue info panel; scope caches
by server id.

* docs(credits): mention Now Playing in PR #717 contribution line

* fix(playback): route scrobble and queue sync to queue server

Address PR review: apiForServer for scrobble/now-playing/savePlayQueue,
clear queueServerId on server removal, mini-player queueServerId sync,
block cross-server enqueue with toast, and regression tests.
2026-05-15 16:08:41 +03:00
Frank Stellmacher 7a7a9f5e6b refactor(utils): group utils/ files into topic folders (Phase L, part 1) (#689)
111 of 122 top-level src/utils/ files move into 16 topic folders (audio,
cache, cover, share, server, playback, playlist, deviceSync, waveform,
mix, format, export, changelog, ui, perf, componentHelpers). True
singletons with no cluster stay at the utils/ root.

Pure file-move: a path-aware codemod rewrote 539 relative-import
specifiers across 275 files; no logic touched. The hot-path coverage
gate list (.github/frontend-hot-path-files.txt) is updated to the new
paths for the 11 gated utils files — a mechanical consequence of the
move, not a CI change. tsc is green.
2026-05-14 14:27:44 +02:00
Frank Stellmacher 1dd74f0fa1 refactor(tauri-bridge): split TauriEventBridge into per-concern hooks (#682)
Each Rust<->React bridge concern moves into its own hook under
hooks/tauriBridge/; TauriEventBridge just composes them:

- useZipDownloadBridge        download:zip:progress
- usePreviewBridge            audio:preview-* lifecycle
- useAudioDeviceBridge        audio:device-changed / -reset
- useCliBridge                full cli:* listener surface
- useTrayIconSync             tray-icon visibility
- useInAppKeybindings         configurable keydown chords
- useMediaAndWindowBridge     media keys, tray, shortcuts, close/force-quit
- usePlayerSnapshotPublisher  psysonic --info JSON snapshot

Pure code-move — event names, payloads, effect deps and cleanup all
verbatim. The MainApp call site is unchanged.
2026-05-14 12:31:14 +02:00
Frank Stellmacher b591a1cb5f refactor(app-shell): I.1 — split AppShell.tsx 691 → 248 LOC across 12 files (#671)
* refactor(app-shell): extract appShellHelpers.ts

Move SIDEBAR_COLLAPSED_STORAGE_KEY + read/persist helpers and the
shouldSuppressQueueResizerMouseDown geometry helper out of AppShell.tsx
into utils/appShellHelpers.ts.

AppShell.tsx: 691 → 639 LOC.

* refactor(app-shell): extract usePlatformShellSetup hook

Bundle the one-shot platform/window-shell effects (tiling-WM detection,
no-compositing class, data-platform attr, custom titlebar sync, kinetic
scroll toggle, logging mode push) into hooks/usePlatformShellSetup.ts.
Returns isTilingWm so AppShell can still gate the custom titlebar.

AppShell.tsx: 639 → 604 LOC.

* refactor(app-shell): extract 5 lifecycle hooks

Pull these effect islands into hooks/:
- useOrbitBodyAttrs       — orbit role/phase → <html data-orbit-*> attrs
- useWindowFullscreenState — Tauri isFullscreen() tracker
- useNowPlayingTrayTitle  — title + tray tooltip sync
- useTrayMenuI18n         — tray menu labels via i18n
- useServerCapabilitiesProbe — music folders + rating support + orbit
                              orphan sweep on login

AppShell.tsx: 604 → 497 LOC.

* refactor(app-shell): extract useQueueResizer hook

Bundle queueWidth state, drag listeners, sidebar-aligned handle position,
and the click-vs-drag mousedown handler into hooks/useQueueResizer.ts.
AppShell drops shouldSuppressQueueResizerMouseDown wiring and 4 local
state pieces.

AppShell.tsx: 497 → 403 LOC.

* refactor(app-shell): extract 4 misc lifecycle hooks

- useGlobalDndAndSelectionBlockers — document-level DnD/select-all/
  selectstart blockers (Linux/WebKitGTK + Wayland workarounds).
- useAppActivityTracking — <html data-app-hidden> + <html data-app-blurred>
  so CSS can pause cosmetic animation when the app isn't being looked at.
- useMainScrollingIndicator — scroll-idle tracker for main + np viewports.
- useOfflineAutoNav — connStatus transitions push to /offline or back.

AppShell.tsx: 403 → 282 LOC.

* refactor(app-shell): extract AppShellQueueResizerSeam subcomponent

The 6px resizer strip and the round resize/toggle handle (~50 LOC of JSX
with embedded scrollbar-collision suppression + self-heal logic) move
into components/AppShellQueueResizerSeam.tsx. Desktop-only.

AppShell.tsx: 282 → 248 LOC.
2026-05-13 23:48:03 +02:00
Frank Stellmacher 9606a99efb refactor(api): F.50 — extract 7 small domain modules (#615)
Seven domain-eng splits peel ~200 LOC of read endpoints out of
`api/subsonic.ts`:

- `subsonicStreamUrl.ts` — `buildStreamUrl`, `coverArtCacheKey`,
  `buildCoverArtUrl`, `buildDownloadUrl` (token-signed URL builders
  for the four /rest endpoints we hand to the browser).
- `subsonicStarRating.ts` — `getStarred`, `star`, `unstar`,
  `setRating`, `probeEntityRatingSupport`. `setRating` still triggers
  the lazy `navidromeBrowse` cache invalidation; the same-folder
  lazy import path is preserved.
- `subsonicSearch.ts` — `search`, `searchSongsPaged`.
- `subsonicScrobble.ts` — `scrobbleSong`, `reportNowPlaying`,
  `getNowPlaying`.
- `subsonicAlbumInfo.ts` — `getAlbumInfo2`.
- `subsonicLyrics.ts` — `getLyricsBySongId`.
- `subsonicGenres.ts` — `getGenres`, `getAlbumsByGenre`.

63 external call sites migrated to direct imports. Four `vi.mock`
targets in the store-level tests pointed at `../api/subsonic` and
were updated to the new module paths.

Pure code-move. subsonic.ts: 762 → 561 LOC (−201).
2026-05-13 00:46:13 +02:00
Frank Stellmacher 006635de4b refactor(api): F.49 — extract library + artists + ratings (#614)
Three domain-eng modules peel ~316 LOC of fetch/mapping out of
`api/subsonic.ts`:

- `subsonicLibrary.ts` — browse + random + per-song fetch
  (`getMusicDirectory`, `getMusicIndexes`, `getMusicFolders`,
  `getRandomAlbums`, `getAlbumList`, `getRandomSongs`,
  `getRandomSongsFiltered`, `getSong`, `getAlbum`,
  `filterSongsToActiveLibrary`, `similarSongsRequestCount`, plus
  the private `albumIdsInActiveLibraryScope` cache).
- `subsonicArtists.ts` — artist endpoints (`getArtists`, `getArtist`,
  `getArtistInfo`, `getTopSongs`, `getSimilarSongs2`,
  `getSimilarSongs`). Uses Library's `filterSongsToActiveLibrary` and
  `similarSongsRequestCount` for the per-library scoping fallback.
- `subsonicRatings.ts` — `parseSubsonicEntityStarRating` parser plus
  `prefetchArtistUserRatings` and `prefetchAlbumUserRatings` workers
  with the shared 7-min cache. Calls back into Library/Artists for
  the per-id fetch.

51 external call sites migrated to direct imports. No re-export
shims in `subsonic.ts`. Statistics endpoints still in `subsonic.ts`
keep their `RATING_CACHE_TTL` constant locally (same 7-min window).

subsonic.ts: 1078 → 762 LOC (−316).
2026-05-13 00:23:53 +02:00
Frank Stellmacher 9fac6eb490 refactor(player): E.42 — migrate playerStore re-exports to direct imports (#607)
Migrates ~74 call sites away from the playerStore re-export shims that
were kept during M0–E.41 to avoid touching 30+ imports per PR. Now
that the bigger refactor work is done, each helper goes back to its
real home:

- `initAudioListeners`, `installQueueUndoHotkey`, `flushPlayQueuePosition`
  → from their own store modules
- `getPlaybackProgressSnapshot`, `subscribePlaybackProgress`,
  `PlaybackProgressSnapshot` → from `playbackProgress`
- `resolveReplayGainDb`, `shuffleArray`, `songToTrack`
  → from `utils/*`
- `_resetQueueUndoStacksForTest`, `consumePendingQueueListScrollTop`,
  `registerQueueListScrollTopReader` → from `queueUndo`
- `PlayerState`, `Track` types → from `playerStoreTypes`

Drops the corresponding 13 re-export stubs from `playerStore.ts` and
the now-unused imports. Also drops dead section banners + per-wrapper
comments above one-line action delegates. Trims one stale "(separate
PR)" note in `transportLightActions.ts` since that follow-up landed
in E.39.

`playerStore.ts`: 180 → 112 LOC (−68). Down from Phase E's starting
3732 LOC.

`bootstrap.test.ts` mock target updated from `../store/playerStore`
to `../store/queueUndoHotkey` to keep the spy reachable after the
import change.
2026-05-12 22:46:13 +02:00
Frank Stellmacher c0f2bc00dd refactor(app): Phase D — move TauriEventBridge into src/app/ (#561)
Final piece of the App.tsx slim-down. The `TauriEventBridge` component
— ZIP download progress, track-preview lifecycle, audio device
changed/reset, the full `cli:*` listener surface (audio-device-set,
instant-mix, library list/set, server list/set, search, player-command),
tray-icon visibility sync, in-app keybindings keydown handler, media
keys + tray actions, `shortcut:global-action` / `shortcut:run-action`,
seek-relative / seek-absolute / set-volume, the window:close-requested
+ app:force-quit flow (with the shared `performExit` Orbit teardown),
and the `psysonic --info` snapshot publisher — moves into
`src/app/TauriEventBridge.tsx`.

`MainApp` imports it from the new file. `App.tsx` is now 57 LOC: just
the `App()` default-export that branches between `MiniPlayerApp` and
`MainApp` after wiring the shared theme / font / track-preview
document attributes.

A.K.A. App.tsx 1453 → 57 LOC over Phase 2 (M0 + B.1 + B.2 + C.1 + C.2
+ D). Pure code move at every step — no behaviour change.

Pre-PR check: PASS (frontend tests, tsc, coverage gates, prod build,
backend tests, clippy, backend coverage gates).
2026-05-12 10:41:28 +02:00
Frank Stellmacher 796c7567ea refactor(app): Phase C.2 — move AppShell into src/app/AppShell.tsx (#560)
Companion to C.1. The full `AppShell` component — the persistent
sidebar / header / route host / queue-resizer / player-bar layout plus
its ~25 effects (tray-tooltip + title sync, Orbit role/phase body marker,
platform attribute, fullscreen tracking, music-folders + rating-support
probe, sidebar persistence, queue drag, WebKitGTK DnD/select-all
blockers, blur/hidden cosmetic-animation pause) — moves into
`src/app/AppShell.tsx` together with its three private helpers
(`readInitialSidebarCollapsed`, `persistSidebarCollapsed`,
`shouldSuppressQueueResizerMouseDown`). `MainApp` now imports `AppShell`
from the new file instead of the App.tsx re-export.

`App.tsx` 1232 → 560 LOC. What's left is the `TauriEventBridge` (~475
LOC, Phase D) plus the ~50-LOC `App()` default-export that splits
between `MiniPlayerApp` and `MainApp`. Imports that were AppShell-only
(Sidebar / PlayerBar / 9 components / 7 hooks / 3 platform helpers /
useOfflineStore / useConnectionStatus / useEqStore / usePerfProbeFlags /
useTranslation / useIsMobile / probeEntityRatingSupport / Suspense /
useCallback / useRef / useState / useLocation / getCurrentWindow's UI
use / ConnectionIndicator / LastfmIndicator / AppUpdater / TitleBar /
OrbitSessionBar / OrbitStartTrigger / useOrbitHost / useOrbitGuest /
cleanupOrphanedOrbitPlaylists / IS_MACOS / IS_WINDOWS / IS_LINUX /
APP_MAIN_SCROLL_VIEWPORT_ID / AppRoutes / lucide icons) all leave with
the component.

No behaviour change — pure code move + import-graph shuffle. Tests
unchanged; the existing AppShell behaviour is already covered indirectly
by the per-component tests it composes.

Pre-PR check: PASS (frontend tests, tsc, coverage gates, prod build,
backend tests, clippy, backend coverage gates).
2026-05-12 10:29:22 +02:00
Frank Stellmacher 2b1ad1542a refactor(app): Phase C.1 — extract AppRoutes + RequireAuth from App.tsx (#559)
Continues the App.tsx slim-down. Two pieces move out of the monolith:

- `src/app/AppRoutes.tsx` — the route table and its 32 lazy page imports.
  AppShell now renders `<AppRoutes />` inside the existing `<Suspense>`;
  the `perfFlags.disableMainRouteContentMount` placeholder stays in
  AppShell because that branch is a layout concern, not a routing one.
  `useIsMobile()` moves inside AppRoutes so the `/now-playing` mobile
  swap stays self-contained.

- `src/app/RequireAuth.tsx` — the 4-line auth guard, with a focused test
  that covers all three reject paths (no login, no active server id,
  empty server list) plus the happy path. MainApp imports it from the
  new file instead of routing through the App.tsx re-export.

Side-cleanup of imports that B.2 had already orphaned in App.tsx
(`version`, `initAudioListeners`, `lazy`, `Routes`, `Route`, `Navigate`,
`MobilePlayerView`).

`App.tsx` 1308 → 1232 LOC. `AppShell` stays in App.tsx for Phase C.2.

Pre-PR check: PASS (frontend tests, tsc, coverage gates, prod build,
backend tests, clippy, backend coverage gates).
2026-05-12 10:13:27 +02:00
Frank Stellmacher f09da2d2a3 refactor(app): Phase B.2 — split App() into MiniPlayerApp + MainApp (#557)
The 186-LOC default export shrinks to a thin window-kind switch with
shared document-attribute hooks. The mini-player tree and the main-app
tree each move into their own module under src/app/.

  - src/app/MiniPlayerApp.tsx (48 LOC):
      DragDropProvider + MiniPlayer + cross-window storage sync
  - src/app/MainApp.tsx (129 LOC):
      BrowserRouter + Routes + main-only lifecycle hooks
      (audio listeners, hot cache, global shortcuts, mini-player
      bridge, easter egg, scrollbar auto-hide)

AppShell + RequireAuth + TauriEventBridge are now named exports from
App.tsx so MainApp can compose them; Phase C/D will extract those into
their own modules.

App.tsx: 1453 -> 1308 LOC. Behaviour-preserving.
2026-05-12 02:08:39 +02:00
Frank Stellmacher 0cd8998dc9 refactor(app): Phase B.1 — extract pre-React bootstrap into src/app/ (#555)
main.tsx shrinks from 56 -> 17 LOC. New module surface:

  - src/app/windowKind.ts: cached getWindowKind() detector,
    replaces the global __PSY_WINDOW_LABEL__ string everywhere
  - src/app/bootstrap.ts: pushUserAgentToBackend +
    pushLoggingModeToBackend + runPreReactBootstrap orchestrator

App.tsx + playerStore.ts now read getWindowKind() instead of poking
window.__PSY_WINDOW_LABEL__ directly. Behaviour-preserving.
2026-05-12 01:39:39 +02:00