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.
This commit is contained in:
Frank Stellmacher
2026-05-12 22:46:13 +02:00
committed by GitHub
parent e92535a5f5
commit 9fac6eb490
81 changed files with 155 additions and 236 deletions
+1 -1
View File
@@ -1,3 +1,4 @@
import { initAudioListeners } from '../store/initAudioListeners';
import { lazy, Suspense, useEffect, useState } from 'react';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
@@ -10,7 +11,6 @@ import ZipDownloadOverlay from '../components/ZipDownloadOverlay';
import FpsOverlay from '../components/FpsOverlay';
import { useAuthStore } from '../store/authStore';
import { useGlobalShortcutsStore } from '../store/globalShortcutsStore';
import { initAudioListeners } from '../store/playerStore';
import { initHotCachePrefetch } from '../hotCachePrefetch';
import { initMiniPlayerBridgeOnMain } from '../utils/miniPlayerBridge';
import { IS_WINDOWS } from '../utils/platform';
+5 -7
View File
@@ -1,3 +1,7 @@
import { flushPlayQueuePosition } from '../store/queueSync';
import { shuffleArray } from '../utils/shuffleArray';
import { songToTrack } from '../utils/songToTrack';
import { getPlaybackProgressSnapshot } from '../store/playbackProgress';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { listen } from '@tauri-apps/api/event';
@@ -14,13 +18,7 @@ import {
} from '../api/subsonic';
import i18n from '../i18n';
import { switchActiveServer } from '../utils/switchActiveServer';
import {
usePlayerStore,
getPlaybackProgressSnapshot,
songToTrack,
shuffleArray,
flushPlayQueuePosition,
} from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useKeybindingsStore, buildInAppBinding } from '../store/keybindingsStore';
import { useGlobalShortcutsStore } from '../store/globalShortcutsStore';
import { useZipDownloadStore } from '../store/zipDownloadStore';
+2 -2
View File
@@ -6,13 +6,14 @@
* payload) and the failure-tolerant path (no throw when invoke or storage
* misbehaves).
*/
import { installQueueUndoHotkey } from '../store/queueUndoHotkey';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(),
}));
vi.mock('../store/playerStore', () => ({
vi.mock('../store/queueUndoHotkey', () => ({
installQueueUndoHotkey: vi.fn(),
}));
@@ -21,7 +22,6 @@ vi.mock('./windowKind', () => ({
}));
import { invoke } from '@tauri-apps/api/core';
import { installQueueUndoHotkey } from '../store/playerStore';
import { getWindowKind } from './windowKind';
import {
pushLoggingModeToBackend,
+1 -1
View File
@@ -1,5 +1,5 @@
import { installQueueUndoHotkey } from '../store/queueUndoHotkey';
import { invoke } from '@tauri-apps/api/core';
import { installQueueUndoHotkey } from '../store/playerStore';
import { getWindowKind } from './windowKind';
/** Sync backend HTTP User-Agent from the main webview once at startup. */
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, HardDriveDownload, Check } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useOfflineStore } from '../store/offlineStore';
import { useAuthStore } from '../store/authStore';
import CachedImage from './CachedImage';
+3 -1
View File
@@ -1,8 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { Play, ChevronRight, Heart, ChevronDown, Check, RotateCcw, Square, AudioLines } from 'lucide-react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import { SubsonicSong } from '../api/subsonic';
import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useDragDrop } from '../contexts/DragDropContext';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -11,7 +12,7 @@ import {
getArtistInfo,
} from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { playAlbum } from '../utils/playAlbum';
import AlbumRow from './AlbumRow';
+2 -1
View File
@@ -6,6 +6,7 @@
* snapshots — tests survive a refactor that re-orders or re-styles the
* markup as long as the menu items + their handlers stay observable.
*/
import type { Track } from '@/store/playerStoreTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/api/subsonic', () => ({
@@ -43,7 +44,7 @@ vi.mock('@/utils/orbitBulkGuard', () => ({
import ContextMenu from './ContextMenu';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import { usePlayerStore, type Track } from '@/store/playerStore';
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore, type ServerProfile } from '@/store/authStore';
import { resetAllStores } from '@/test/helpers/storeReset';
import { makeTrack, makeServer } from '@/test/helpers/factories';
+3 -1
View File
@@ -1,3 +1,5 @@
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { Play, ListPlus, Radio, Heart, Download, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Plus, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react';
import { useOrbitStore } from '../store/orbitStore';
@@ -10,7 +12,7 @@ import {
import LastfmIcon from './LastfmIcon';
import StarRating from './StarRating';
import { lastfmLoveTrack, lastfmUnloveTrack } from '../api/lastfm';
import { usePlayerStore, Track, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { SubsonicAlbum, SubsonicArtist, star, unstar, getSimilarSongs2, getSimilarSongs, getTopSongs, buildDownloadUrl, getAlbum, getArtist, getPlaylists, getPlaylist, updatePlaylist, SubsonicPlaylist, setRating } from '../api/subsonic';
import { useNavigate } from 'react-router-dom';
+3 -2
View File
@@ -1,10 +1,11 @@
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useCallback, useEffect, useState, useRef, memo, useMemo } from 'react';
import {
Play, Pause, SkipBack, SkipForward,
ChevronDown, Repeat, Repeat1, Square, Music, Heart, MicVocal,
Moon, Sunrise,
} from 'lucide-react';
import { usePlayerStore, getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { buildCoverArtUrl, coverArtCacheKey, getArtistInfo, star, unstar } from '../api/subsonic';
import { useCachedUrl } from './CachedImage';
import { getCachedBlob } from '../utils/imageCache';
@@ -13,7 +14,7 @@ import { useTranslation } from 'react-i18next';
import { useLyrics, type WordLyricsLine } from '../hooks/useLyrics';
import { useAuthStore } from '../store/authStore';
import type { LrcLine } from '../api/lrclib';
import type { Track } from '../store/playerStore';
import type { Track } from '../store/playerStoreTypes';
import { EaseScroller, targetForFraction } from '../utils/easeScroll';
import { usePlaybackDelayPress } from '../hooks/usePlaybackDelayPress';
import PlaybackDelayModal from './PlaybackDelayModal';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus } from 'lucide-react';
import { getRandomAlbums, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey, getAlbum } from '../api/subsonic';
import CachedImage, { useCachedUrl } from './CachedImage';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { playAlbum } from '../utils/playAlbum';
import { useIsMobile } from '../hooks/useIsMobile';
+2 -1
View File
@@ -1,8 +1,9 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Search, Disc3, Users, Music, TextSearch } from 'lucide-react';
import { search, SearchResults, buildCoverArtUrl, coverArtCacheKey, type SubsonicArtist } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
import CachedImage, { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from './CachedImage';
+3 -2
View File
@@ -1,11 +1,12 @@
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import { useEffect, useRef, useCallback } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { usePlayerStore, getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import type { LrcLine } from '../api/lrclib';
import { useLyrics, type WordLyricsLine } from '../hooks/useLyrics';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
import type { Track } from '../store/playerStore';
import type { Track } from '../store/playerStoreTypes';
import { EaseScroller, targetForFraction } from '../utils/easeScroll';
import OverlayScrollArea from './OverlayScrollArea';
+3 -1
View File
@@ -1,3 +1,5 @@
import type { Track } from '../store/playerStoreTypes';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useState, useCallback, useMemo, useRef, useEffect, useSyncExternalStore, CSSProperties } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
@@ -6,7 +8,7 @@ import {
Shuffle, Repeat, Repeat1, Heart, Music, MicVocal, ListMusic, X,
Moon, Sunrise,
} from 'lucide-react';
import { usePlayerStore, getPlaybackProgressSnapshot, subscribePlaybackProgress, Track } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { buildCoverArtUrl, coverArtCacheKey, star, unstar } from '../api/subsonic';
import { useCachedUrl } from './CachedImage';
import LyricsPane from './LyricsPane';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { X, Search, Disc3, Users, Music, Music2, Clock, ChevronRight } from 'lucide-react';
import { search, SearchResults, buildCoverArtUrl, coverArtCacheKey, type SubsonicArtist } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useTranslation } from 'react-i18next';
import CachedImage, { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from './CachedImage';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import { useEffect, useRef, useState } from 'react';
import { X, RefreshCw, Shuffle, Settings2, Share2, HelpCircle, Activity } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore';
import { useHelpModalStore } from '../store/helpModalStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { getSong } from '../api/subsonic';
import {
endOrbitSession,
+2 -1
View File
@@ -1,3 +1,4 @@
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
@@ -6,7 +7,7 @@ import {
PictureInPicture2, ArrowLeftRight, Moon, Sunrise, Ellipsis,
} from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { usePlayerStore, getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { useAuthStore } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
+4 -7
View File
@@ -1,12 +1,9 @@
import { registerQueueListScrollTopReader, consumePendingQueueListScrollTop } from '../store/queueUndo';
import { songToTrack } from '../utils/songToTrack';
import type { Track } from '../store/playerStoreTypes';
import React, { useState, useRef, useMemo, useEffect, useLayoutEffect } from 'react';
import { createPortal } from 'react-dom';
import {
Track,
usePlayerStore,
songToTrack,
registerQueueListScrollTopReader,
consumePendingQueueListScrollTop,
} from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useOrbitStore } from '../store/orbitStore';
import OrbitGuestQueue from './OrbitGuestQueue';
import OrbitQueueHead from './OrbitQueueHead';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, Star } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { SubsonicSong, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from './CachedImage';
import { enqueueAndPlay } from '../utils/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import type { SubsonicSong } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { enqueueAndPlay } from '../utils/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
+2 -1
View File
@@ -1,5 +1,6 @@
import { getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playbackProgress';
import React, { useEffect, useRef, useState } from 'react';
import { usePlayerStore, getPlaybackProgressSnapshot, subscribePlaybackProgress } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useAuthStore, type SeekbarStyle } from '../store/authStore';
import { bumpPerfCounter } from '../utils/perfTelemetry';
+2 -1
View File
@@ -1,8 +1,9 @@
import { songToTrack } from '../utils/songToTrack';
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';
import i18n from '../i18n';
import { getSong, setRating, star, unstar } from '../api/subsonic';
import { songToTrack, usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useLyricsStore } from '../store/lyricsStore';
import { showToast } from '../utils/toast';
+1 -2
View File
@@ -1,3 +1,4 @@
import type { Track } from '../store/playerStoreTypes';
import { useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { invoke } from '@tauri-apps/api/core';
@@ -9,8 +10,6 @@ import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
import { useHotCacheStore } from '../store/hotCacheStore';
import { getCachedLyrics, putCachedLyrics, lyricsCacheKey } from '../utils/lyricsPersistentCache';
import type { Track } from '../store/playerStore';
export type LyricsSource = 'server' | 'lrclib' | 'netease' | 'embedded' | 'lyricsplus';
/**
+2 -1
View File
@@ -1,7 +1,8 @@
import { songToTrack } from '../utils/songToTrack';
import { useEffect, useRef } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { getSong } from '../api/subsonic';
import {
readOrbitState,
+2 -1
View File
@@ -1,6 +1,7 @@
import { songToTrack } from '../utils/songToTrack';
import { useEffect, useRef } from 'react';
import { useOrbitStore } from '../store/orbitStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { getSong } from '../api/subsonic';
import {
writeOrbitState,
+2 -1
View File
@@ -1,9 +1,10 @@
import type { Track } from './store/playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from './api/subsonic';
import { useAuthStore } from './store/authStore';
import { HOT_CACHE_PROTECT_AFTER_CURRENT, useHotCacheStore, type HotCacheEntry } from './store/hotCacheStore';
import { useOfflineStore } from './store/offlineStore';
import { usePlayerStore, type Track } from './store/playerStore';
import { usePlayerStore } from './store/playerStore';
import {
bumpHotCachePreviousTrackGrace,
clearHotCachePreviousGrace,
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Search, X, ListPlus } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { getAlbum, getArtist, getArtistInfo, setRating, buildCoverArtUrl, coverArtCacheKey, buildDownloadUrl, star, unstar, SubsonicSong, SubsonicAlbum } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { useDownloadModalStore } from '../store/downloadModalStore';
+1 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useState, useEffect, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
import AlbumCard from '../components/AlbumCard';
import GenreFilterBar from '../components/GenreFilterBar';
@@ -5,7 +6,6 @@ import YearFilterButton from '../components/YearFilterButton';
import StarFilterButton from '../components/StarFilterButton';
import SortDropdown from '../components/SortDropdown';
import { getAlbumList, getAlbumsByGenre, getAlbum, SubsonicAlbum, buildDownloadUrl } from '../api/subsonic';
import { songToTrack } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import { useEffect, useState, useRef, Fragment, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, getAlbum, search, setRating, SubsonicArtist, SubsonicAlbum, SubsonicSong, SubsonicArtistInfo, buildCoverArtUrl, coverArtCacheKey, star, unstar, uploadArtistImage } from '../api/subsonic';
@@ -8,7 +9,7 @@ import { ArrowLeft, Users, ExternalLink, Heart, Play, Square, Shuffle, Radio, Ha
import { useIsMobile } from '../hooks/useIsMobile';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { open } from '@tauri-apps/plugin-shell';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useOfflineStore } from '../store/offlineStore';
import { useOfflineJobStore } from '../store/offlineJobStore';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns';
import AlbumRow from '../components/AlbumRow';
@@ -8,7 +9,7 @@ import {
SubsonicAlbum, SubsonicArtist, SubsonicSong, InternetRadioStation,
buildCoverArtUrl, coverArtCacheKey,
} from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import StarRating from '../components/StarRating';
import { Cast, ChevronDown, ChevronLeft, ChevronRight, Check, Heart, ListPlus, Play, Square, Star, Users, X, SlidersHorizontal, ArrowUp, ArrowDown, RotateCcw, AudioLines } from 'lucide-react';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { Track } from '../store/playerStoreTypes';
import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import {
getMusicFolders,
@@ -7,7 +8,7 @@ import {
SubsonicArtist,
SubsonicAlbum,
} from '../api/subsonic';
import { usePlayerStore, Track } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useTranslation } from 'react-i18next';
import { Folder, FolderOpen, Music, ChevronRight } from 'lucide-react';
import { useLocation } from 'react-router-dom';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import AlbumCard from '../components/AlbumCard';
import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse';
@@ -6,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore';
import { useOfflineStore } from '../store/offlineStore';
import { useDownloadModalStore } from '../store/downloadModalStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useZipDownloadStore } from '../store/zipDownloadStore';
import { useRangeSelection } from '../hooks/useRangeSelection';
import { usePerfProbeFlags } from '../utils/perfFlags';
+2 -1
View File
@@ -1,9 +1,10 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound, Play, ListPlus } from 'lucide-react';
import { getAlbumList, getAlbum, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';
import { playAlbum } from '../utils/playAlbum';
import { useTranslation } from 'react-i18next';
+1 -1
View File
@@ -11,7 +11,7 @@ import {
getAlbum, getArtist, getArtistInfo, getTopSongs,
SubsonicSong, SubsonicArtistInfo, SubsonicAlbum,
} from '../api/subsonic';
import { songToTrack } from '../store/playerStore';
import { songToTrack } from '../utils/songToTrack';
import {
lastfmIsConfigured,
lastfmGetTrackInfo, lastfmGetArtistStats,
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
@@ -9,7 +10,7 @@ import {
search, setRating, star, unstar,
getRandomSongs, buildDownloadUrl, filterSongsToActiveLibrary, SubsonicPlaylist, SubsonicSong,
} from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useShallow } from 'zustand/react/shallow';
import { usePlaylistStore } from '../store/playlistStore';
import { usePreviewStore } from '../store/previewStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { ListMusic, Play, Plus, Trash2, CheckSquare2, Check, Clock3, Sparkles, Pencil } from 'lucide-react';
import { deletePlaylist, SubsonicPlaylist, getPlaylist, buildCoverArtUrl, coverArtCacheKey, updatePlaylist, getGenres, SubsonicGenre, filterSongsToActiveLibrary } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePlaylistStore } from '../store/playlistStore';
import { useAuthStore } from '../store/authStore';
import CachedImage from '../components/CachedImage';
+2 -1
View File
@@ -1,6 +1,7 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useMemo, useState } from 'react';
import { getGenres, SubsonicSong, SubsonicGenre, star, unstar } from '../api/subsonic';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { usePreviewStore } from '../store/previewStore';
import { useAuthStore, RANDOM_MIX_SIZE_OPTIONS } from '../store/authStore';
import { Play, RefreshCw, ChevronDown, ChevronRight, ChevronUp, Heart, Square, AudioLines } from 'lucide-react';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import React, { useEffect, useState, useCallback, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Play, ListPlus, RefreshCw, Sparkles } from 'lucide-react';
@@ -9,7 +10,7 @@ import {
coverArtCacheKey,
} from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import CachedImage from '../components/CachedImage';
import SongRail from '../components/SongRail';
import VirtualSongList from '../components/VirtualSongList';
+2 -1
View File
@@ -1,3 +1,4 @@
import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { lastfmGetTrackLoved, lastfmScrobble, lastfmUpdateNowPlaying } from '../api/lastfm';
import { reportNowPlaying, scrobbleSong } from '../api/subsonic';
@@ -44,7 +45,7 @@ import {
import {
playbackSourceHintForResolvedUrl,
} from './playbackUrlRouting';
import { usePlayerStore, type Track } from './playerStore';
import { usePlayerStore } from './playerStore';
import { promoteCompletedStreamToHotCache } from './promoteStreamCache';
import {
flushQueueSyncToServer,
+1 -1
View File
@@ -1,8 +1,8 @@
import type { Track } from './playerStoreTypes';
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core';
import { isHotCachePreviousTrackUnderGrace } from '../utils/hotCacheGate';
import type { Track } from './playerStore';
import { emitAnalysisStorageChanged } from './analysisSync';
import { useAuthStore } from './authStore';
+5 -17
View File
@@ -12,23 +12,11 @@ type SetState = (
type GetState = () => PlayerState;
/**
* Four Last.fm love-related actions, factored out of the playerStore
* `create()` body so the action set can be tested + reasoned about
* separately:
*
* - `toggleLastfmLove` flip the current track's love state on the
* server, write through to the local cache map keyed by
* `${title}::${artist}` so other queue rows showing the same song
* update too.
* - `setLastfmLoved` force-set the boolean (used by the
* `track:lastfm-loved` SSE-style event). Updates the cache when a
* current track exists.
* - `setLastfmLovedForSong` write the cache for an arbitrary
* title/artist pair (used by the QueuePanel love button on
* not-yet-current tracks).
* - `syncLastfmLovedTracks` startup-time bulk fetch of the user's
* loved-tracks list, merged into the local cache (local likes win
* on conflict) plus a recompute of the current track's flag.
* Four Last.fm love-related actions. The `lastfmLovedCache` is a map
* keyed by `${title}::${artist}` (not by track id) so other queue rows
* showing the same song update too when one is loved/unloved.
* `syncLastfmLovedTracks` merges the server's loved list with local
* cache local likes win on conflict.
*/
export function createLastfmActions(set: SetState, get: GetState): Pick<
PlayerState,
+1 -1
View File
@@ -4,8 +4,8 @@
* = current track + next `LOUDNESS_BACKFILL_WINDOW_AHEAD` entries, with
* duplicates collapsed.
*/
import type { Track } from './playerStoreTypes';
import { describe, expect, it } from 'vitest';
import type { Track } from './playerStore';
import {
LOUDNESS_BACKFILL_WINDOW_AHEAD,
collectLoudnessBackfillWindowTrackIds,
+1 -2
View File
@@ -1,5 +1,4 @@
import type { Track } from './playerStore';
import type { Track } from './playerStoreTypes';
/**
* After a bulk enqueue (queue replace, append-many, lucky-mix) the runtime
* warms the loudness cache for the current track + the next N entries so
+1 -2
View File
@@ -4,9 +4,8 @@
* guard, the window collection, and the no-sync-engine flag on each
* refresh call.
*/
import type { Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const hoisted = vi.hoisted(() => {
const auth = { normalizationEngine: 'loudness' as 'off' | 'replaygain' | 'loudness' };
const player = { currentTrack: null as Track | null };
+2 -2
View File
@@ -1,8 +1,8 @@
import type { Track } from './playerStoreTypes';
import { useAuthStore } from './authStore';
import { collectLoudnessBackfillWindowTrackIds } from './loudnessBackfillWindow';
import { refreshLoudnessForTrack } from './loudnessRefresh';
import { usePlayerStore, type Track } from './playerStore';
import { usePlayerStore } from './playerStore';
/**
* After a bulk enqueue (queue replace, append-many, lucky-mix) warm the
* loudness cache for the current track + the next N entries so the
+1 -2
View File
@@ -4,9 +4,8 @@
* album-mode resolution). useAuthStore is mocked through a hoisted state
* object so each test can flip flags without rebuilding the store.
*/
import type { Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const { authState } = vi.hoisted(() => ({
authState: {
normalizationEngine: 'off' as 'off' | 'replaygain' | 'loudness',
+1 -2
View File
@@ -1,7 +1,6 @@
import type { PlayerState, Track } from './playerStoreTypes';
import { useAuthStore } from './authStore';
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
import type { PlayerState, Track } from './playerStore';
/**
* Compute the normalization fields that should land in the next state commit
* when the runtime switches tracks or rewrites the queue. Three branches:
+2 -1
View File
@@ -6,6 +6,7 @@
* regression test from §4.2 of the pre-refactor testing plan v2 the
* cleanup function returned by `initAudioListeners` must actually unsub.
*/
import { initAudioListeners } from './initAudioListeners';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/api/subsonic', async () => {
@@ -40,7 +41,7 @@ vi.mock('@/utils/orbitBulkGuard', () => ({
orbitBulkGuard: vi.fn(async () => true),
}));
import { initAudioListeners, usePlayerStore } from './playerStore';
import { usePlayerStore } from './playerStore';
import {
emitTauriEvent,
onInvoke,
+3 -5
View File
@@ -8,6 +8,8 @@
* Mocks `savePlayQueue` at the module boundary so we can assert the exact
* args passed to the Subsonic API call.
*/
import { initAudioListeners } from './initAudioListeners';
import { flushPlayQueuePosition } from './queueSync';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Explicit (non-spread) mock map — the `...actual` spread pattern lets the
@@ -40,11 +42,7 @@ vi.mock('@/api/lastfm', () => ({
}));
import { savePlayQueue } from '@/api/subsonic';
import {
flushPlayQueuePosition,
initAudioListeners,
usePlayerStore,
} from './playerStore';
import { usePlayerStore } from './playerStore';
import { emitTauriEvent, onInvoke } from '@/test/mocks/tauri';
import { resetPlayerStore, resetAuthStore } from '@/test/helpers/storeReset';
import { makeTrack, makeTracks } from '@/test/helpers/factories';
+3 -7
View File
@@ -10,6 +10,8 @@
* Drive emits via the `audio:progress` Tauri event (the only public path
* to `emitPlaybackProgress`).
*/
import { initAudioListeners } from './initAudioListeners';
import { getPlaybackProgressSnapshot, subscribePlaybackProgress, type PlaybackProgressSnapshot } from './playbackProgress';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('@/api/subsonic', async () => {
@@ -36,13 +38,7 @@ vi.mock('@/api/lastfm', () => ({
lastfmGetAllLovedTracks: vi.fn(async () => []),
}));
import {
getPlaybackProgressSnapshot,
initAudioListeners,
subscribePlaybackProgress,
usePlayerStore,
type PlaybackProgressSnapshot,
} from './playerStore';
import { usePlayerStore } from './playerStore';
import { emitTauriEvent, onInvoke } from '@/test/mocks/tauri';
import { resetPlayerStore, resetAuthStore } from '@/test/helpers/storeReset';
import { makeTrack } from '@/test/helpers/factories';
+2 -71
View File
@@ -1,62 +1,8 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
import { shuffleArray } from '../utils/shuffleArray';
import { songToTrack } from '../utils/songToTrack';
import {
emitPlaybackProgress,
getPlaybackProgressSnapshot,
subscribePlaybackProgress,
type PlaybackProgressSnapshot,
} from './playbackProgress';
import { flushPlayQueuePosition } from './queueSync';
import { initAudioListeners } from './initAudioListeners';
import { installQueueUndoHotkey } from './queueUndoHotkey';
import { emitPlaybackProgress } from './playbackProgress';
import type { PlayerState } from './playerStoreTypes';
import { readInitialQueueVisibility } from './queueVisibilityStorage';
// Re-export so MainApp + the 3 playerStore characterization tests keep
// their existing `from './playerStore'` imports.
export { initAudioListeners };
// Re-export so bootstrap.ts + bootstrap.test keep their existing
// `from './playerStore'` imports.
export { installQueueUndoHotkey };
// Re-export so TauriEventBridge + persistence test keep their existing
// `from './playerStore'` imports.
export { flushPlayQueuePosition };
// Re-export the playback-progress public surface so existing call sites
// (PlayerBar, FullscreenPlayer, WaveformSeek, LyricsPane, MobilePlayerView,
// TauriEventBridge, plus the progress characterization test) keep their
// `from './playerStore'` imports working.
export {
getPlaybackProgressSnapshot,
subscribePlaybackProgress,
type PlaybackProgressSnapshot,
};
import {
_resetQueueUndoStacksForTest,
consumePendingQueueListScrollTop,
pushQueueUndoFromGetter,
registerQueueListScrollTopReader,
} from './queueUndo';
// Re-export for backward compatibility with the ~30 call sites that still
// import these helpers from playerStore. Phase E (store splits) will migrate
// the imports to '../utils/*' directly and drop these re-exports.
export { resolveReplayGainDb, shuffleArray, songToTrack };
// Re-export the queue-undo public API so existing callers (QueuePanel,
// test/helpers/storeReset) keep their `from './playerStore'` imports.
export {
_resetQueueUndoStacksForTest,
consumePendingQueueListScrollTop,
registerQueueListScrollTopReader,
};
import type { PlayerState, Track } from './playerStoreTypes';
export type { PlayerState, Track };
import { createLastfmActions } from './lastfmActions';
import { createMiscActions } from './miscActions';
import { runNext } from './nextAction';
@@ -70,12 +16,6 @@ import { createTransportLightActions } from './transportLightActions';
import { createUiStateActions } from './uiStateActions';
import { createUndoRedoActions } from './undoRedoActions';
// ─── Module-level playback primitives ─────────────────────────────────────────
// ─── Store ────────────────────────────────────────────────────────────────────
export const usePlayerStore = create<PlayerState>()(
persist(
(set, get) => {
@@ -125,20 +65,11 @@ export const usePlayerStore = create<PlayerState>()(
...createMiscActions(set, get),
...createScheduleActions(set, get),
// ── playTrack ────────────────────────────────────────────────────────────
playTrack: (track, queue, manual = true, _orbitConfirmed = false, targetQueueIndex) =>
runPlayTrack(set, get, track, queue, manual, _orbitConfirmed, targetQueueIndex),
// ── resume ───────────────────────────────────────────────────────────────
resume: () => runResume(set, get),
// ── next ────────────────────────────────────────────────────────────────
next: (manual = true) => runNext(set, get, manual),
// ── seek ─────────────────────────────────────────────────────────────────
seek: (progress) => runSeek(set, get, progress),
updateReplayGainForCurrentTrack: () => runUpdateReplayGainForCurrentTrack(set, get),
};
},
+1 -2
View File
@@ -4,9 +4,8 @@
* suffix fallback, the null-result skip, and the swallow-on-error
* contract.
*/
import type { Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const { invokeMock, setEntryMock, buildStreamUrlMock } = vi.hoisted(() => ({
invokeMock: vi.fn(async (_cmd: string, _args?: Record<string, unknown>) => null as { path: string; size: number } | null),
setEntryMock: vi.fn(),
+1 -2
View File
@@ -1,8 +1,7 @@
import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { buildStreamUrl } from '../api/subsonic';
import { useHotCacheStore } from './hotCacheStore';
import type { Track } from './playerStore';
/**
* Promote a track whose stream cache is full to the on-disk hot cache.
* Rust copies the cached bytes into the hot-cache directory and returns
+2 -6
View File
@@ -24,12 +24,8 @@ type SetState = (
type GetState = () => PlayerState;
/**
* Queue-mutation actions factored out of the playerStore `create()` body.
* All eleven members of the cluster: insertion (`enqueue`, `enqueueAt`,
* `playNext`, `enqueueRadio`, `setRadioArtistId`), pruning (`clearQueue`,
* `pruneUpcomingToCurrent`, `removeTrack`), and rearrangement
* (`reorderQueue`, `shuffleQueue`, `shuffleUpcomingQueue`). All but
* `setRadioArtistId` push a queue-undo snapshot and call
* Eleven queue-mutation actions. Shared invariant: every action except
* `setRadioArtistId` pushes a queue-undo snapshot and calls
* `syncQueueToServer` so the Navidrome `savePlayQueue` stays in sync.
*/
export function createQueueMutationActions(set: SetState, get: GetState): Pick<
+1 -2
View File
@@ -5,9 +5,8 @@
* in for `savePlayQueue`, the playerStore, and the playback-progress
* snapshot.
*/
import type { Track } from './playerStoreTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const { savePlayQueueMock, playerState, progressSnapshot } = vi.hoisted(() => ({
savePlayQueueMock: vi.fn(async (_ids: string[], _currentId: string | undefined, _pos: number) => undefined),
playerState: {
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Track } from './playerStoreTypes';
import { savePlayQueue } from '../api/subsonic';
import { getPlaybackProgressSnapshot } from './playbackProgress';
import { usePlayerStore, type Track } from './playerStore';
import { usePlayerStore } from './playerStore';
/**
* Server-side play-queue persistence. Subsonic's `savePlayQueue` accepts
* the current queue, the active track id, and the position in ms so the
+1 -1
View File
@@ -4,8 +4,8 @@
* push, and (c) the scroll-top reader / consumer pair that QueuePanel uses
* to restore list scroll position after an undo/redo commit.
*/
import type { PlayerState, Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it } from 'vitest';
import type { PlayerState, Track } from './playerStore';
import {
QUEUE_UNDO_MAX,
_resetQueueUndoStacksForTest,
+1 -2
View File
@@ -1,5 +1,4 @@
import type { PlayerState, Track } from './playerStore';
import type { PlayerState, Track } from './playerStoreTypes';
/** Hard cap on undo/redo depth — keeps memory bounded for very long sessions. */
export const QUEUE_UNDO_MAX = 32;
+1 -2
View File
@@ -4,9 +4,8 @@
* atSeconds > 0.05, the wantPlaying=false branch that issues audio_pause,
* and the generation-mismatch bail-out.
*/
import type { Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const hoisted = vi.hoisted(() => {
const auth = {
activeServerId: 'srv',
+2 -2
View File
@@ -1,3 +1,4 @@
import type { Track } from './playerStoreTypes';
import { invoke } from '@tauri-apps/api/core';
import { setDeferHotCachePrefetch } from '../utils/hotCacheGate';
import { resolvePlaybackUrl } from '../utils/resolvePlaybackUrl';
@@ -7,8 +8,7 @@ import { getPlayGeneration, setIsAudioPaused } from './engineState';
import { touchHotCacheOnPlayback } from './hotCacheTouch';
import { isReplayGainActive, loudnessGainDbForEngineBind } from './loudnessGainCache';
import { playbackSourceHintForResolvedUrl, recordEnginePlayUrl } from './playbackUrlRouting';
import { usePlayerStore, type Track } from './playerStore';
import { usePlayerStore } from './playerStore';
/**
* Reload the Rust audio engine to match a queue-undo snapshot. Zustand
* alone can rewrite the queue + currentTrack, but the engine is still
+1 -2
View File
@@ -5,9 +5,8 @@
* the player-store state surface so the test can drive every input
* independently.
*/
import type { Track } from './playerStoreTypes';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { Track } from './playerStore';
const { setRatingMock, recordSkipStarMock, playerSetStateMock, playerStateGet } = vi.hoisted(() => {
const playerState = {
queue: [] as Track[],
+2 -2
View File
@@ -1,7 +1,7 @@
import type { Track } from './playerStoreTypes';
import { setRating } from '../api/subsonic';
import { useAuthStore } from './authStore';
import { usePlayerStore, type Track } from './playerStore';
import { usePlayerStore } from './playerStore';
/**
* Skip 1 behaviour: every user-initiated `next()` on an unrated track
* counts in `authStore.skipStarManualSkipCountsByKey` (persisted). Once the
+6 -12
View File
@@ -15,18 +15,12 @@ type SetState = (
type GetState = () => PlayerState;
/**
* Light transport actions factored out of the playerStore `create()`
* body everything except `resume` (~165 LOC, separate PR) and the
* scheduled pause/resume timer setters.
*
* - `stop` full reset: stops audio/radio, clears timers + seek + visual
* state, blanks playback metadata.
* - `pause` pauses audio (or radio), flushes queue position so other
* devices can pick up the resume point.
* - `resetAudioPause` flips the engine-paused flag without touching
* the UI `isPlaying` state. Used by `audio:ended` paths.
* - `togglePlay` guarded toggle so a double media-key tap can't race
* pause + resume into a stuck state.
* Light transport actions everything except `resume` (own module,
* see `resumeAction.ts`) and scheduled timers (`scheduleActions.ts`).
* `togglePlay` is guarded so a double media-key tap can't race
* pause + resume into a stuck state. `resetAudioPause` flips the
* engine-paused flag without touching the UI `isPlaying`, used by
* `audio:ended` paths.
*/
export function createTransportLightActions(set: SetState, get: GetState): Pick<
PlayerState,
+2 -7
View File
@@ -8,13 +8,8 @@ type SetState = (
) => void;
/**
* Pure-UI state setters that mutate playerStore fields with no audio
* engine / network side effects: starred + rating optimistic overrides,
* context menu modal, song info modal, queue panel visibility (with
* localStorage round-trip), fullscreen toggle, repeat mode cycle.
*
* Factored out of the playerStore `create()` body so the action set
* stays a flat object literal instead of a giant inline block.
* Pure-UI state setters: no audio engine / network side effects.
* Add new actions here only if they fit that contract.
*/
export function createUiStateActions(set: SetState): Pick<
PlayerState,
+1 -1
View File
@@ -5,7 +5,7 @@
* the fields the test cares about. Keeps tests focused on behaviour rather
* than on assembling boilerplate.
*/
import type { Track } from '@/store/playerStore';
import type { Track } from '@/store/playerStoreTypes';
import type { ServerProfile } from '@/store/authStore';
import type { SubsonicSong } from '@/api/subsonic';
+2 -1
View File
@@ -20,7 +20,8 @@
* // or for cross-store tests:
* beforeEach(resetAllStores);
*/
import { usePlayerStore, _resetQueueUndoStacksForTest } from '@/store/playerStore';
import { _resetQueueUndoStacksForTest } from '@/store/queueUndo';
import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '@/store/authStore';
import { usePreviewStore } from '@/store/previewStore';
import { useOrbitStore } from '@/store/orbitStore';
+2 -1
View File
@@ -1,8 +1,9 @@
import { songToTrack } from '../utils/songToTrack';
import type { NavigateFunction } from 'react-router-dom';
import type { TFunction } from 'i18next';
import { getAlbum, getArtist, getSong, type SubsonicSong } from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { songToTrack, usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { findServerIdForShareUrl, type EntitySharePayloadV1 } from './shareLink';
import { showToast } from './toast';
@@ -6,6 +6,7 @@
* refactor (2026-05-12). This test pins the artist-first / random-fallback
* order, the dedup contract against existingIds, and the autoAdded flag.
*/
import type { Track } from '../store/playerStoreTypes';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
vi.mock('../api/subsonic', () => ({
@@ -26,7 +27,6 @@ import {
enrichSongsForMixRatingFilter,
getMixMinRatingsConfigFromAuth,
} from './mixRatingFilter';
import type { Track } from '../store/playerStore';
import { makeSubsonicSong } from '@/test/helpers/factories';
const seed = (overrides: Partial<Track> = {}): Track => ({
+1 -2
View File
@@ -1,3 +1,4 @@
import type { Track } from '../store/playerStoreTypes';
import { getRandomSongs, getSimilarSongs2, getTopSongs } from '../api/subsonic';
import {
enrichSongsForMixRatingFilter,
@@ -6,8 +7,6 @@ import {
} from './mixRatingFilter';
import { shuffleArray } from './shuffleArray';
import { songToTrack } from './songToTrack';
import type { Track } from '../store/playerStore';
/**
* Infinite queue source strategy (Instant Mix-like):
* 1) Prefer artist-driven candidates (Top + Similar) around the current track.
+3 -1
View File
@@ -1,3 +1,5 @@
import type { Track } from '../store/playerStoreTypes';
import { songToTrack } from '../utils/songToTrack';
import {
filterSongsToActiveLibrary,
getAlbum,
@@ -11,7 +13,7 @@ import {
import { invoke } from '@tauri-apps/api/core';
import i18n from '../i18n';
import { useAuthStore } from '../store/authStore';
import { songToTrack, usePlayerStore, type Track } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { useLuckyMixStore } from '../store/luckyMixStore';
import { isLuckyMixAvailable } from '../hooks/useLuckyMixAvailable';
import { showToast } from './toast';
+2 -1
View File
@@ -1,3 +1,4 @@
import { songToTrack } from '../utils/songToTrack';
import {
createPlaylist,
updatePlaylist,
@@ -9,7 +10,7 @@ import {
} from '../api/subsonic';
import { useAuthStore } from '../store/authStore';
import { useOrbitStore } from '../store/orbitStore';
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
import { encodeSharePayload, decodeOrbitSharePayloadFromText } from './shareLink';
import {
makeInitialOrbitState,
+1 -1
View File
@@ -1,6 +1,6 @@
import { getAlbum } from '../api/subsonic';
import { usePlayerStore } from '../store/playerStore';
import { songToTrack } from '../store/playerStore';
import { songToTrack } from './songToTrack';
import { useOrbitStore } from '../store/orbitStore';
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
+3 -2
View File
@@ -1,6 +1,7 @@
import { songToTrack } from '../utils/songToTrack';
import { shuffleArray } from '../utils/shuffleArray';
import { getAlbum, getArtist } from '../api/subsonic';
import { shuffleArray, songToTrack, usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
/**
* All tracks from the artists albums, shuffled same idea as Artist page shuffle play.
*/
+2 -2
View File
@@ -1,8 +1,8 @@
import { songToTrack } from '../utils/songToTrack';
import { getAlbum, getSong } from '../api/subsonic';
import { playAlbum } from './playAlbum';
import { playArtistShuffled } from './playArtistShuffled';
import { songToTrack, usePlayerStore } from '../store/playerStore';
import { usePlayerStore } from '../store/playerStore';
/**
* `getSong` `getAlbum` `getArtist`: one opaque Subsonic id may refer to a track,
* album, or artist depending on the server.
+2 -1
View File
@@ -1,4 +1,5 @@
import { usePlayerStore, songToTrack } from '../store/playerStore';
import { songToTrack } from '../utils/songToTrack';
import { usePlayerStore } from '../store/playerStore';
import type { SubsonicSong } from '../api/subsonic';
function fadeOut(setVolume: (v: number) => void, from: number, durationMs: number): Promise<void> {
+1 -1
View File
@@ -4,8 +4,8 @@
* routing through the HTTP source) and the no-op detection in
* `queuesStructuralEqual` that prevents unnecessary store rewrites.
*/
import type { Track } from '../store/playerStoreTypes';
import { describe, expect, it } from 'vitest';
import type { Track } from '../store/playerStore';
import {
normalizeAnalysisTrackId,
queuesStructuralEqual,
+1 -2
View File
@@ -1,5 +1,4 @@
import type { Track } from '../store/playerStore';
import type { Track } from '../store/playerStoreTypes';
/**
* Strip the `stream:` prefix that some Rust events attach to track ids when
* they're routed through the HTTP source. Both forms identify the same track,
+1 -2
View File
@@ -5,10 +5,9 @@
* Originally lived in `playerStore.ts`; extracted in M0 of the frontend
* refactor (2026-05-12).
*/
import type { Track } from '../store/playerStoreTypes';
import { describe, expect, it } from 'vitest';
import { resolveReplayGainDb } from './resolveReplayGainDb';
import type { Track } from '../store/playerStore';
describe('resolveReplayGainDb', () => {
const t = (overrides: Partial<Track> = {}): Track => ({
id: 'x',
+1 -2
View File
@@ -1,5 +1,4 @@
import type { Track } from '../store/playerStore';
import type { Track } from '../store/playerStoreTypes';
/**
* Resolve the ReplayGain dB value for a track based on the configured mode.
* In 'auto' mode, picks album-gain when an adjacent queue neighbour shares the
+1 -1
View File
@@ -5,9 +5,9 @@
* playback queue. Originally lived in `playerStore.ts`; extracted in M0 of
* the frontend refactor (2026-05-12).
*/
import type { Track } from '../store/playerStoreTypes';
import { describe, expect, it } from 'vitest';
import { songToTrack } from './songToTrack';
import type { Track } from '../store/playerStore';
import { makeSubsonicSong } from '@/test/helpers/factories';
describe('songToTrack', () => {
+1 -2
View File
@@ -1,6 +1,5 @@
import type { Track } from '../store/playerStoreTypes';
import type { SubsonicSong } from '../api/subsonic';
import type { Track } from '../store/playerStore';
export function songToTrack(song: SubsonicSong): Track {
return {
id: song.id,