Commit Graph

1497 Commits

Author SHA1 Message Date
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 6651abbc6f refactor(decouple): playback-engine bridge — break authStore→engine edges
The last core→engine inversions blocking the playback-core move live in the
authStore settings/profile family: authServerProfileActions reads
usePlayerStore.queueServerId + calls clearQueueServerForPlayback (on server
delete), and authAudioSettingsActions calls
usePlayerStore.updateReplayGainForCurrentTrack from ~8 ReplayGain/normalization
setters. Both are re-exported through the authStore barrel, so a global store
would depend on the (soon-to-move) engine.

Add core seam store/playbackEngineBridge.ts: a registry exposing getQueueServerId
/ clearQueueServerForPlayback / updateReplayGainForCurrentTrack with no-op/null
defaults. The engine registers its impls via store/playbackEngineBridgeRegister.ts
(side-effect-imported by MainApp at boot). authStore actions now call the neutral
delegators instead of importing the engine.

Default is safe: the only callers are user-triggered settings/profile actions that
fire long after boot (engine already registered); at boot there's no current track
or queue binding, so the no-op would be correct anyway. authStore.servers.test
imports the register module so removeServer's queue-clear runs through the real
engine wiring.

Prepares the playback-core move (Engine → features/playback): with this + keeping
the 3 config-helper utils in utils/ + tolerating 2 type-only edges, the engine has
no inbound core→feature inversions left. tsc 0, lint 0, suite 319/2353 green.
Behavior-touching (server-delete queue clear, settings→gain refresh) → Frank QA.
2026-06-30 14:45:34 +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
Psychotoxical 36b0042d9a refactor(decouple): move coerceOpenArtistRefs to lib/api (off artist feature)
First step of decoupling the audio core from feature imports. coerceOpenArtist
Refs is a pure Subsonic-response normalizer (one-object-vs-array quirk) that was
mis-membered in features/artist but consumed by the audio core (songToTrack,
trackArtistRefs) and others. Relocate it beside its SubsonicOpenArtistRef type in
lib/api/; drop the artist-barrel re-export; repoint the 4 barrel consumers.

Removes the @/features/artist runtime edge from the audio core (utils/playback)
entirely. tsc 0, lint 0/0, suite 319/2353 green.

(composerBrowseSessionStore's ALL_SENTINEL import from @/features/artist remains
— that's a non-audio global browse store, separate from this decouple.)
2026-06-30 08:53:56 +02:00
Psychotoxical a27b26abbd fix(shortcuts): keep shortcut contract in config/ (Rust include_str! path)
The shortcut contract (shortcutActions.ts + shortcutTypes.ts) is NOT a
pure-frontend file: src-tauri/src/cli/parse.rs include_str!s
'/../src/config/shortcutActions.ts' at compile time, so moving it to
lib/shortcuts/ (slice 7ad19671) broke the Rust build (couldn't read the file ->
dev build won't start). Revert the contract back to src/config/ — it has a
build-time backend path contract and belongs where Rust anchors it, not in lib/.
No Rust file touched (fix = restore the frontend path the backend expects).

cargo check green (13s), tsc 0, lint 0/0, frontend suite 319/2353 green.

Lesson: grep src-tauri for include_str!/include_bytes! '../src/...' before
moving ANY frontend file — Rust can pin a TS file by path.
2026-06-30 08:46:59 +02:00
Psychotoxical d5bbabac1d refactor(queue): co-locate QueuePanel UI into features/queue
Extract the queue UI (QueuePanel + queuePanel/* components + useQueue* hooks)
into features/queue/ with a barrel. This is the chosen playback/queue boundary:
queue = the QueuePanel UI that CONSUMES the playback store; queue STATE and the
audio engine stay in store/ (playback-core, moved separately). Verified
one-way: no playback-core file imports the queue UI back (only doc comments
mention it).

Excluded as NOT queue-UI: useIdlePlayQueuePull (AppShell) and
usePlayQueueSyncLedState (ConnectionIndicator) are queue-SYNC orchestration,
not panel UI — left in hooks/ to move with playback.

Pure move. One test fix: QueuePanel.test.tsx reads its subject via a hardcoded
readFileSync('src/components/QueuePanel.tsx') path (architecture-pin grep for
forbidden HTML5 DnD) — repointed to the new location (the move tool can't
rewrite non-import string paths). tsc 0, lint 0/0, suite 319/2353 green.
2026-06-30 08:40:51 +02:00
Psychotoxical 7ad196711e refactor(lib): move generic hooks, DnD engine, shortcut contract to lib
Continue M4 lib/ de-flattening with domain-agnostic infra:
- lib/hooks/: 9 pure generic hooks (useDebouncedValue, useIsMobile,
  useLongPressAction, useRangeSelection, useResizeClientHeight,
  useWindowVisibility, useSystemPrefersDark, useVirtualizerScrollMargin,
  useRemeasureGridVirtualizer) — all PURE (only react/zustand/tanstack deps).
- lib/dnd/DragDropContext.tsx: the generic mouse-event DnD engine (WebKitGTK
  HTML5-DnD workaround, ~24 importers, self-contained) — empties src/contexts/.
- lib/shortcuts/: shortcutActions + shortcutTypes (the action-id + binding
  contract; registry/dispatch/bindings stay app-level and import the contract,
  app->lib direction).

useWindowFullscreenState deliberately NOT moved — kept in hooks/ as app-shell
per the handoff iron-rule list (overrides its pure-helper appearance).

Pure move via deep @/lib/* specifiers. tsc 0, lint 0/0, full suite 319 files /
2353 tests green.
2026-06-30 08:20:11 +02:00
Psychotoxical 209dd61442 refactor(lib): consolidate generic infra into src/lib
Move domain-agnostic, feature-free helpers out of the flat utils/ and store/
roots into src/lib/ (plan M4, §3 lib/ layer):
- lib/format/: formatBytes, formatClockTime, formatDuration, formatHumanDuration,
  relativeTime (pure format/date/byte/clock helpers)
- lib/i18n.ts: i18next bootstrap (global app infra)
- lib/util/: sanitizeHtml, platform, dedupeById, safeStorage (pure helpers +
  the zustand storage adapter)

playbackScheduleFormat stays in utils/format (runtime usePlayerStore dep =
audio-core coupling, not generic). formatClockTime keeps a type-only
@/store/authStoreTypes import (erased, no runtime inversion — accepted per the
deviceSync precedent).

Pure move via deep @/lib/* specifiers (no barrel → no mock-collapse surface).
tsc 0, lint 0/0, full suite 319 files / 2353 tests green.

Tooling note: lib_move.py regex extended to also rewrite side-effect imports
(import './i18n') and vi.importActual paths — both were silent gaps.
2026-06-30 08:07:04 +02:00
Psychotoxical 8cc022581f refactor(api): pull feature-resident subsonic clients into lib/api
Move the 5 subsonic api modules that M3 had co-located into features
(subsonicAlbumInfo/Artists/Playlists/Radio/Statistics) into src/lib/api/, the
feature-free infra layer, and drop their feature-barrel re-exports. Consumers
now import these protocol calls directly from @/lib/api/subsonic*; the feature
barrels export only domain UI/hooks/state.

This is the consistent api placement (plan §10.3, revised: ALL subsonic protocol
clients are feature-free infra, not per-feature) and it kills the documented
api-induced feature<->feature cycles: offline->artist/playlist (getArtist/
getPlaylist*), album->artist (getArtistInfo), deviceSync/nowPlaying/orbit->*.
Remaining artist<->album refs are UI-only (OpenArtistRefInline/coerceOpenArtist
Refs) and offline->playlist is store-level (usePlaylistStore) — both deeper,
tracked for M5, not api placement.

Pure move: import specifiers + vi.mock/spy targets repointed; no behaviour
change. tsc 0, lint 0/0, full suite 319 files / 2353 tests green.

Tooling note: barrel-imported and dynamic-import api symbols were split out of
@/features/* to @/lib/api/* (tsc-driven); 12 vi.mock barrels retargeted to the
deep lib module (mock-collapse sweep), AlbumHeader's UI mock left untouched.
2026-06-30 08:02:19 +02:00
Psychotoxical 7e9cb60763 refactor(api): co-locate server-protocol clients into lib/api
Move the Subsonic + Navidrome protocol clients and the library IPC facade
(subsonic*, navidrome*, library.ts + tests) from src/api/ into src/lib/api/,
the feature-free infra layer (plan M4, decision §10.3: shared client core → lib/).
Pure move: deep-path import specifiers @/api/* -> @/lib/api/* across ~150
consumers; no behaviour change. Domain REST (lyrics/events) and cover/analysis
infra stay in src/api/ pending their own M4 placement.

tsc 0, lint 0/0, full suite 319 files / 2353 tests green.
2026-06-30 07:53:25 +02:00
Psychotoxical 429e632598 test(m3): complete genreAlbumBrowse mock pulled in via album barrel
The album barrel reaches genreBrowsePlayback.test's transitive graph through the
artist↔album edge (songToTrack → @/features/artist → @/features/album →
useGenreAlbumBrowse), which needs GENRE_ALBUM_FIRST_PAGE from the partially
mocked ./genreAlbumBrowse. Spread importOriginal so it stays real.
2026-06-30 01:54:32 +02:00
Psychotoxical fecda65f11 test(m3): retarget collapsed barrel mocks to deep submodules
The move tool rewrote single-module mocks (e.g. vi.mock('@/utils/orbitBulkGuard'))
into feature-BARREL mocks (vi.mock('@/features/orbit', ...)). A partial barrel
factory shadows every other barrel export to undefined — and src/test/helpers/
storeReset.ts reads useOrbitStore.getState() at module load, so 6 test files
(playerStore.misc/events/queue/playbackActions, ContextMenu, QueuePanel) crashed
at collection (0 tests run, mis-read as flaky teardown). Retarget each collapsed
mock to its deep submodule so the barrel re-exports the stub while siblings
(useOrbitStore, offlineActionPolicy, usePlaylistStore) stay real.
2026-06-30 01:50:37 +02:00
Psychotoxical 862941c145 refactor(playlist): co-locate playlist feature into features/playlist 2026-06-30 01:37:05 +02:00
Psychotoxical 2a425862ae refactor(album): co-locate album feature into features/album 2026-06-30 01:33:29 +02:00
Psychotoxical 9ffc42688c refactor(artist): co-locate artist feature into features/artist 2026-06-30 01:27:01 +02:00
Psychotoxical 17ea96dcdb test(offline): mock useOfflineBrowseReloadToken submodule in useSongBrowseList
The offline move collapsed a single-module mock into a `@/features/offline`
barrel mock, which shadowed the real `useOfflineBrowseContext` the test relied
on. Mock the reload-token submodule directly so the barrel re-exports the stub
while the sibling context hook stays live.
2026-06-30 01:26:46 +02:00
Psychotoxical 321d5ff6ab refactor(orbit): co-locate orbit feature into features/orbit 2026-06-30 01:02:58 +02:00
Psychotoxical d6dbb615fd refactor(offline): co-locate offline feature into features/offline 2026-06-30 00:56:08 +02:00
Psychotoxical 70c145db06 refactor(waveform): move waveform data pipeline back to audio-core
The M0 pilot mis-membered the waveform data pipeline into features/waveform,
creating a core->feature inversion: ~12 audio-core files in store/ imported
refreshWaveformForTrack / fetchWaveformBins / bumpWaveformRefreshGen from the
feature barrel. These are playback coordination (write waveformBins into
playerStore on track change, last-write-wins gen guard), not seekbar rendering.

Move waveformRefresh + waveformRefreshGen (+ tests) to src/store/ and
waveformParse (+ test) to src/utils/waveform/ (beside waveformSilence). The
feature now holds only the seekbar UI (WaveformSeek/SeekbarPreview + their
hooks/render utils); its barrel exports UI only. Core consumers import the
pipeline directly from @/store/*. Restores the no-core->feature-inversion rule.
2026-06-30 00:07:34 +02:00
Psychotoxical e945da693a refactor(settings): co-locate settings feature into features/settings
Theme infra (fixedThemes, theme utils), music-network presetIcon, audio-device
probe, library-index sync, and the auth*SettingsActions slices stay in the
core/global layer (consumed via @/ alias) to avoid inverting core->feature.
2026-06-29 23:48:19 +02:00
Psychotoxical fbc37db64e refactor(search): co-locate search feature into features/search 2026-06-29 23:42:22 +02:00
Psychotoxical 931c47e19e refactor(nowPlaying): co-locate now playing feature into features/nowPlaying 2026-06-29 23:38:09 +02:00
Psychotoxical e0e10e0034 refactor(sidebar): co-locate sidebar feature into features/sidebar 2026-06-29 23:26:00 +02:00
Psychotoxical e6269c7b85 refactor(miniPlayer): co-locate mini player feature into features/miniPlayer 2026-06-29 23:22:00 +02:00
Psychotoxical 6dd0d4a130 refactor(fullscreenPlayer): co-locate fullscreen player feature into features/fullscreenPlayer 2026-06-29 23:17:14 +02:00
Psychotoxical 896fe3f407 refactor(radio): co-locate internet radio feature into features/radio 2026-06-29 23:04:34 +02:00
Psychotoxical dbffe59e58 refactor(favorites): co-locate favorites feature into features/favorites 2026-06-29 22:59:21 +02:00
Psychotoxical ea3e3f1adf refactor(deviceSync): co-locate device sync feature into features/deviceSync 2026-06-29 22:55:35 +02:00
Psychotoxical 236acc33dd refactor(stats): co-locate statistics feature into features/stats 2026-06-29 22:51:54 +02:00
Psychotoxical 431d65b587 refactor(ui): co-locate CustomSelect as a shared primitive in src/ui 2026-06-29 22:36:43 +02:00
Psychotoxical 53b244de1f refactor(ui): co-locate CachedImage as a shared primitive in src/ui 2026-06-29 22:36:12 +02:00
Psychotoxical bd2d669b99 refactor(ui): co-locate OverlayScrollArea as a shared primitive in src/ui 2026-06-29 22:35:41 +02:00
Psychotoxical 146727e980 refactor(ui): co-locate TooltipPortal and tooltipAttrs as shared primitives in src/ui 2026-06-29 22:35:14 +02:00
Psychotoxical a18f8fc857 refactor(ui): co-locate BackToTopButton as a shared primitive in src/ui 2026-06-29 22:34:34 +02:00
Psychotoxical 6d7464fddf refactor(waveform): co-locate seekbar feature into features/waveform 2026-06-29 22:30:53 +02:00
Psychotoxical d9f8d01be9 build(restructure): wire @/ alias into vite dev/build resolver 2026-06-29 22:15:57 +02:00
Psychotoxical e66b674937 Revise README for feature enhancements and Polish language (#1221)
Updated README.md to reflect new features and language support.
2026-06-29 20:20:13 +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
Psychotoxical 4c102d88a5 chore(aur): bump PKGBUILD to v1.49.0 (#1214) 2026-06-29 03:09:23 +02:00
github-actions[bot] c9fa69a0ad chore(release): bump main to 1.50.0-dev (#1212)
* chore(release): bump main to 1.50.0-dev

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

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-29 02:47:22 +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 19860672eb Update TELEMETRY.md (#1210)
added fanart to external services
2026-06-28 23:20:22 +02:00
Psychotoxical 2b80a87bfc refactor(settings): shared id-based reorder for the customizer panels (#1207)
Extract the drag-to-reorder boilerplate the sidebar, artist-layout, lyrics,
queue-toolbar and servers customizers each hand-rolled into a single
`useListReorderDnd` hook plus an id-based `applyListReorderById`, and migrate
all five onto it. Reorders resolve by stable item id, never positional index,
so a render filter can never share an index space with the reorder and desync
it (the class behind #1164). No user-facing change.

- New: useListReorderDnd, applyListReorderById (+ tests), ReorderGripHandle.
- applySidebarReorderById now builds on the shared core; section/conserved
  guards stay sidebar-specific.
- Queue/mini-queue/playlist reorder stay backend-index based (out of scope).
2026-06-28 16:17:31 +02: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