diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 88b58c91..e160bf6a 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -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'; diff --git a/src/app/TauriEventBridge.tsx b/src/app/TauriEventBridge.tsx index 3210e3b4..56e57783 100644 --- a/src/app/TauriEventBridge.tsx +++ b/src/app/TauriEventBridge.tsx @@ -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'; diff --git a/src/app/bootstrap.test.ts b/src/app/bootstrap.test.ts index 86ef0140..7b16f5ae 100644 --- a/src/app/bootstrap.test.ts +++ b/src/app/bootstrap.test.ts @@ -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, diff --git a/src/app/bootstrap.ts b/src/app/bootstrap.ts index 42a6ea0e..736ce86a 100644 --- a/src/app/bootstrap.ts +++ b/src/app/bootstrap.ts @@ -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. */ diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index e7d2e77d..da8fda35 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -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'; diff --git a/src/components/AlbumTrackList.tsx b/src/components/AlbumTrackList.tsx index b97eeb0d..a6bc7851 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/components/AlbumTrackList.tsx @@ -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'; diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx index 95c7a5c3..c7235fff 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/components/BecauseYouLikeRail.tsx @@ -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'; diff --git a/src/components/ContextMenu.test.tsx b/src/components/ContextMenu.test.tsx index 3fed2a3c..59b99f63 100644 --- a/src/components/ContextMenu.test.tsx +++ b/src/components/ContextMenu.test.tsx @@ -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'; diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index 03ecfcfb..8b9bec57 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -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'; diff --git a/src/components/FullscreenPlayer.tsx b/src/components/FullscreenPlayer.tsx index 25f8e5b5..18717b04 100644 --- a/src/components/FullscreenPlayer.tsx +++ b/src/components/FullscreenPlayer.tsx @@ -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'; diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index e8505ace..1428c018 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -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'; diff --git a/src/components/LiveSearch.tsx b/src/components/LiveSearch.tsx index a864e955..31731876 100644 --- a/src/components/LiveSearch.tsx +++ b/src/components/LiveSearch.tsx @@ -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'; diff --git a/src/components/LyricsPane.tsx b/src/components/LyricsPane.tsx index 415598fa..c11e95cc 100644 --- a/src/components/LyricsPane.tsx +++ b/src/components/LyricsPane.tsx @@ -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'; diff --git a/src/components/MobilePlayerView.tsx b/src/components/MobilePlayerView.tsx index bf5d2248..15582b7a 100644 --- a/src/components/MobilePlayerView.tsx +++ b/src/components/MobilePlayerView.tsx @@ -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'; diff --git a/src/components/MobileSearchOverlay.tsx b/src/components/MobileSearchOverlay.tsx index 90731a91..f18d68fd 100644 --- a/src/components/MobileSearchOverlay.tsx +++ b/src/components/MobileSearchOverlay.tsx @@ -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'; diff --git a/src/components/OrbitSessionBar.tsx b/src/components/OrbitSessionBar.tsx index cfcb61ff..e6303cee 100644 --- a/src/components/OrbitSessionBar.tsx +++ b/src/components/OrbitSessionBar.tsx @@ -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, diff --git a/src/components/PlayerBar.tsx b/src/components/PlayerBar.tsx index 387d262c..f1eb6173 100644 --- a/src/components/PlayerBar.tsx +++ b/src/components/PlayerBar.tsx @@ -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'; diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 6de1991c..a6b2705b 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -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'; diff --git a/src/components/SongCard.tsx b/src/components/SongCard.tsx index 53edbc2f..b2cb9c29 100644 --- a/src/components/SongCard.tsx +++ b/src/components/SongCard.tsx @@ -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'; diff --git a/src/components/SongRow.tsx b/src/components/SongRow.tsx index 36873eae..d694e424 100644 --- a/src/components/SongRow.tsx +++ b/src/components/SongRow.tsx @@ -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'; diff --git a/src/components/WaveformSeek.tsx b/src/components/WaveformSeek.tsx index b0abbf6f..8724c46e 100644 --- a/src/components/WaveformSeek.tsx +++ b/src/components/WaveformSeek.tsx @@ -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'; diff --git a/src/config/shortcutActions.ts b/src/config/shortcutActions.ts index 37aae0e8..c2fc6903 100644 --- a/src/config/shortcutActions.ts +++ b/src/config/shortcutActions.ts @@ -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'; diff --git a/src/hooks/useLyrics.ts b/src/hooks/useLyrics.ts index 22fdfc7d..4dc37304 100644 --- a/src/hooks/useLyrics.ts +++ b/src/hooks/useLyrics.ts @@ -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'; /** diff --git a/src/hooks/useOrbitGuest.ts b/src/hooks/useOrbitGuest.ts index 01625f5d..03e479fc 100644 --- a/src/hooks/useOrbitGuest.ts +++ b/src/hooks/useOrbitGuest.ts @@ -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, diff --git a/src/hooks/useOrbitHost.ts b/src/hooks/useOrbitHost.ts index de747241..4264f819 100644 --- a/src/hooks/useOrbitHost.ts +++ b/src/hooks/useOrbitHost.ts @@ -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, diff --git a/src/hotCachePrefetch.ts b/src/hotCachePrefetch.ts index a4e7ad28..ddc3f2ae 100644 --- a/src/hotCachePrefetch.ts +++ b/src/hotCachePrefetch.ts @@ -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, diff --git a/src/pages/AlbumDetail.tsx b/src/pages/AlbumDetail.tsx index ba6644c3..eb16f8de 100644 --- a/src/pages/AlbumDetail.tsx +++ b/src/pages/AlbumDetail.tsx @@ -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'; diff --git a/src/pages/Albums.tsx b/src/pages/Albums.tsx index d5109feb..cefbefee 100644 --- a/src/pages/Albums.tsx +++ b/src/pages/Albums.tsx @@ -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'; diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index efdbfd28..2f3e4b6c 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -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'; diff --git a/src/pages/Favorites.tsx b/src/pages/Favorites.tsx index a08e4959..6e5d0b7a 100644 --- a/src/pages/Favorites.tsx +++ b/src/pages/Favorites.tsx @@ -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'; diff --git a/src/pages/FolderBrowser.tsx b/src/pages/FolderBrowser.tsx index cef8174f..90aa07dc 100644 --- a/src/pages/FolderBrowser.tsx +++ b/src/pages/FolderBrowser.tsx @@ -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'; diff --git a/src/pages/LosslessAlbums.tsx b/src/pages/LosslessAlbums.tsx index bfb01626..55defc92 100644 --- a/src/pages/LosslessAlbums.tsx +++ b/src/pages/LosslessAlbums.tsx @@ -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'; diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index 20262c96..a480a3ed 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -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'; diff --git a/src/pages/NowPlaying.tsx b/src/pages/NowPlaying.tsx index 0b793345..bb74ee24 100644 --- a/src/pages/NowPlaying.tsx +++ b/src/pages/NowPlaying.tsx @@ -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, diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index a76873f0..5d811dd4 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -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'; diff --git a/src/pages/Playlists.tsx b/src/pages/Playlists.tsx index a93b16b3..4cc0032f 100644 --- a/src/pages/Playlists.tsx +++ b/src/pages/Playlists.tsx @@ -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'; diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index 59db7779..2248b69c 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -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'; diff --git a/src/pages/Tracks.tsx b/src/pages/Tracks.tsx index dad91dd9..30b597a4 100644 --- a/src/pages/Tracks.tsx +++ b/src/pages/Tracks.tsx @@ -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'; diff --git a/src/store/audioEventHandlers.ts b/src/store/audioEventHandlers.ts index af44af83..a5f70e2b 100644 --- a/src/store/audioEventHandlers.ts +++ b/src/store/audioEventHandlers.ts @@ -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, diff --git a/src/store/hotCacheStore.ts b/src/store/hotCacheStore.ts index 24a56bb6..90fef9d9 100644 --- a/src/store/hotCacheStore.ts +++ b/src/store/hotCacheStore.ts @@ -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'; diff --git a/src/store/lastfmActions.ts b/src/store/lastfmActions.ts index 084b85cd..d0ad1e14 100644 --- a/src/store/lastfmActions.ts +++ b/src/store/lastfmActions.ts @@ -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, diff --git a/src/store/loudnessBackfillWindow.test.ts b/src/store/loudnessBackfillWindow.test.ts index 0f6570e3..a69cdef5 100644 --- a/src/store/loudnessBackfillWindow.test.ts +++ b/src/store/loudnessBackfillWindow.test.ts @@ -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, diff --git a/src/store/loudnessBackfillWindow.ts b/src/store/loudnessBackfillWindow.ts index db431de7..8dcc2be6 100644 --- a/src/store/loudnessBackfillWindow.ts +++ b/src/store/loudnessBackfillWindow.ts @@ -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 diff --git a/src/store/loudnessPrefetch.test.ts b/src/store/loudnessPrefetch.test.ts index 51ffa357..0bb49546 100644 --- a/src/store/loudnessPrefetch.test.ts +++ b/src/store/loudnessPrefetch.test.ts @@ -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 }; diff --git a/src/store/loudnessPrefetch.ts b/src/store/loudnessPrefetch.ts index 9dfff334..dd084e1f 100644 --- a/src/store/loudnessPrefetch.ts +++ b/src/store/loudnessPrefetch.ts @@ -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 diff --git a/src/store/normalizationSnapshot.test.ts b/src/store/normalizationSnapshot.test.ts index 6128b5a0..3e1dc35d 100644 --- a/src/store/normalizationSnapshot.test.ts +++ b/src/store/normalizationSnapshot.test.ts @@ -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', diff --git a/src/store/normalizationSnapshot.ts b/src/store/normalizationSnapshot.ts index 14ca3762..401cffbd 100644 --- a/src/store/normalizationSnapshot.ts +++ b/src/store/normalizationSnapshot.ts @@ -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: diff --git a/src/store/playerStore.events.test.ts b/src/store/playerStore.events.test.ts index c4e851e0..c11da2b6 100644 --- a/src/store/playerStore.events.test.ts +++ b/src/store/playerStore.events.test.ts @@ -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, diff --git a/src/store/playerStore.persistence.test.ts b/src/store/playerStore.persistence.test.ts index 426547b5..3dd3b8c2 100644 --- a/src/store/playerStore.persistence.test.ts +++ b/src/store/playerStore.persistence.test.ts @@ -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'; diff --git a/src/store/playerStore.progress.test.ts b/src/store/playerStore.progress.test.ts index 03a25db7..06cabe20 100644 --- a/src/store/playerStore.progress.test.ts +++ b/src/store/playerStore.progress.test.ts @@ -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'; diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index 0a2c75f9..560f9052 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -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()( persist( (set, get) => { @@ -125,20 +65,11 @@ export const usePlayerStore = create()( ...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), }; }, diff --git a/src/store/promoteStreamCache.test.ts b/src/store/promoteStreamCache.test.ts index 814456db..47d9804f 100644 --- a/src/store/promoteStreamCache.test.ts +++ b/src/store/promoteStreamCache.test.ts @@ -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) => null as { path: string; size: number } | null), setEntryMock: vi.fn(), diff --git a/src/store/promoteStreamCache.ts b/src/store/promoteStreamCache.ts index 88b2adc9..4714c695 100644 --- a/src/store/promoteStreamCache.ts +++ b/src/store/promoteStreamCache.ts @@ -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 diff --git a/src/store/queueMutationActions.ts b/src/store/queueMutationActions.ts index 9cca50d0..4c16d25c 100644 --- a/src/store/queueMutationActions.ts +++ b/src/store/queueMutationActions.ts @@ -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< diff --git a/src/store/queueSync.test.ts b/src/store/queueSync.test.ts index 8767e72a..c7a1cc3b 100644 --- a/src/store/queueSync.test.ts +++ b/src/store/queueSync.test.ts @@ -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: { diff --git a/src/store/queueSync.ts b/src/store/queueSync.ts index c55c8c1e..92851396 100644 --- a/src/store/queueSync.ts +++ b/src/store/queueSync.ts @@ -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 diff --git a/src/store/queueUndo.test.ts b/src/store/queueUndo.test.ts index f01c83f8..1fe9c4b5 100644 --- a/src/store/queueUndo.test.ts +++ b/src/store/queueUndo.test.ts @@ -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, diff --git a/src/store/queueUndo.ts b/src/store/queueUndo.ts index a6b72be1..2f872647 100644 --- a/src/store/queueUndo.ts +++ b/src/store/queueUndo.ts @@ -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; diff --git a/src/store/queueUndoAudioRestore.test.ts b/src/store/queueUndoAudioRestore.test.ts index c3972e12..09086e0c 100644 --- a/src/store/queueUndoAudioRestore.test.ts +++ b/src/store/queueUndoAudioRestore.test.ts @@ -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', diff --git a/src/store/queueUndoAudioRestore.ts b/src/store/queueUndoAudioRestore.ts index 7baec0e8..4c70f8c2 100644 --- a/src/store/queueUndoAudioRestore.ts +++ b/src/store/queueUndoAudioRestore.ts @@ -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 diff --git a/src/store/skipStarRating.test.ts b/src/store/skipStarRating.test.ts index d9dc19f5..d3e58e8a 100644 --- a/src/store/skipStarRating.test.ts +++ b/src/store/skipStarRating.test.ts @@ -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[], diff --git a/src/store/skipStarRating.ts b/src/store/skipStarRating.ts index 445f9f7d..941f01c1 100644 --- a/src/store/skipStarRating.ts +++ b/src/store/skipStarRating.ts @@ -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 diff --git a/src/store/transportLightActions.ts b/src/store/transportLightActions.ts index 506a67a0..16ff132c 100644 --- a/src/store/transportLightActions.ts +++ b/src/store/transportLightActions.ts @@ -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, diff --git a/src/store/uiStateActions.ts b/src/store/uiStateActions.ts index 5f85f8a7..a14995dd 100644 --- a/src/store/uiStateActions.ts +++ b/src/store/uiStateActions.ts @@ -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, diff --git a/src/test/helpers/factories.ts b/src/test/helpers/factories.ts index 9b12537a..c2bf23c0 100644 --- a/src/test/helpers/factories.ts +++ b/src/test/helpers/factories.ts @@ -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'; diff --git a/src/test/helpers/storeReset.ts b/src/test/helpers/storeReset.ts index eacaaec7..0757fe53 100644 --- a/src/test/helpers/storeReset.ts +++ b/src/test/helpers/storeReset.ts @@ -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'; diff --git a/src/utils/applySharePaste.ts b/src/utils/applySharePaste.ts index 401ebd56..dfe5dd71 100644 --- a/src/utils/applySharePaste.ts +++ b/src/utils/applySharePaste.ts @@ -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'; diff --git a/src/utils/buildInfiniteQueueCandidates.test.ts b/src/utils/buildInfiniteQueueCandidates.test.ts index f648e527..cfb58435 100644 --- a/src/utils/buildInfiniteQueueCandidates.test.ts +++ b/src/utils/buildInfiniteQueueCandidates.test.ts @@ -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 => ({ diff --git a/src/utils/buildInfiniteQueueCandidates.ts b/src/utils/buildInfiniteQueueCandidates.ts index d8624f54..ba55fd13 100644 --- a/src/utils/buildInfiniteQueueCandidates.ts +++ b/src/utils/buildInfiniteQueueCandidates.ts @@ -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. diff --git a/src/utils/luckyMix.ts b/src/utils/luckyMix.ts index 5ffa9da2..19ff3e61 100644 --- a/src/utils/luckyMix.ts +++ b/src/utils/luckyMix.ts @@ -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'; diff --git a/src/utils/orbit.ts b/src/utils/orbit.ts index 29b0d060..2b91d539 100644 --- a/src/utils/orbit.ts +++ b/src/utils/orbit.ts @@ -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, diff --git a/src/utils/playAlbum.ts b/src/utils/playAlbum.ts index 3e6b7e65..568344f5 100644 --- a/src/utils/playAlbum.ts +++ b/src/utils/playAlbum.ts @@ -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 { diff --git a/src/utils/playArtistShuffled.ts b/src/utils/playArtistShuffled.ts index 8989160a..20f0fa99 100644 --- a/src/utils/playArtistShuffled.ts +++ b/src/utils/playArtistShuffled.ts @@ -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 artist’s albums, shuffled — same idea as Artist page “shuffle play”. */ diff --git a/src/utils/playByOpaqueId.ts b/src/utils/playByOpaqueId.ts index ff065e81..24438bf0 100644 --- a/src/utils/playByOpaqueId.ts +++ b/src/utils/playByOpaqueId.ts @@ -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. diff --git a/src/utils/playSong.ts b/src/utils/playSong.ts index a1ecf2d1..2490b06b 100644 --- a/src/utils/playSong.ts +++ b/src/utils/playSong.ts @@ -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 { diff --git a/src/utils/queueIdentity.test.ts b/src/utils/queueIdentity.test.ts index 68ef72da..0248916a 100644 --- a/src/utils/queueIdentity.test.ts +++ b/src/utils/queueIdentity.test.ts @@ -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, diff --git a/src/utils/queueIdentity.ts b/src/utils/queueIdentity.ts index af39612b..b7c35105 100644 --- a/src/utils/queueIdentity.ts +++ b/src/utils/queueIdentity.ts @@ -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, diff --git a/src/utils/resolveReplayGainDb.test.ts b/src/utils/resolveReplayGainDb.test.ts index f7937dea..c444166b 100644 --- a/src/utils/resolveReplayGainDb.test.ts +++ b/src/utils/resolveReplayGainDb.test.ts @@ -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 => ({ id: 'x', diff --git a/src/utils/resolveReplayGainDb.ts b/src/utils/resolveReplayGainDb.ts index ac9ecb96..a03bdf0e 100644 --- a/src/utils/resolveReplayGainDb.ts +++ b/src/utils/resolveReplayGainDb.ts @@ -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 diff --git a/src/utils/songToTrack.test.ts b/src/utils/songToTrack.test.ts index 8632cd29..1aa278dd 100644 --- a/src/utils/songToTrack.test.ts +++ b/src/utils/songToTrack.test.ts @@ -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', () => { diff --git a/src/utils/songToTrack.ts b/src/utils/songToTrack.ts index 4ad7cd2d..26017918 100644 --- a/src/utils/songToTrack.ts +++ b/src/utils/songToTrack.ts @@ -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,