Commit Graph

76 Commits

Author SHA1 Message Date
Psychotoxical 10d948db61 refactor(lib): relocate cardGridLayout → lib/util (store-free)
cardGridLayout's only store dep was three grid-column constants it imported from
authStoreDefaults. Flipped ownership: the constants now live in lib/util/cardGridLayout
(pure layout config) and authStoreDefaults re-exports them (store→lib), so the auth
settings clamp/default and every existing consumer are unchanged. cardGridLayout is now
store-free and homes in lib/util alongside the other pure helpers — consumed cleanly by
ui/VirtualCardGrid + cover/layoutSizes. Resolves the documented store-free-lib block for
the card-grid layout helper. Pure move + constant relocation; tests pass unmodified.
2026-06-30 21:28:03 +02:00
Psychotoxical c37d5f7389 refactor(network): decouple network-reachability layer from features → lib/network
The connection-reachability + Subsonic network-guard helpers were pinned low by
four lib/api consumers (subsonicLibrary/Playlists/Ratings/Scrobble) yet ran two
lower-layer→feature inversions. Both removed without a registry:

- devOfflineBrowseStore is a self-contained DEV-only toggle with zero offline-
  feature coupling — it was only colocated there. Relocated features/offline/store
  → store/ (global). The @/features/offline barrel re-exports it so feature/UI
  consumers are unchanged; the three lower-layer readers (subsonicNetworkGuard,
  activeServerReachability, useConnectionStatus) now import it from @/store directly.

- subsonicNetworkGuard's only other feature dep was playback's resolvePlaybackUrl,
  used solely for the psysonic-local:// skip check. Added hasLocalPlaybackUrl to the
  existing M4 substrate store/localPlaybackResolve — it mirrors resolvePlaybackUrl's
  local-source branch exactly (same profile resolution; the empty-serverId playback
  fallback never applies in the guard), so the skip stays bit-identical.

network/ (subsonicNetworkGuard + activeServerReachability + tests) → lib/network,
now @/features-free. useConnectionStatus is now iron-rule-clean and stays in hooks/
(cross-cutting). Test mocks retargeted to the new seam modules.

Behavior-adjacent (covered by suite; default paths identical): the local-bytes skip
helper — flag for offline-playback runtime QA alongside the M4 media-resolver seam.
2026-06-30 21:23:44 +02:00
Psychotoxical d4ab56f5a6 refactor(lib,playback): relocate albumDetailNavigation + mixRatingFilter decouple-knots
Two lower-layer→feature inversions removed:

- Detail-route predicates (isAlbumDetailPath/isArtistDetailPath/isComposerDetailPath)
  were defined in features/album's browse store but are pure URL checks. Extracted
  to lib/navigation/detailRoutePaths; the browse store re-exports them so the
  @/features/album barrel surface is unchanged. This frees albumDetailNavigation of
  its only @/features import, so it moves utils/navigation → lib/navigation (drains
  utils/navigation). Also fixes the M5 isArtistDetailPath misplacement.

- mixRatingFilter's sole feature dep is playback's userRatingOverrides and its only
  binding consumer is playback's buildInfiniteQueueCandidates, so it belongs in the
  playback feature: utils/mix → features/playback/utils (drains utils/mix). The
  earlier handoff note that lib/api/subsonicStarRating consumes it was stale — that
  is only a code comment, verified no runtime import.

Pure moves; tests pass unmodified. The share cluster + switchActiveServer stay in
utils/ — they are imported by the a11y-HELD PasteClipboardHandler, so relocating
them would rewrite a do-not-touch file.
2026-06-30 21:17:01 +02:00
Psychotoxical fbca1831a1 refactor(utils): split componentHelpers grab-bag to owning layers
Per-file routing of the 9-file utils/componentHelpers grab-bag, each driven by
its verified consumer set:
- contextMenu{Actions,Helpers} → features/contextMenu/utils (sole consumer)
- queuePanelHelpers → features/queue/utils (sole consumer)
- nowPlayingHelpers → features/nowPlaying/utils; isRealArtistImage split out to
  cover/ (consumed by cover/artistHero, a lower layer)
- appUpdaterHelpers(+test) + listReorder(+test) → lib/util (config/ + lib/hooks
  consume them; pure)
- playlistDetailHelpers → lib/format (consumed by 3 features; pure)
- appShellHelpers → app/ (app-shell consumers only)
- userMgmtHelpers left in place: consumed by the a11y-HELD SongInfoModal, so a
  move would rewrite a do-not-touch file (same block as licensesData).

Pure moves; tests pass unmodified.
2026-06-30 21:05:16 +02:00
Psychotoxical 13142e6ae3 refactor(lib): extract clean server + navigation infra to lib/ (decouple-knot files stay in utils/ pending seam) 2026-06-30 20:44:34 +02:00
Psychotoxical 7d98e649da refactor(hooks): relocate misplaced useTracklistColumns hook from utils/ to hooks/ (cross-cutting) 2026-06-30 20:39:22 +02:00
Psychotoxical 390fdfbcca refactor(lib): fold utils/perf into lib/perf + utils/ui (DOM utilities) into lib/dom 2026-06-30 20:36:23 +02:00
Psychotoxical 6becd57798 refactor(lib,playback): fold utils/cache+themes into lib; playbackScheduleFormat→playback (drains utils/format) 2026-06-30 20:33:25 +02:00
Psychotoxical 31d4a87401 refactor(lib): fold utils/audio + autodj-overlap into lib/audio (audio-transition infra; drains utils/playback) 2026-06-30 20:30:07 +02:00
Psychotoxical aea8b7750b refactor(lib,playback,whatsNew): home waveform→lib, timeline utils→playback, releaseNotes+changelog→whatsNew 2026-06-30 20:26:34 +02:00
Psychotoxical 695462ed87 refactor(cover,lib): fold utils/cover into cover/ + utils/media into lib/media (generic infra) 2026-06-30 20:21:15 +02:00
Psychotoxical 982499d32b refactor(orbit,playback): move feature-owned stores (helpModal→orbit; libraryPlaybackHint+playerBarLayout→playback) 2026-06-30 20:16:55 +02:00
Psychotoxical 7ae259cacf refactor(updater,settings): move feature-owned updater + theme-update hooks into their features 2026-06-30 20:03:25 +02:00
Psychotoxical 312fa78240 refactor(playback,playlist): move player-bar overflow/popover/delay + bulk-picker hooks into their features 2026-06-30 19:59:38 +02:00
Psychotoxical c2461c88d5 refactor(playback): co-locate floating-player-bar + fs-idle-fade hooks (and orphaned playback tests) with their features 2026-06-30 19:55:20 +02:00
Psychotoxical f03a5f13e8 refactor(cover): move cover-coupled hooks into cover/; drop deprecated usePlaybackCoverArt shim 2026-06-30 19:52:29 +02:00
Psychotoxical 28e049425e refactor(music-network): move enrichment-primary icon/label hooks into music-network/ui 2026-06-30 19:48:54 +02:00
Psychotoxical 777c72adf7 refactor(lib): move library-index hooks into lib/library/hooks 2026-06-30 19:48:49 +02:00
Psychotoxical 1f93984482 refactor(hooks): move generic scroll/pagination/dnd UI hooks into lib/hooks 2026-06-30 19:34:28 +02:00
Psychotoxical 31bc24a178 refactor(hooks): co-locate feature-coupled hooks (playback server/navigate/timeline/audio-devices->playback, smart-collage/pending-polling->playlist, contextmenu rating/keyboard->contextMenu) 2026-06-30 19:30:41 +02:00
Psychotoxical e0d6623bf1 refactor(lyrics): co-locate lyrics feature into features/lyrics (LyricsPane + hooks + cache + lrclib/lyricsplus/netease providers; lyricsStore stays global) 2026-06-30 19:21:21 +02:00
Psychotoxical d9dfdf66e9 refactor(music-network): co-locate MusicNetworkIndicator + presetIcon into music-network/ui (barrel-exported) 2026-06-30 19:15:56 +02:00
Psychotoxical 15a6590e77 refactor(updater+contextMenu): co-locate updater feature (AppUpdater+Changelog) and the context-menu subsystem into features/ 2026-06-30 19:12:26 +02:00
Psychotoxical 7d2645171b refactor(components): co-locate remaining UI/feature components (TracklistColumnPicker+LosslessModeBanner->ui, fixedThemes->utils/themes, ThemeUpdateBanner/WindowButtonPreview/AboutPsysonicLol->settings, BottomNav/MobileMoreOverlay->sidebar, MobilePlayerView->nowPlaying, WhatsNewBanner->whatsNew) 2026-06-30 19:07:46 +02:00
Psychotoxical d876781fcb refactor(components): co-locate feature-specific components (HostApprovalQueue->orbit, Playback{Delay,Schedule}->playback, LicensesPanel->settings) 2026-06-30 18:55:48 +02:00
Psychotoxical 9eee4eed5b refactor(components): GenreFilterBar -> ui/ (shared), SongCard -> features/home (sole consumer) 2026-06-30 18:50:46 +02:00
Psychotoxical afa8999d37 refactor(search): co-locate tracks song-browse cluster (SongRow/PagedSongList/SongBrowseSection/TracksPageChrome/useSongBrowseList) into features/search 2026-06-30 18:41:31 +02:00
Psychotoxical 5c3a135040 refactor(playback): co-locate player bar UI (PlayerBar + playerBar/* + PlaybackBufferingOverlay) into features/playback/components 2026-06-30 18:37:06 +02:00
Psychotoxical 5cb5cf1d24 refactor(equalizer): co-locate equalizer UI into features/equalizer (eqStore/eqCurve stay audio-core) 2026-06-30 18:34:11 +02:00
Psychotoxical 41877d10d8 refactor(ui): move generic presentational primitives into ui/ (CoverLightbox, StarRating, LongPressWaveOverlay, icons, VirtualCardGrid) 2026-06-30 18:28:54 +02:00
Psychotoxical 8cd23e86db refactor(ui): move generic filter/list primitives into ui/ (filter buttons, SortDropdown, MarqueeText, SelectionToggleButton, InpageScrollSentinel) 2026-06-30 18:25:20 +02:00
Psychotoxical b3b7af0390 refactor(help): co-locate help page into features/help; pages/ now empty 2026-06-30 18:16:44 +02:00
Psychotoxical 1e3c7f1915 refactor(whatsNew): co-locate release-notes page into features/whatsNew 2026-06-30 18:16:44 +02:00
Psychotoxical 132db4e620 refactor(auth): co-locate login page into features/auth 2026-06-30 18:16:44 +02:00
Psychotoxical 2f4303ecc8 refactor(home): co-locate Mainstage/home feature into features/home 2026-06-30 18:12:27 +02:00
Psychotoxical f05183d412 refactor(randomMix): co-locate random/lucky-mix feature into features/randomMix 2026-06-30 18:08:15 +02:00
Psychotoxical f443a60c54 refactor(composers): co-locate composers feature into features/composers 2026-06-30 18:03:43 +02:00
Psychotoxical 5fdec6cd2a refactor(genre): co-locate genre browse pages into features/genre 2026-06-30 17:59:06 +02:00
Psychotoxical 5808ef0839 refactor(folderBrowser): co-locate folder-browser feature into features/folderBrowser 2026-06-30 17:57:17 +02:00
Psychotoxical ac6974d4f1 refactor(album): co-locate NewReleases + MostPlayed browse pages into features/album 2026-06-30 17:54:25 +02:00
Psychotoxical 572dce4703 refactor(lib): move songToTrack + pure server-scope helpers to lib/media; split trackServerScope
songToTrack (the canonical Subsonic-song -> Track mapper) and the pure
server-scope helpers (activeServerProfileId, stampTrackServerId/s,
isMultiServerQueue, profileIdFromQueueRef) operate only on the lib/media model +
authStore + server-key utils -- no playback-store read. Move them to lib/media so
the ~58 app-wide consumers (cover, context menus, sharing, lucky-mix, library
browse, pages, hooks) depend on lib, not on the playback feature.

trackServerScope is split: the store-reading queue helpers (queueItemRefAt,
filterQueueRefs*, activeServerQueueTrackIds) stay in
features/playback/utils/playback/trackServerScope and build on the pure lib half.

With Track/QueueItemRef (prior commit), songToTrack and shuffleArray now in lib,
the media-domain model is fully out of features/playback. The remaining
playerStoreTypes imports are PlayerState only (the store shape). tsc 0, lint 0,
full suite 2353/2353, build OK.
2026-06-30 17:24:29 +02:00
Psychotoxical 9058abd340 refactor(lib): extract Track/QueueItemRef domain model to lib/media/trackTypes
Track is the app's normalized song model and QueueItemRef its thin queue
identity -- consumed app-wide (queue, cover, context menus, sharing, lucky-mix,
library browse), not a playback-internal detail. They lived in
features/playback/store/playerStoreTypes, forcing ~115 core/feature files to
reach into the playback feature for the central media type.

Move the two interfaces to lib/media/trackTypes; playerStoreTypes keeps
PlayerState and now imports them from lib. 115 importers repointed (mixed
'PlayerState, Track' lines split so PlayerState stays). Pure type move (erased at
runtime). tsc 0, lint 0.
2026-06-30 17:16:56 +02:00
Psychotoxical 2756cb7698 refactor(lib): relocate the local-library subsystem utils/library -> lib/library
utils/library is the local-index query/browse/sync engine -- feature-free shared
infra consumed by the album, search, genre, artist and offline features. With
its last feature edges removed (queue resolver, shuffleArray, albumBrowseCatalogChunk,
genreBrowsePlayback all relocated earlier this branch), the whole directory is
pure infra and belongs under lib/.

Whole-directory move (58 files, 76 consumers rewritten) via resolver-based
rewrite: cross-cluster relatives absolutized to @/, intra-cluster siblings kept
relative. utils/library is gone; lib/library imports zero @/features. Three
stale doc comments in the album/search/artist barrels repointed.

tsc 0, lint 0, full suite 2353/2353, production build OK.
2026-06-30 17:06:03 +02:00
Psychotoxical e5705f853e refactor(playback): move genreBrowsePlayback into the feature; utils/library now feature-free
genreBrowsePlayback is a "play X" queue builder -- it turns a genre seed into a
Track[] for the player, the genre analogue of playArtistShuffled which already
lives in features/playback/utils/playback. Consumed only by the 3 genre pages,
by no utils/library sibling, so the whole-file move is clean (no split). Its
Track/songToTrack edges become intra-feature; its remaining utils/library
siblings (advancedSearchLocal, albumBrowseSort, genreCatalogCountsCache,
genreAlbumBrowse, libraryReady) are plain infra deps.

utils/library now has ZERO @/features importers (source AND tests) -- the bulk
is ready to relocate to lib/library. tsc 0, lint 0, genre suite green.
2026-06-30 17:00:57 +02:00
Psychotoxical edab32d7ee refactor(lib): relocate shuffleArray to lib/util; albumBrowseCatalogChunk to features/album
shuffleArray is a pure generic Fisher-Yates over T[], misfiled in
features/playback. Move to lib/util (next to dedupeById): fixes the existing
core->feature edges (utils/componentHelpers/*) and drops one of
genreBrowsePlayback's playback deps.

albumBrowseCatalogChunk is the feature-layer orchestrator that picks the offline
branch on top of the pure lib catalog loaders; its sole consumer lives in
features/album and its offline dep is a legal feature->feature edge there. Its
.test.ts stays in utils/library -- that test exercises fetchLocalAlbumCatalogChunk
from albumBrowseLoad (misnamed), which is not moving.

utils/library now has a single remaining feature importer: genreBrowsePlayback
(songToTrack + type Track), blocked on the Track domain model living in
features/playback. tsc 0, lint 0, targeted suites green.
2026-06-30 16:55:18 +02:00
Psychotoxical f41005682d refactor(playback): move thin-state queue resolver out of utils/library into the feature
The queue-resolver family (queueTrackResolver, queueRestore, queueItemRef,
queueTrackView) runtime-imports usePlayerStore / playerStoreTypes — it is the
playback engine's thin-state queue subsystem, not shared library infra. It sat
in utils/library only because it queries the local index. Co-locate into
features/playback/store so those edges become intra-feature; its remaining deps
(advancedSearchLocal, libraryReady, serverLookup, serverIndexKey, authStore) are
plain core/infra (feature -> infra, no inversion).

Removes 4 of the 6 store/utils -> feature runtime inversions that blocked the
utils/library -> lib move. 8 files moved, 46 consumers rewritten. tsc 0, lint 0,
targeted suites green.
2026-06-30 16:50:29 +02:00
Psychotoxical cb1a110afb refactor(playback): move the audio engine into features/playback
Relocate the playback/queue/transport/audio-output engine out of the type-first
store/ + utils/playback/ + utils/audio/ dirs into a cohesive src/features/playback/,
structure-preserving:
  store/<x>                    -> features/playback/store/<x>
  store/audioListenerSetup/<x> -> features/playback/store/audioListenerSetup/<x>
  utils/playback/<x>           -> features/playback/utils/playback/<x>
  utils/audio/<x>              -> features/playback/utils/audio/<x>

184 files moved (107 source + 77 tests), 365 consumers rewritten. Pure move — no
behavior change, no state-split (the playerStore state-split stays a separate M5
question). Enabled by this session's decouple seams (artist/offline/orbit/auth →
core registries), so the engine carries no inbound core->feature inversion: store/
now holds only the 50 cross-cutting global stores (auth family, the seams, library
index, UI/settings stores).

KEPT OUT of the move (would re-create global->engine edges): the 3 pure config
helpers utils/audio/{loudnessPreAnalysisSlider,hiResCrossfadeResample} +
utils/playback/autodjOverlapCap (authStore + settings UI read them — they stay in
utils/). Ambiguous view-state stores (eqStore, queueToolbarStore,
playerBarLayoutStore) stay global (no engine imports).

Consumers use DEEP paths (@/features/playback/...), no barrel — matches the lib/
approach and avoids barrel-mock-collapse across the 140 usePlayerStore consumers.
Two tolerated type-only core->feature edges remain (localPlaybackStore->QueueItemRef,
localPlaybackMigration->HotCacheEntry, both erased).

tsc 0, lint 0, full suite 319/2353 green, iron-rule clean (no runtime store->feature
import). Behavior-touching only via the prerequisite bridge seam (already QA-flagged);
the move itself is pure.
2026-06-30 15:00:20 +02:00
Psychotoxical 651a2adba4 refactor(decouple): orbit seam — invert orbit off the audio core
Add core seam store/orbitRuntime.ts: a registry (registerOrbitRuntime) exposing a
neutral session snapshot {role,phase,state} + an async bulkGuard, plus pure
derivations mirrored from the feature (isInOrbitSession, isOrbitPlaybackSyncActive,
estimateLivePosition). Default (unregistered) = neutral snapshot + bulkGuard allow
— bit-identical to today's no-session behavior, and a session can only start via
the topbar which loads the @/features/orbit barrel (→ registers) first.

features/orbit/utils/orbitBulkGuard registers the runtime at module init
(store-backed getSnapshot + the existing confirm-modal orbitBulkGuard as the gate).
The orbit feature keeps its own copies of the pure helpers for UI (incl. the
arg-form isOrbitPlaybackSyncActive(role,phase) used by two settings/player-bar
components), so nothing UI-facing changes.

Repointed the 9 audio-core sites (playbackRateStore, previewStore,
playbackReportSession, nextAction, resumeAction, playTrackAction,
queueMutationActions, playAlbum) from @/features/orbit to @/store/orbitRuntime;
state reads (useOrbitStore.getState().role/.state) become orbitSnapshot().
Migrated 6 audio-core test mocks to @/store/orbitRuntime via importOriginal-spread
(keeps registerOrbitRuntime callable). enqueueShareSearchPayload stays on the orbit
barrel (share util, not audio core) — its test mock unchanged.

Decouple Step 3 — last seam. The audio ENGINE is now free of @/features/* runtime
imports (only type-only edges + the non-engine composerBrowseSessionStore browse
store remain). Unblocks the playback-core move + utils/library→lib.

tsc 0, lint 0, full suite 319/2353 green.
NEEDS Frank's live-session QA before relying on it (host+guest bulk-replace modal,
guest catch-up, rate/preview/scrobble suppression).
2026-06-30 14:19:22 +02:00
Psychotoxical 42ef09f1e4 refactor(decouple): media-resolver seam — invert resolve* off the audio core
Add core seam store/mediaResolver.ts: a registry (registerMediaResolver) with
delegating resolveAlbum/resolveArtist/resolvePlaylist + the pure auth wrappers
resolveMediaServerId/resolveAlbumForActiveServer/resolveAlbumForServer and the
ResolvedAlbum type. Default (unregistered) = network-only safety net (no
library-index/offline branch — those stay in the feature).

features/offline/utils/offlineMediaResolve keeps the offline-aware policy impls
and registers them at module init (registerMediaResolver). The call runs at boot:
AppShell eagerly imports the @/features/offline barrel, whose export * evaluates
offlineMediaResolve. The 3 thin wrappers + ResolvedAlbum move to the core seam and
are re-exported from offlineMediaResolve so the offline barrel still surfaces them
for the ~25 UI consumers (unchanged).

Repointed the 5 audio-core call sites (fetchTracksForSource, playAlbum,
playArtistShuffled, playByOpaqueId, luckyMixHelpers) from @/features/offline to
@/store/mediaResolver. Migrated playAlbum.test's mock to the new module via
importOriginal-spread (keeps registerMediaResolver callable when the offline
barrel loads transitively — partial-mock-collapse otherwise).

Decouple Step 2b. The audio core's only remaining @/features/offline reference is
now the type-only localPlaybackMigration import (erased). Runtime offline edge gone;
orbit seam (Step 3) remains.

tsc 0, lint 0, full suite 319/2353 green.
2026-06-30 14:08:43 +02:00
Psychotoxical bd5143d98c refactor(decouple): extract local-playback resolve substrate to store/
Pull findLocalPlaybackUrl/findLocalPlaybackEntry/hasLocalPersistentPlaybackBytes
+ the entry/index-key membership helpers (entryBelongsToServer,
indexKeyBelongsToServer, findFavoriteAutoEntry, hasLocal{Library,FavoriteAuto}Bytes)
out of features/offline/utils/offlineLibraryHelpers into a new core module
store/localPlaybackResolve.ts. The substrate depends only on authStore +
localPlaybackStore + serverIndexKey utils (no useOfflineStore), so the audio
core can resolve on-disk bytes without inverting into @/features/offline.

Repointed the audio-core consumers (crossfadePreload, playbackUrlRouting,
resolvePlaybackUrl, playTrackAction, promoteStreamCache, hotCachePrefetch,
hotCacheStore) + offline-internal callers + favorites hook; moved the two
barrel vi.mocks onto the new leaf module. offlineLibraryHelpers re-imports the
two primitives it still needs internally.

Decouple Step 2a. Removes the local-bytes offline edge from the audio core;
the resolve* media-resolution family (Step 2b) + orbit seam (Step 3) remain.

tsc 0, lint 0, full suite 319/2353 green.
2026-06-30 09:10:18 +02:00