From d6dbb615fdde188b5c6c5886a837ec308d38ada2 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:56:08 +0200 Subject: [PATCH] refactor(offline): co-locate offline feature into features/offline --- src/api/subsonicPlaylists.ts | 2 +- src/api/subsonicStarRating.ts | 4 +- src/app/AppRoutes.tsx | 2 +- src/app/AppShell.tsx | 12 ++-- src/app/MainApp.tsx | 12 ++-- src/app/TauriEventBridge.tsx | 2 +- src/components/AlbumCard.tsx | 2 +- src/components/AlbumHeader.tsx | 2 +- src/components/AlbumTrackList.tsx | 2 +- src/components/BecauseYouLikeRail.tsx | 2 +- src/components/ContextMenu.test.tsx | 2 +- src/components/ContextMenu.tsx | 4 +- src/components/DevNetworkModeToggle.tsx | 2 +- src/components/Hero.tsx | 2 +- src/components/MobileMoreOverlay.tsx | 6 +- src/components/QueuePanel.tsx | 2 +- .../albumDetail/AlbumDetailToolbar.tsx | 2 +- src/components/albumTrackList/TrackRow.tsx | 2 +- .../artistDetail/ArtistDetailHero.tsx | 4 +- .../AlbumArtistToPlaylistSubmenu.tsx | 2 +- .../contextMenu/AlbumContextItems.tsx | 2 +- .../MultiAlbumToPlaylistSubmenu.tsx | 2 +- .../MultiArtistToPlaylistSubmenu.tsx | 2 +- .../contextMenu/SongContextItems.tsx | 2 +- .../contextMenu/contextMenuItemTypes.ts | 2 +- src/components/playerBar/PlayerTrackInfo.tsx | 4 +- src/components/playlist/PlaylistHero.tsx | 6 +- src/components/playlists/PlaylistsHeader.tsx | 2 +- .../components/FavoritesOfflineHeader.tsx | 2 +- .../favorites/hooks/useFavoritesData.ts | 6 +- .../hooks/useFavoritesOfflineStatus.ts | 8 +-- .../utils/nowPlayingMetadataResolve.ts | 2 +- .../components/DownloadFolderModal.tsx | 4 +- .../offline}/components/OfflineBanner.tsx | 0 .../components/OfflineLibraryDiskStat.tsx | 2 +- .../offline}/components/OfflineOverlay.tsx | 0 .../components/ZipDownloadOverlay.tsx | 4 +- .../offline}/hooks/useOfflineAutoNav.ts | 2 +- .../offline}/hooks/useOfflineBrowseContext.ts | 12 ++-- .../hooks/useOfflineBrowseReloadToken.ts | 0 .../hooks/useOfflineLibraryFilterSuspend.ts | 4 +- .../offline/hooks}/useZipDownloadBridge.ts | 2 +- src/features/offline/index.ts | 55 +++++++++++++++++++ .../offline}/pages/OfflineLibrary.tsx | 34 ++++++------ .../offline}/store/devOfflineBrowseStore.ts | 0 .../offline}/store/downloadModalStore.ts | 0 .../store/favoritesOfflineSyncStore.ts | 0 .../offline}/store/offlineJobStore.test.ts | 2 +- .../offline}/store/offlineJobStore.ts | 0 .../offline}/store/offlineStore.ts | 32 +++++------ .../offline}/store/zipDownloadStore.ts | 0 .../utils}/favoritesOfflineBrowse.test.ts | 22 ++++---- .../offline/utils}/favoritesOfflineBrowse.ts | 8 +-- .../utils}/favoritesOfflineConstants.test.ts | 2 +- .../utils}/favoritesOfflineConstants.ts | 0 .../utils}/favoritesOfflineSync.test.ts | 18 +++--- .../offline/utils}/favoritesOfflineSync.ts | 38 ++++++------- .../utils}/legacyOfflineFileMigration.test.ts | 10 ++-- .../utils}/legacyOfflineFileMigration.ts | 18 +++--- .../offline/utils}/libraryTierReconcile.ts | 18 +++--- .../utils}/offlineActionPolicy.test.ts | 2 +- .../offline/utils}/offlineActionPolicy.ts | 0 .../utils}/offlineAlbumBrowseCatalog.ts | 8 +-- .../utils}/offlineBrowseContext.test.ts | 16 +++--- .../offline/utils}/offlineBrowseContext.ts | 14 ++--- .../offline/utils}/offlineBrowseMode.test.ts | 6 +- .../offline/utils}/offlineBrowseMode.ts | 6 +- .../utils}/offlineBrowseRouting.test.ts | 2 +- .../offline/utils}/offlineBrowseRouting.ts | 2 +- .../offlineLibraryFilterSuspend.test.ts | 4 +- .../utils}/offlineLibraryFilterSuspend.ts | 2 +- .../utils}/offlineLibraryHelpers.test.ts | 22 ++++---- .../offline/utils}/offlineLibraryHelpers.ts | 28 +++++----- .../offline/utils}/offlineLibraryIndexLoad.ts | 6 +- .../offline/utils}/offlineLocalBrowse.test.ts | 12 ++-- .../offline/utils}/offlineLocalBrowse.ts | 26 ++++----- .../utils}/offlineMediaResolve.test.ts | 22 ++++---- .../offline/utils}/offlineMediaResolve.ts | 22 ++++---- .../offline/utils}/offlineNavPolicy.ts | 0 .../offline/utils}/offlinePinQueue.test.ts | 4 +- .../offline/utils}/offlinePinQueue.ts | 6 +- .../utils}/offlinePlaylistBrowse.test.ts | 14 ++--- .../offline/utils}/offlinePlaylistBrowse.ts | 16 +++--- .../offline/utils}/offlineStarredLoad.ts | 18 +++--- .../offline/utils}/pinnedOfflineSync.test.ts | 28 +++++----- .../offline/utils}/pinnedOfflineSync.ts | 34 ++++++------ .../resumeIncompleteOfflinePins.test.ts | 14 ++--- .../utils}/resumeIncompleteOfflinePins.ts | 24 ++++---- src/features/sidebar/components/Sidebar.tsx | 10 ++-- .../stats/components/StatisticsTabBar.tsx | 2 +- src/features/stats/pages/Statistics.tsx | 2 +- src/hooks/useAlbumBrowseData.ts | 6 +- src/hooks/useAlbumDetailData.ts | 8 +-- src/hooks/useAlbumOfflineState.test.ts | 2 +- src/hooks/useAlbumOfflineState.ts | 4 +- src/hooks/useArtistDetailData.ts | 6 +- src/hooks/useArtistOfflineState.test.ts | 2 +- src/hooks/useArtistOfflineState.ts | 4 +- src/hooks/useArtistsBrowseCatalog.ts | 6 +- src/hooks/useBrowseAlbumTextSearch.ts | 4 +- src/hooks/useBrowseArtistTextSearch.ts | 4 +- src/hooks/useConnectionStatus.test.ts | 2 +- src/hooks/useConnectionStatus.ts | 2 +- src/hooks/useLyrics.ts | 2 +- src/hooks/usePlaylistsLibraryScopeCounts.ts | 2 +- src/hooks/useQueuePanelDrag.ts | 2 +- src/hooks/useSongBrowseList.test.ts | 2 +- src/hooks/useSongBrowseList.ts | 6 +- src/hotCachePrefetch.ts | 2 +- src/localPlaybackInvalidation.ts | 4 +- src/pages/AlbumDetail.tsx | 18 +++--- src/pages/Albums.tsx | 8 +-- src/pages/ArtistDetail.tsx | 4 +- src/pages/Home.tsx | 6 +- src/pages/LosslessAlbums.tsx | 8 +-- src/pages/MostPlayed.tsx | 2 +- src/pages/NewReleases.tsx | 8 +-- src/pages/PlaylistDetail.tsx | 10 ++-- src/pages/Playlists.tsx | 6 +- src/pages/RandomAlbums.tsx | 8 +-- src/store/crossfadePreload.ts | 2 +- src/store/hotCacheStore.ts | 2 +- src/store/localPlaybackMigration.ts | 2 +- src/store/playTrackAction.ts | 2 +- src/store/playbackUrlRouting.test.ts | 2 +- src/store/playbackUrlRouting.ts | 2 +- src/store/playlistStore.ts | 4 +- src/store/promoteStreamCache.test.ts | 2 +- src/store/promoteStreamCache.ts | 2 +- .../componentHelpers/contextMenuActions.ts | 4 +- .../runArtistDetailPlay.test.ts | 4 +- .../componentHelpers/runArtistDetailPlay.ts | 2 +- src/utils/library/albumBrowseCatalogChunk.ts | 4 +- src/utils/mix/luckyMixHelpers.ts | 2 +- .../network/activeServerReachability.test.ts | 2 +- src/utils/network/activeServerReachability.ts | 2 +- src/utils/network/subsonicNetworkGuard.ts | 2 +- src/utils/playback/fetchTracksForSource.ts | 2 +- src/utils/playback/playAlbum.test.ts | 4 +- src/utils/playback/playAlbum.ts | 2 +- src/utils/playback/playArtistShuffled.ts | 2 +- src/utils/playback/playByOpaqueId.ts | 2 +- src/utils/playback/resolvePlaybackUrl.ts | 2 +- src/utils/playlist/runPlaylistLoad.ts | 4 +- src/utils/playlist/runPlaylistZipDownload.ts | 2 +- .../server/rewriteFrontendStoreKeys.test.ts | 2 +- src/utils/server/rewriteFrontendStoreKeys.ts | 2 +- src/utils/share/applySharePaste.ts | 2 +- src/utils/share/enqueueShareSearchPayload.ts | 2 +- 149 files changed, 521 insertions(+), 466 deletions(-) rename src/{ => features/offline}/components/DownloadFolderModal.tsx (93%) rename src/{ => features/offline}/components/OfflineBanner.tsx (100%) rename src/{ => features/offline}/components/OfflineLibraryDiskStat.tsx (98%) rename src/{ => features/offline}/components/OfflineOverlay.tsx (100%) rename src/{ => features/offline}/components/ZipDownloadOverlay.tsx (94%) rename src/{ => features/offline}/hooks/useOfflineAutoNav.ts (95%) rename src/{ => features/offline}/hooks/useOfflineBrowseContext.ts (70%) rename src/{ => features/offline}/hooks/useOfflineBrowseReloadToken.ts (100%) rename src/{ => features/offline}/hooks/useOfflineLibraryFilterSuspend.ts (84%) rename src/{hooks/tauriBridge => features/offline/hooks}/useZipDownloadBridge.ts (86%) create mode 100644 src/features/offline/index.ts rename src/{ => features/offline}/pages/OfflineLibrary.tsx (95%) rename src/{ => features/offline}/store/devOfflineBrowseStore.ts (100%) rename src/{ => features/offline}/store/downloadModalStore.ts (100%) rename src/{ => features/offline}/store/favoritesOfflineSyncStore.ts (100%) rename src/{ => features/offline}/store/offlineJobStore.test.ts (98%) rename src/{ => features/offline}/store/offlineJobStore.ts (100%) rename src/{ => features/offline}/store/offlineStore.ts (93%) rename src/{ => features/offline}/store/zipDownloadStore.ts (100%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineBrowse.test.ts (95%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineBrowse.ts (75%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineConstants.test.ts (68%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineConstants.ts (100%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineSync.test.ts (87%) rename src/{utils/offline => features/offline/utils}/favoritesOfflineSync.ts (91%) rename src/{utils/migrations => features/offline/utils}/legacyOfflineFileMigration.test.ts (88%) rename src/{utils/migrations => features/offline/utils}/legacyOfflineFileMigration.ts (92%) rename src/{utils/offline => features/offline/utils}/libraryTierReconcile.ts (93%) rename src/{utils/offline => features/offline/utils}/offlineActionPolicy.test.ts (93%) rename src/{utils/offline => features/offline/utils}/offlineActionPolicy.ts (100%) rename src/{utils/offline => features/offline/utils}/offlineAlbumBrowseCatalog.ts (82%) rename src/{utils/offline => features/offline/utils}/offlineBrowseContext.test.ts (83%) rename src/{utils/offline => features/offline/utils}/offlineBrowseContext.ts (81%) rename src/{utils/offline => features/offline/utils}/offlineBrowseMode.test.ts (69%) rename src/{utils/offline => features/offline/utils}/offlineBrowseMode.ts (81%) rename src/{utils/offline => features/offline/utils}/offlineBrowseRouting.test.ts (97%) rename src/{utils/offline => features/offline/utils}/offlineBrowseRouting.ts (96%) rename src/{utils/offline => features/offline/utils}/offlineLibraryFilterSuspend.test.ts (90%) rename src/{utils/offline => features/offline/utils}/offlineLibraryFilterSuspend.ts (95%) rename src/{utils/offline => features/offline/utils}/offlineLibraryHelpers.test.ts (94%) rename src/{utils/offline => features/offline/utils}/offlineLibraryHelpers.ts (95%) rename src/{utils/offline => features/offline/utils}/offlineLibraryIndexLoad.ts (96%) rename src/{utils/offline => features/offline/utils}/offlineLocalBrowse.test.ts (89%) rename src/{utils/offline => features/offline/utils}/offlineLocalBrowse.ts (92%) rename src/{utils/offline => features/offline/utils}/offlineMediaResolve.test.ts (92%) rename src/{utils/offline => features/offline/utils}/offlineMediaResolve.ts (83%) rename src/{utils/offline => features/offline/utils}/offlineNavPolicy.ts (100%) rename src/{utils/offline => features/offline/utils}/offlinePinQueue.test.ts (97%) rename src/{utils/offline => features/offline/utils}/offlinePinQueue.ts (96%) rename src/{utils/offline => features/offline/utils}/offlinePlaylistBrowse.test.ts (90%) rename src/{utils/offline => features/offline/utils}/offlinePlaylistBrowse.ts (87%) rename src/{utils/offline => features/offline/utils}/offlineStarredLoad.ts (90%) rename src/{utils/offline => features/offline/utils}/pinnedOfflineSync.test.ts (94%) rename src/{utils/offline => features/offline/utils}/pinnedOfflineSync.ts (95%) rename src/{utils/offline => features/offline/utils}/resumeIncompleteOfflinePins.test.ts (86%) rename src/{utils/offline => features/offline/utils}/resumeIncompleteOfflinePins.ts (77%) diff --git a/src/api/subsonicPlaylists.ts b/src/api/subsonicPlaylists.ts index 26efd72c..b1ea411d 100644 --- a/src/api/subsonicPlaylists.ts +++ b/src/api/subsonicPlaylists.ts @@ -57,7 +57,7 @@ export async function updatePlaylist(id: string, songIds: string[], prevCount = songIndexToRemove: Array.from({ length: prevCount }, (_, i) => i), }); } - void import('../utils/offline/pinnedOfflineSync') + void import('@/features/offline') .then(m => m.schedulePinnedPlaylistSync(id)) .catch(() => {}); } diff --git a/src/api/subsonicStarRating.ts b/src/api/subsonicStarRating.ts index 7ed05c0d..5d769046 100644 --- a/src/api/subsonicStarRating.ts +++ b/src/api/subsonicStarRating.ts @@ -85,7 +85,7 @@ export async function star( const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId); void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {}); } - void import('../utils/offline/favoritesOfflineSync') + void import('@/features/offline') .then(m => m.onFavoritesOfflineStarChange(id, type, true, serverId ?? undefined)) .catch(() => {}); } @@ -108,7 +108,7 @@ export async function unstar( const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId); void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {}); } - void import('../utils/offline/favoritesOfflineSync') + void import('@/features/offline') .then(m => m.onFavoritesOfflineStarChange(id, type, false, serverId ?? undefined)) .catch(() => {}); } diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 3e840dba..18b14ec7 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -32,7 +32,7 @@ const Statistics = lazy(() => import('@/features/stats/pages/Statistics')); const Help = lazy(() => import('../pages/Help')); const WhatsNew = lazy(() => import('../pages/WhatsNew')); const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync')); -const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary')); +const OfflineLibrary = lazy(() => import('@/features/offline/pages/OfflineLibrary')); const LabelAlbums = lazy(() => import('../pages/LabelAlbums')); const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage')); const FolderBrowser = lazy(() => import('../pages/FolderBrowser')); diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index a041b0bb..8e3d550b 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -17,7 +17,7 @@ import AppRoutes from './AppRoutes'; import FullscreenPlayer from '@/features/fullscreenPlayer'; import ContextMenu from '../components/ContextMenu'; import SongInfoModal from '../components/SongInfoModal'; -import DownloadFolderModal from '../components/DownloadFolderModal'; +import { DownloadFolderModal } from '@/features/offline'; import GlobalConfirmModal from '../components/GlobalConfirmModal'; import ThemeMigrationNotice from '../components/ThemeMigrationNotice'; import OrbitAccountPicker from '../components/OrbitAccountPicker'; @@ -30,7 +30,7 @@ import { } from '../constants/appScroll'; import ConnectionIndicator from '../components/ConnectionIndicator'; import MusicNetworkIndicator from '../components/MusicNetworkIndicator'; -import OfflineBanner from '../components/OfflineBanner'; +import { OfflineBanner } from '@/features/offline'; import AppUpdater from '../components/AppUpdater'; import TitleBar from '../components/TitleBar'; import OrbitSessionBar from '../components/OrbitSessionBar'; @@ -39,8 +39,8 @@ import { useOrbitHost } from '../hooks/useOrbitHost'; import { useOrbitGuest } from '../hooks/useOrbitGuest'; import { useOrbitBodyAttrs } from '../hooks/useOrbitBodyAttrs'; import { usePlatformShellSetup } from '../hooks/usePlatformShellSetup'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineBrowseNavFlags } from '../utils/offline/offlineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineBrowseNavFlags } from '@/features/offline'; import { useWindowFullscreenState } from '../hooks/useWindowFullscreenState'; import { useNowPlayingTrayTitle } from '../hooks/useNowPlayingTrayTitle'; import { usePrefetchReleaseNotes } from '../hooks/usePrefetchReleaseNotes'; @@ -53,8 +53,8 @@ import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator'; import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority'; import { useLiveSearchRouteScope } from '@/features/search'; import { useNowPlayingPrewarm } from '@/features/nowPlaying'; -import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; -import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; +import { useOfflineAutoNav } from '@/features/offline'; +import { useOfflineLibraryFilterSuspend } from '@/features/offline'; import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam'; import { IS_LINUX, IS_MACOS } from '../utils/platform'; import { useConnectionStatus } from '../hooks/useConnectionStatus'; diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 7d89a629..a493480a 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -7,18 +7,18 @@ import { WindowVisibilityProvider } from '../hooks/useWindowVisibility'; import { DragDropProvider } from '../contexts/DragDropContext'; import PasteClipboardHandler from '../components/PasteClipboardHandler'; import ExportPickerModal from '../components/ExportPickerModal'; -import ZipDownloadOverlay from '../components/ZipDownloadOverlay'; +import { ZipDownloadOverlay } from '@/features/offline'; import FpsOverlay from '../components/FpsOverlay'; import { useAuthStore } from '../store/authStore'; import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useGlobalShortcutsStore } from '../store/globalShortcutsStore'; import { initHotCachePrefetch } from '../hotCachePrefetch'; import { initLocalPlaybackInvalidation } from '../localPlaybackInvalidation'; -import { initFavoritesOfflineSync } from '../utils/offline/favoritesOfflineSync'; -import { initPinnedOfflineSync } from '../utils/offline/pinnedOfflineSync'; -import { initResumeIncompleteOfflinePins, scheduleResumeIncompleteOfflinePins } from '../utils/offline/resumeIncompleteOfflinePins'; -import { runLegacyOfflineFileMigration } from '../utils/migrations/legacyOfflineFileMigration'; -import { reconcileLibraryTierForServer } from '../utils/offline/libraryTierReconcile'; +import { initFavoritesOfflineSync } from '@/features/offline'; +import { initPinnedOfflineSync } from '@/features/offline'; +import { initResumeIncompleteOfflinePins, scheduleResumeIncompleteOfflinePins } from '@/features/offline'; +import { runLegacyOfflineFileMigration } from '@/features/offline'; +import { reconcileLibraryTierForServer } from '@/features/offline'; import { initMiniPlayerBridgeOnMain } from '@/features/miniPlayer'; import { runAdvancedModeMigration } from '../utils/migrations/advancedModeMigration'; import { bootstrapAllIndexedServers } from '../utils/library/librarySession'; diff --git a/src/app/TauriEventBridge.tsx b/src/app/TauriEventBridge.tsx index f376cd7c..78c11480 100644 --- a/src/app/TauriEventBridge.tsx +++ b/src/app/TauriEventBridge.tsx @@ -1,5 +1,5 @@ import { useNavigate } from 'react-router-dom'; -import { useZipDownloadBridge } from '../hooks/tauriBridge/useZipDownloadBridge'; +import { useZipDownloadBridge } from '@/features/offline'; import { usePreviewBridge } from '../hooks/tauriBridge/usePreviewBridge'; import { useAudioDeviceBridge } from '../hooks/tauriBridge/useAudioDeviceBridge'; import { useCliBridge } from '../hooks/tauriBridge/useCliBridge'; diff --git a/src/components/AlbumCard.tsx b/src/components/AlbumCard.tsx index c6b5c5fa..db9e50be 100644 --- a/src/components/AlbumCard.tsx +++ b/src/components/AlbumCard.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { usePlayerStore } from '../store/playerStore'; import { useAuthStore } from '../store/authStore'; import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; +import { isOfflinePinComplete } from '@/features/offline'; import { CoverArtImage } from '../cover/CoverArtImage'; import { useAlbumCoverRef } from '../cover/useLibraryCoverRef'; import { coverStorageKeyFromRef } from '../cover/storageKeys'; diff --git a/src/components/AlbumHeader.tsx b/src/components/AlbumHeader.tsx index 69512148..34c82c9a 100644 --- a/src/components/AlbumHeader.tsx +++ b/src/components/AlbumHeader.tsx @@ -19,7 +19,7 @@ import { formatMb } from '../utils/format/formatBytes'; import { sanitizeHtml } from '../utils/sanitizeHtml'; import { OpenArtistRefInline } from './OpenArtistRefInline'; import { tooltipAttrs } from '@/ui/tooltipAttrs'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; import { deriveAlbumGenreTags } from '../utils/library/genreTags'; import { genreColor } from '../utils/library/genreColor'; diff --git a/src/components/AlbumTrackList.tsx b/src/components/AlbumTrackList.tsx index 1a2f6184..dc11df1e 100644 --- a/src/components/AlbumTrackList.tsx +++ b/src/components/AlbumTrackList.tsx @@ -15,7 +15,7 @@ import { TrackRow } from './albumTrackList/TrackRow'; import { AlbumTrackListMobile } from './albumTrackList/AlbumTrackListMobile'; import { TracklistColumnPicker } from './albumTrackList/TracklistColumnPicker'; import { TracklistHeaderRow } from './albumTrackList/TracklistHeaderRow'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; export type { SortKey } from '../utils/componentHelpers/albumTrackListHelpers'; diff --git a/src/components/BecauseYouLikeRail.tsx b/src/components/BecauseYouLikeRail.tsx index 924f85cf..5039244a 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/components/BecauseYouLikeRail.tsx @@ -1,6 +1,6 @@ import { getArtist, getArtistInfo } from '../api/subsonicArtists'; import { filterAlbumsToActiveLibrary } from '../api/subsonicLibrary'; -import { resolveAlbum, resolveMediaServerId } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import { shuffleArray } from '../utils/playback/shuffleArray'; diff --git a/src/components/ContextMenu.test.tsx b/src/components/ContextMenu.test.tsx index dee38914..80465d93 100644 --- a/src/components/ContextMenu.test.tsx +++ b/src/components/ContextMenu.test.tsx @@ -34,7 +34,7 @@ vi.mock('@/utils/orbitBulkGuard', () => ({ orbitBulkGuard: vi.fn(async () => true), })); -vi.mock('@/hooks/useOfflineBrowseContext', () => ({ +vi.mock('@/features/offline', () => ({ useOfflineBrowseContext: () => ({ active: false, serverId: 'srv-1', diff --git a/src/components/ContextMenu.tsx b/src/components/ContextMenu.tsx index d0ca337c..09022c1c 100644 --- a/src/components/ContextMenu.tsx +++ b/src/components/ContextMenu.tsx @@ -17,11 +17,11 @@ import { useContextMenuKeyboardNav } from '../hooks/useContextMenuKeyboardNav'; import { useContextMenuRating } from '../hooks/useContextMenuRating'; import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate'; import { useNavigate } from 'react-router-dom'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; import { offlineActionPolicy, type OfflineSurface, -} from '../utils/offline/offlineActionPolicy'; +} from '@/features/offline'; import ContextMenuItems from './contextMenu/ContextMenuItems'; function contextMenuSurfaceForType(type: string | null): OfflineSurface { diff --git a/src/components/DevNetworkModeToggle.tsx b/src/components/DevNetworkModeToggle.tsx index 55a1fa28..a0b76b7a 100644 --- a/src/components/DevNetworkModeToggle.tsx +++ b/src/components/DevNetworkModeToggle.tsx @@ -1,5 +1,5 @@ import { Cloud, CloudOff } from 'lucide-react'; -import { useDevOfflineBrowseStore } from '../store/devOfflineBrowseStore'; +import { useDevOfflineBrowseStore } from '@/features/offline'; /** DEV-only: simulate full offline (disconnect UI, block Subsonic, local playback only). */ export default function DevNetworkModeToggle() { diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index dc0c998f..1c2717d7 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -1,5 +1,5 @@ import { getRandomAlbums } from '../api/subsonicLibrary'; -import { resolveAlbum, resolveMediaServerId } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; diff --git a/src/components/MobileMoreOverlay.tsx b/src/components/MobileMoreOverlay.tsx index 17f08b74..a961cac3 100644 --- a/src/components/MobileMoreOverlay.tsx +++ b/src/components/MobileMoreOverlay.tsx @@ -6,9 +6,9 @@ import { useSidebarStore } from '@/features/sidebar'; import { useAuthStore } from '../store/authStore'; import { ALL_NAV_ITEMS } from '../config/navItems'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; -import { isOfflineSidebarNavAllowed } from '../utils/offline/offlineNavPolicy'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineBrowseNavFlags } from '../utils/offline/offlineBrowseContext'; +import { isOfflineSidebarNavAllowed } from '@/features/offline'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineBrowseNavFlags } from '@/features/offline'; const BOTTOM_NAV_ROUTES = new Set(['/', '/albums', '/now-playing']); diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 601106de..c617b02f 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -1,6 +1,6 @@ import { Play } from 'lucide-react'; import { updatePlaylist } from '../api/subsonicPlaylists'; -import { resolvePlaylist, resolveMediaServerId } from '../utils/offline/offlineMediaResolve'; +import { resolvePlaylist, resolveMediaServerId } from '@/features/offline'; import { songToTrack } from '../utils/playback/songToTrack'; import type { Track } from '../store/playerStoreTypes'; import { useState, useRef, useMemo } from 'react'; diff --git a/src/components/albumDetail/AlbumDetailToolbar.tsx b/src/components/albumDetail/AlbumDetailToolbar.tsx index a6e1efab..10149d9f 100644 --- a/src/components/albumDetail/AlbumDetailToolbar.tsx +++ b/src/components/albumDetail/AlbumDetailToolbar.tsx @@ -3,7 +3,7 @@ import { ListPlus, Search, X } from 'lucide-react'; import type { TFunction } from 'i18next'; import { useSelectionStore } from '../../store/selectionStore'; import { AddToPlaylistSubmenu } from '../ContextMenu'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; interface Props { filterText: string; diff --git a/src/components/albumTrackList/TrackRow.tsx b/src/components/albumTrackList/TrackRow.tsx index cd6501ab..69c50213 100644 --- a/src/components/albumTrackList/TrackRow.tsx +++ b/src/components/albumTrackList/TrackRow.tsx @@ -14,7 +14,7 @@ import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrack import { formatLongDuration } from '../../utils/format/formatDuration'; import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; import i18n from '../../i18n'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs'; type ContextMenuFn = ( diff --git a/src/components/artistDetail/ArtistDetailHero.tsx b/src/components/artistDetail/ArtistDetailHero.tsx index 9a8634e6..8934aeaf 100644 --- a/src/components/artistDetail/ArtistDetailHero.tsx +++ b/src/components/artistDetail/ArtistDetailHero.tsx @@ -6,7 +6,7 @@ import { Loader2, Play, Radio, Share2, Shuffle, Users, } from 'lucide-react'; import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes'; -import { useOfflineStore } from '../../store/offlineStore'; +import { useOfflineStore } from '@/features/offline'; import { useAuthStore } from '../../store/authStore'; import { useThemeStore } from '../../store/themeStore'; import { useArtistOfflineState } from '../../hooks/useArtistOfflineState'; @@ -22,7 +22,7 @@ import LastfmIcon from '../LastfmIcon'; import WikipediaIcon from '../WikipediaIcon'; import StarRating from '../StarRating'; import { tooltipAttrs } from '@/ui/tooltipAttrs'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; interface Props { artist: SubsonicArtist; diff --git a/src/components/contextMenu/AlbumArtistToPlaylistSubmenu.tsx b/src/components/contextMenu/AlbumArtistToPlaylistSubmenu.tsx index fe297792..9f962fc9 100644 --- a/src/components/contextMenu/AlbumArtistToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/AlbumArtistToPlaylistSubmenu.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { resolveAlbum, resolveArtist, resolveMediaServerId } from '../../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/features/offline'; import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu'; interface AlbumProps { diff --git a/src/components/contextMenu/AlbumContextItems.tsx b/src/components/contextMenu/AlbumContextItems.tsx index 6aeab744..dc87d804 100644 --- a/src/components/contextMenu/AlbumContextItems.tsx +++ b/src/components/contextMenu/AlbumContextItems.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import { Play, ListPlus, Heart, Download, ChevronRight, ChevronsRight, User, ListMusic, Star, Share2 } from 'lucide-react'; import { useNavigate } from 'react-router-dom'; -import { resolveAlbum, resolveMediaServerId } from '../../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; import { star, unstar } from '../../api/subsonicStarRating'; import type { SubsonicAlbum } from '../../api/subsonicTypes'; import { songToTrack } from '../../utils/playback/songToTrack'; diff --git a/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx b/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx index 1b3b2321..a21dd7c4 100644 --- a/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/MultiAlbumToPlaylistSubmenu.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; -import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '../../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; import { usePlaylistStore } from '../../store/playlistStore'; import { showToast } from '../../utils/ui/toast'; diff --git a/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx b/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx index 3e9702c4..1bd7a5f5 100644 --- a/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx +++ b/src/components/contextMenu/MultiArtistToPlaylistSubmenu.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ListMusic, Plus } from 'lucide-react'; -import { resolveAlbum, resolveArtist, resolveMediaServerId, resolvePlaylist } from '../../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveArtist, resolveMediaServerId, resolvePlaylist } from '@/features/offline'; import { getPlaylists } from '../../api/subsonicPlaylists'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; import { usePlaylistStore } from '../../store/playlistStore'; diff --git a/src/components/contextMenu/SongContextItems.tsx b/src/components/contextMenu/SongContextItems.tsx index e329b42f..55205409 100644 --- a/src/components/contextMenu/SongContextItems.tsx +++ b/src/components/contextMenu/SongContextItems.tsx @@ -2,7 +2,7 @@ import { useTranslation } from 'react-i18next'; import { Play, ListPlus, Radio, Heart, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react'; import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum'; import { useNavigateToArtist } from '../../hooks/useNavigateToArtist'; -import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '../../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline'; import { queueSongStar } from '../../store/pendingStarSync'; import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network'; import type { Track } from '../../store/playerStoreTypes'; diff --git a/src/components/contextMenu/contextMenuItemTypes.ts b/src/components/contextMenu/contextMenuItemTypes.ts index cd16f895..7fc1991d 100644 --- a/src/components/contextMenu/contextMenuItemTypes.ts +++ b/src/components/contextMenu/contextMenuItemTypes.ts @@ -2,7 +2,7 @@ import type React from 'react'; import type { SubsonicAlbum, SubsonicArtist } from '../../api/subsonicTypes'; import type { QueueItemRef, Track } from '../../store/playerStoreTypes'; import type { EntityShareKind } from '../../utils/share/shareLink'; -import type { OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import type { OfflineActionPolicy } from '@/features/offline'; export type RatingKind = 'song' | 'album' | 'artist'; diff --git a/src/components/playerBar/PlayerTrackInfo.tsx b/src/components/playerBar/PlayerTrackInfo.tsx index 2b9c3ff9..14987383 100644 --- a/src/components/playerBar/PlayerTrackInfo.tsx +++ b/src/components/playerBar/PlayerTrackInfo.tsx @@ -21,8 +21,8 @@ import { usePlayerBarLayoutStore, type PlayerBarLayoutItemId, } from '../../store/playerBarLayoutStore'; -import { useOfflineBrowseContext } from '../../hooks/useOfflineBrowseContext'; -import { offlineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineActionPolicy } from '@/features/offline'; interface Props { currentTrack: Track | null; diff --git a/src/components/playlist/PlaylistHero.tsx b/src/components/playlist/PlaylistHero.tsx index 24829bf8..eda40d97 100644 --- a/src/components/playlist/PlaylistHero.tsx +++ b/src/components/playlist/PlaylistHero.tsx @@ -6,9 +6,9 @@ import { Loader2, Lock, Pencil, Play, Search, Shuffle, Sparkles, Trash2, } from 'lucide-react'; import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; -import type { ZipDownload } from '../../store/zipDownloadStore'; +import type { ZipDownload } from '@/features/offline'; import type { AlbumOfflineStatus } from '../../hooks/useAlbumOfflineState'; -import { dequeueOfflinePin } from '../../utils/offline/offlinePinQueue'; +import { dequeueOfflinePin } from '@/features/offline'; import { useThemeStore } from '../../store/themeStore'; import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore'; import { @@ -18,7 +18,7 @@ import type { CoverArtId } from '../../cover/types'; import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; import { PLAYLIST_MAIN_COVER_CSS_PX } from '../../hooks/usePlaylistCovers'; import { PlaylistSmartCoverCell } from '../playlists/PlaylistCoverImages'; -import type { OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import type { OfflineActionPolicy } from '@/features/offline'; interface Props { playlist: SubsonicPlaylist; diff --git a/src/components/playlists/PlaylistsHeader.tsx b/src/components/playlists/PlaylistsHeader.tsx index 315a1971..c764b43c 100644 --- a/src/components/playlists/PlaylistsHeader.tsx +++ b/src/components/playlists/PlaylistsHeader.tsx @@ -5,7 +5,7 @@ import type { SubsonicPlaylist } from '../../api/subsonicTypes'; import { defaultSmartFilters, type SmartFilters, } from '../../utils/playlist/playlistsSmart'; -import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; +import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline'; import PlaylistsNewFolderButton from './PlaylistsNewFolderButton'; import PlaylistsFolderViewToggle from './PlaylistsFolderViewToggle'; diff --git a/src/features/favorites/components/FavoritesOfflineHeader.tsx b/src/features/favorites/components/FavoritesOfflineHeader.tsx index 3e7875d5..d44fcdd0 100644 --- a/src/features/favorites/components/FavoritesOfflineHeader.tsx +++ b/src/features/favorites/components/FavoritesOfflineHeader.tsx @@ -8,7 +8,7 @@ import { import { disableFavoritesOfflineSync, scheduleFavoritesOfflineSync, -} from '@/utils/offline/favoritesOfflineSync'; +} from '@/features/offline'; function semaphoreTooltipKey(semaphore: FavoritesOfflineSemaphore): string { switch (semaphore) { diff --git a/src/features/favorites/hooks/useFavoritesData.ts b/src/features/favorites/hooks/useFavoritesData.ts index add631be..b67aa528 100644 --- a/src/features/favorites/hooks/useFavoritesData.ts +++ b/src/features/favorites/hooks/useFavoritesData.ts @@ -9,12 +9,12 @@ import { usePlayerStore } from '@/store/playerStore'; import type { TopFavoriteArtist } from '@/features/favorites/components/TopFavoriteArtists'; import { useConnectionStatus } from '@/hooks/useConnectionStatus'; import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; -import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; -import { useOfflineBrowseReloadToken } from '@/hooks/useOfflineBrowseReloadToken'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { useOfflineBrowseReloadToken } from '@/features/offline'; import { loadStarredFromAllLibraryIndexes, loadStarredFromAllServersOnline, -} from '@/utils/offline/offlineStarredLoad'; +} from '@/features/offline'; export interface FavoritesDataResult { albums: SubsonicAlbum[]; diff --git a/src/features/favorites/hooks/useFavoritesOfflineStatus.ts b/src/features/favorites/hooks/useFavoritesOfflineStatus.ts index c35b7869..8a0f380a 100644 --- a/src/features/favorites/hooks/useFavoritesOfflineStatus.ts +++ b/src/features/favorites/hooks/useFavoritesOfflineStatus.ts @@ -1,10 +1,10 @@ import { useMemo } from 'react'; import { useAuthStore } from '@/store/authStore'; -import { useFavoritesOfflineSyncStore } from '@/store/favoritesOfflineSyncStore'; +import { useFavoritesOfflineSyncStore } from '@/features/offline'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; -import { useOfflineJobStore } from '@/store/offlineJobStore'; -import { FAVORITES_OFFLINE_JOB_ID } from '@/utils/offline/favoritesOfflineConstants'; -import { entryBelongsToServer } from '@/utils/offline/offlineLibraryHelpers'; +import { useOfflineJobStore } from '@/features/offline'; +import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline'; +import { entryBelongsToServer } from '@/features/offline'; export type FavoritesOfflineUiStatus = | 'disabled' diff --git a/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts index 6ccd10f8..06652451 100644 --- a/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts +++ b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts @@ -21,7 +21,7 @@ import { getArtistForServer, getTopSongsForServer } from '@/api/subsonicArtists' import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary'; import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes'; import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; -import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '@/utils/offline/offlineLibraryIndexLoad'; +import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '@/features/offline'; import { trackToSong } from '@/utils/library/advancedSearchLocal'; import { libraryIsReady } from '@/utils/library/libraryReady'; diff --git a/src/components/DownloadFolderModal.tsx b/src/features/offline/components/DownloadFolderModal.tsx similarity index 93% rename from src/components/DownloadFolderModal.tsx rename to src/features/offline/components/DownloadFolderModal.tsx index eb535572..991d2c29 100644 --- a/src/components/DownloadFolderModal.tsx +++ b/src/features/offline/components/DownloadFolderModal.tsx @@ -1,8 +1,8 @@ import { FolderOpen } from 'lucide-react'; import { open as openDialog } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; -import { useDownloadModalStore } from '../store/downloadModalStore'; -import { useAuthStore } from '../store/authStore'; +import { useDownloadModalStore } from '@/features/offline/store/downloadModalStore'; +import { useAuthStore } from '@/store/authStore'; export default function DownloadFolderModal() { const { isOpen, folder, remember, setFolder, setRemember, confirm, cancel } = useDownloadModalStore(); diff --git a/src/components/OfflineBanner.tsx b/src/features/offline/components/OfflineBanner.tsx similarity index 100% rename from src/components/OfflineBanner.tsx rename to src/features/offline/components/OfflineBanner.tsx diff --git a/src/components/OfflineLibraryDiskStat.tsx b/src/features/offline/components/OfflineLibraryDiskStat.tsx similarity index 98% rename from src/components/OfflineLibraryDiskStat.tsx rename to src/features/offline/components/OfflineLibraryDiskStat.tsx index de9b5f8e..25dc3d60 100644 --- a/src/components/OfflineLibraryDiskStat.tsx +++ b/src/features/offline/components/OfflineLibraryDiskStat.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { HardDriveDownload, Heart } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { formatBytes } from '../utils/format/formatBytes'; +import { formatBytes } from '@/utils/format/formatBytes'; const OPEN_DELAY_MS = 450; diff --git a/src/components/OfflineOverlay.tsx b/src/features/offline/components/OfflineOverlay.tsx similarity index 100% rename from src/components/OfflineOverlay.tsx rename to src/features/offline/components/OfflineOverlay.tsx diff --git a/src/components/ZipDownloadOverlay.tsx b/src/features/offline/components/ZipDownloadOverlay.tsx similarity index 94% rename from src/components/ZipDownloadOverlay.tsx rename to src/features/offline/components/ZipDownloadOverlay.tsx index 8f169aaa..8613d45d 100644 --- a/src/components/ZipDownloadOverlay.tsx +++ b/src/features/offline/components/ZipDownloadOverlay.tsx @@ -1,8 +1,8 @@ import { useEffect } from 'react'; import { createPortal } from 'react-dom'; import { HardDriveDownload, Check, X } from 'lucide-react'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; -import { formatBytes } from '../utils/format/formatBytes'; +import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore'; +import { formatBytes } from '@/utils/format/formatBytes'; function ZipDownloadItem({ id }: { id: string }) { const dismiss = useZipDownloadStore(s => s.dismiss); diff --git a/src/hooks/useOfflineAutoNav.ts b/src/features/offline/hooks/useOfflineAutoNav.ts similarity index 95% rename from src/hooks/useOfflineAutoNav.ts rename to src/features/offline/hooks/useOfflineAutoNav.ts index c952748a..039fc6f1 100644 --- a/src/hooks/useOfflineAutoNav.ts +++ b/src/features/offline/hooks/useOfflineAutoNav.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from 'react'; import type { Location, NavigateFunction } from 'react-router-dom'; -import { resolveOfflineDisconnectNavAction } from '../utils/offline/offlineBrowseRouting'; +import { resolveOfflineDisconnectNavAction } from '@/features/offline/utils/offlineBrowseRouting'; type ConnStatus = 'connected' | 'disconnected' | 'connecting' | 'unknown'; diff --git a/src/hooks/useOfflineBrowseContext.ts b/src/features/offline/hooks/useOfflineBrowseContext.ts similarity index 70% rename from src/hooks/useOfflineBrowseContext.ts rename to src/features/offline/hooks/useOfflineBrowseContext.ts index cc61ccde..3af49c6c 100644 --- a/src/hooks/useOfflineBrowseContext.ts +++ b/src/features/offline/hooks/useOfflineBrowseContext.ts @@ -1,14 +1,14 @@ -import { useAuthStore } from '../store/authStore'; -import { useOfflineStore } from '../store/offlineStore'; -import { useConnectionStatus } from './useConnectionStatus'; +import { useAuthStore } from '@/store/authStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { useConnectionStatus } from '@/hooks/useConnectionStatus'; import { usePlayerStatsRecordingEnabled } from '@/features/stats'; -import { hasOfflineBrowsingContent } from '../utils/offline/favoritesOfflineBrowse'; -import { useOfflineBrowseActive } from '../utils/offline/offlineBrowseMode'; +import { hasOfflineBrowsingContent } from '@/features/offline/utils/favoritesOfflineBrowse'; +import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; import { buildOfflineBrowseContext, computeOfflineBrowseCapabilities, type OfflineBrowseContext, -} from '../utils/offline/offlineBrowseContext'; +} from '@/features/offline/utils/offlineBrowseContext'; /** Single subscription for shell and pages: offline browse mode + capabilities. */ export function useOfflineBrowseContext(): OfflineBrowseContext { diff --git a/src/hooks/useOfflineBrowseReloadToken.ts b/src/features/offline/hooks/useOfflineBrowseReloadToken.ts similarity index 100% rename from src/hooks/useOfflineBrowseReloadToken.ts rename to src/features/offline/hooks/useOfflineBrowseReloadToken.ts diff --git a/src/hooks/useOfflineLibraryFilterSuspend.ts b/src/features/offline/hooks/useOfflineLibraryFilterSuspend.ts similarity index 84% rename from src/hooks/useOfflineLibraryFilterSuspend.ts rename to src/features/offline/hooks/useOfflineLibraryFilterSuspend.ts index 9c102355..23e5445f 100644 --- a/src/hooks/useOfflineLibraryFilterSuspend.ts +++ b/src/features/offline/hooks/useOfflineLibraryFilterSuspend.ts @@ -1,9 +1,9 @@ import { useEffect, useRef } from 'react'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline/hooks/useOfflineBrowseContext'; import { restoreMusicLibraryFiltersAfterOffline, suspendMusicLibraryFiltersForOffline, -} from '../utils/offline/offlineLibraryFilterSuspend'; +} from '@/features/offline/utils/offlineLibraryFilterSuspend'; /** Disable scoped library browse offline; restore the picker value when back online. */ export function useOfflineLibraryFilterSuspend(): void { diff --git a/src/hooks/tauriBridge/useZipDownloadBridge.ts b/src/features/offline/hooks/useZipDownloadBridge.ts similarity index 86% rename from src/hooks/tauriBridge/useZipDownloadBridge.ts rename to src/features/offline/hooks/useZipDownloadBridge.ts index 879336d2..2d224733 100644 --- a/src/hooks/tauriBridge/useZipDownloadBridge.ts +++ b/src/features/offline/hooks/useZipDownloadBridge.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { listen } from '@tauri-apps/api/event'; -import { useZipDownloadStore } from '../../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore'; /** ZIP download progress events from Rust. */ export function useZipDownloadBridge() { diff --git a/src/features/offline/index.ts b/src/features/offline/index.ts new file mode 100644 index 00000000..f647af7a --- /dev/null +++ b/src/features/offline/index.ts @@ -0,0 +1,55 @@ +/** + * Offline feature — offline mode + the downloaded/pinned-library machinery: + * the offline/job/zip-download/download-modal stores, the offline-browse and + * media-resolution utils, pin-queue + sync engines, the offline overlays/banner, + * and the OfflineLibrary page (lazy via the deep path `pages/OfflineLibrary`, so + * it is not re-exported here). The favorites-offline integration layer + * (`favoritesOffline*`, `favoritesOfflineSyncStore`) lives here too — the + * offline core owns it; the favorites feature consumes it through this barrel. + * + * Stays OUT (consumed by, not owned by, offline): the per-entity offline-status + * hooks `useAlbumOfflineState` / `useArtistOfflineState` (album/artist), + * `runPlaylistZipDownload` (playlist), `ensureQueueServerPinned` (playback), and + * the `localPlayback*` byte-store substrate (decided with playback, which moves + * last). Playback-core (`playTrackAction`) and the server key-remigration infra + * consume this barrel — the correct feature→feature edge, realized early. + */ +export * from './hooks/useOfflineAutoNav'; +export * from './hooks/useOfflineBrowseContext'; +export * from './hooks/useOfflineBrowseReloadToken'; +export * from './hooks/useOfflineLibraryFilterSuspend'; +export * from './hooks/useZipDownloadBridge'; +export * from './store/devOfflineBrowseStore'; +export * from './store/downloadModalStore'; +export * from './store/favoritesOfflineSyncStore'; +export * from './store/offlineJobStore'; +export * from './store/offlineStore'; +export * from './store/zipDownloadStore'; +export * from './utils/favoritesOfflineBrowse'; +export * from './utils/favoritesOfflineConstants'; +export * from './utils/favoritesOfflineSync'; +export * from './utils/legacyOfflineFileMigration'; +export * from './utils/libraryTierReconcile'; +export * from './utils/offlineActionPolicy'; +export * from './utils/offlineAlbumBrowseCatalog'; +export * from './utils/offlineBrowseContext'; +export * from './utils/offlineBrowseMode'; +export * from './utils/offlineBrowseRouting'; +export * from './utils/offlineLibraryFilterSuspend'; +export * from './utils/offlineLibraryHelpers'; +export * from './utils/offlineLibraryIndexLoad'; +export * from './utils/offlineLocalBrowse'; +export * from './utils/offlineMediaResolve'; +export * from './utils/offlineNavPolicy'; +export * from './utils/offlinePinQueue'; +export * from './utils/offlinePlaylistBrowse'; +export * from './utils/offlineStarredLoad'; +export * from './utils/pinnedOfflineSync'; +export * from './utils/resumeIncompleteOfflinePins'; +// `OfflinePinKind` (= PinSource['kind']) is declared in both pin modules; the +// explicit re-export resolves the `export *` ambiguity (TS2308). +export type { OfflinePinKind } from './utils/offlinePinQueue'; +export { OfflineLibraryDiskStat } from './components/OfflineLibraryDiskStat'; +export { default as DownloadFolderModal } from './components/DownloadFolderModal'; +export { default as OfflineBanner } from './components/OfflineBanner'; +export { default as ZipDownloadOverlay } from './components/ZipDownloadOverlay'; diff --git a/src/pages/OfflineLibrary.tsx b/src/features/offline/pages/OfflineLibrary.tsx similarity index 95% rename from src/pages/OfflineLibrary.tsx rename to src/features/offline/pages/OfflineLibrary.tsx index 44922f3d..db8c8e55 100644 --- a/src/pages/OfflineLibrary.tsx +++ b/src/features/offline/pages/OfflineLibrary.tsx @@ -3,16 +3,16 @@ import { useNavigate } from 'react-router-dom'; import { invoke } from '@tauri-apps/api/core'; import { Play, HardDriveDownload, Trash2, ListPlus, ListMusic, Heart } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { useOfflineStore } from '../store/offlineStore'; -import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; -import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; -import { coverServerScopeForServerId } from '../cover/serverScope'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { GRID_COVER_WARM_LIMIT } from '../cover/layoutSizes'; -import { VirtualCardGrid } from '../components/VirtualCardGrid'; -import { OfflineLibraryDiskStat } from '../components/OfflineLibraryDiskStat'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { coverServerScopeForServerId } from '@/cover/serverScope'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { GRID_COVER_WARM_LIMIT } from '@/cover/layoutSizes'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; +import { OfflineLibraryDiskStat } from '@/features/offline/components/OfflineLibraryDiskStat'; import { buildOfflineCacheQueueTracks, buildOfflineFavoritesQueueTracks, @@ -31,16 +31,16 @@ import { offlineTrackCount, type OfflineCoverQuadCell, type OfflineLibraryCard, -} from '../utils/offline/offlineLibraryHelpers'; -import { showToast } from '../utils/ui/toast'; -import { shuffleArray } from '../utils/playback/shuffleArray'; -import { getMediaDir } from '../utils/media/mediaDir'; -import { canonicalQueueServerKey, resolveIndexKey } from '../utils/server/serverIndexKey'; -import { reconcileAllLibraryTiersFromDisk } from '../utils/offline/libraryTierReconcile'; +} from '@/features/offline/utils/offlineLibraryHelpers'; +import { showToast } from '@/utils/ui/toast'; +import { shuffleArray } from '@/utils/playback/shuffleArray'; +import { getMediaDir } from '@/utils/media/mediaDir'; +import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey'; +import { reconcileAllLibraryTiersFromDisk } from '@/features/offline/utils/libraryTierReconcile'; import { inferPinSourcesFromLibraryIndex, restoreOfflineLibraryPinSources, -} from '../utils/migrations/legacyOfflineFileMigration'; +} from '@/features/offline/utils/legacyOfflineFileMigration'; const OFFLINE_CARD_COVER_CSS_PX = 300; const OFFLINE_CACHE_GRID_KEY = '__offline_cache__'; diff --git a/src/store/devOfflineBrowseStore.ts b/src/features/offline/store/devOfflineBrowseStore.ts similarity index 100% rename from src/store/devOfflineBrowseStore.ts rename to src/features/offline/store/devOfflineBrowseStore.ts diff --git a/src/store/downloadModalStore.ts b/src/features/offline/store/downloadModalStore.ts similarity index 100% rename from src/store/downloadModalStore.ts rename to src/features/offline/store/downloadModalStore.ts diff --git a/src/store/favoritesOfflineSyncStore.ts b/src/features/offline/store/favoritesOfflineSyncStore.ts similarity index 100% rename from src/store/favoritesOfflineSyncStore.ts rename to src/features/offline/store/favoritesOfflineSyncStore.ts diff --git a/src/store/offlineJobStore.test.ts b/src/features/offline/store/offlineJobStore.test.ts similarity index 98% rename from src/store/offlineJobStore.test.ts rename to src/features/offline/store/offlineJobStore.test.ts index 9a8cf0d9..50bb607c 100644 --- a/src/store/offlineJobStore.test.ts +++ b/src/features/offline/store/offlineJobStore.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { onInvoke } from '@/test/mocks/tauri'; -import { useOfflineJobStore, cancelledDownloads, type DownloadJob } from './offlineJobStore'; +import { useOfflineJobStore, cancelledDownloads, type DownloadJob } from '@/features/offline/store/offlineJobStore'; function job(over: Partial): DownloadJob { return { diff --git a/src/store/offlineJobStore.ts b/src/features/offline/store/offlineJobStore.ts similarity index 100% rename from src/store/offlineJobStore.ts rename to src/features/offline/store/offlineJobStore.ts diff --git a/src/store/offlineStore.ts b/src/features/offline/store/offlineStore.ts similarity index 93% rename from src/store/offlineStore.ts rename to src/features/offline/store/offlineStore.ts index d3cb5501..fc32d4ce 100644 --- a/src/store/offlineStore.ts +++ b/src/features/offline/store/offlineStore.ts @@ -1,30 +1,30 @@ -import { libraryUpsertSongsFromApi } from '../api/library'; -import { buildStreamUrl } from '../api/subsonicStreamUrl'; -import { getAlbum } from '../api/subsonicLibrary'; -import { getArtist } from '../api/subsonicArtists'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import { libraryUpsertSongsFromApi } from '@/api/library'; +import { buildStreamUrl } from '@/api/subsonicStreamUrl'; +import { getAlbum } from '@/api/subsonicLibrary'; +import { getArtist } from '@/api/subsonicArtists'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { invoke } from '@tauri-apps/api/core'; -import { useAuthStore } from './authStore'; -import { showToast } from '../utils/ui/toast'; -import { useOfflineJobStore, cancelledDownloads } from './offlineJobStore'; -import { useLocalPlaybackStore, type PinSource } from './localPlaybackStore'; -import { getMediaDir } from '../utils/media/mediaDir'; +import { useAuthStore } from '@/store/authStore'; +import { showToast } from '@/utils/ui/toast'; +import { useOfflineJobStore, cancelledDownloads } from '@/features/offline/store/offlineJobStore'; +import { useLocalPlaybackStore, type PinSource } from '@/store/localPlaybackStore'; +import { getMediaDir } from '@/utils/media/mediaDir'; import { findLocalPlaybackEntry, isOfflinePinComplete, pendingOfflinePinSongs, -} from '../utils/offline/offlineLibraryHelpers'; -import { librarySqlServerId } from '../api/coverCache'; -import { resolveIndexKey, serverIndexKeyForProfile } from '../utils/server/serverIndexKey'; -import { isSmartPlaylistName } from '../utils/componentHelpers/playlistDetailHelpers'; +} from '@/features/offline/utils/offlineLibraryHelpers'; +import { librarySqlServerId } from '@/api/coverCache'; +import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey'; +import { isSmartPlaylistName } from '@/utils/componentHelpers/playlistDetailHelpers'; import { enqueueOfflinePin, registerOfflinePinExecutor, removeOfflinePinTask, type OfflinePinTask, -} from '../utils/offline/offlinePinQueue'; +} from '@/features/offline/utils/offlinePinQueue'; /** @deprecated Metadata lives in the library index; kept for type-compat during transition. */ export interface OfflineTrackMeta { @@ -60,7 +60,7 @@ export interface OfflineAlbumMeta { type?: 'album' | 'playlist' | 'artist' | 'track'; } -export type { DownloadJob } from './offlineJobStore'; +export type { DownloadJob } from '@/features/offline/store/offlineJobStore'; function serverIndexKeyForOffline(serverId: string): string { const server = useAuthStore.getState().servers.find(s => s.id === serverId); diff --git a/src/store/zipDownloadStore.ts b/src/features/offline/store/zipDownloadStore.ts similarity index 100% rename from src/store/zipDownloadStore.ts rename to src/features/offline/store/zipDownloadStore.ts diff --git a/src/utils/offline/favoritesOfflineBrowse.test.ts b/src/features/offline/utils/favoritesOfflineBrowse.test.ts similarity index 95% rename from src/utils/offline/favoritesOfflineBrowse.test.ts rename to src/features/offline/utils/favoritesOfflineBrowse.test.ts index 8d1ea4e5..6d86e538 100644 --- a/src/utils/offline/favoritesOfflineBrowse.test.ts +++ b/src/features/offline/utils/favoritesOfflineBrowse.test.ts @@ -1,28 +1,28 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { favoritesOfflineBrowseEnabled, hasOfflineBrowsingContent, -} from './favoritesOfflineBrowse'; +} from '@/features/offline/utils/favoritesOfflineBrowse'; import { isOfflineSidebarLibraryNavAllowed, isOfflineSidebarNavAllowed, isOfflineSidebarSystemNavAllowed, -} from './offlineNavPolicy'; +} from '@/features/offline/utils/offlineNavPolicy'; import { loadStarredFromLibraryIndex, mergeStarredFromServers, -} from './offlineStarredLoad'; -import { resolveAlbumForServer } from './offlineMediaResolve'; +} from '@/features/offline/utils/offlineStarredLoad'; +import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaResolve'; const isActiveServerReachableMock = vi.fn(() => true); const shouldAttemptSubsonicForServerMock = vi.fn((_serverId: string, _trackId?: string) => true); -vi.mock('../network/activeServerReachability', () => ({ +vi.mock('@/utils/network/activeServerReachability', () => ({ isActiveServerReachable: () => isActiveServerReachableMock(), })); -vi.mock('../network/subsonicNetworkGuard', () => ({ +vi.mock('@/utils/network/subsonicNetworkGuard', () => ({ shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) => shouldAttemptSubsonicForServerMock(serverId, trackId), })); @@ -32,11 +32,11 @@ const libraryAdvancedSearchMock = vi.fn(); const libraryGetTracksByAlbumMock = vi.fn(); const libraryGetTracksBatchChunkedMock = vi.fn(); -vi.mock('../../api/subsonicLibrary', () => ({ +vi.mock('@/api/subsonicLibrary', () => ({ getAlbumForServer: (...args: unknown[]) => getAlbumForServerMock(...args), })); -vi.mock('../../api/library', () => ({ +vi.mock('@/api/library', () => ({ libraryAdvancedSearch: (...args: unknown[]) => libraryAdvancedSearchMock(...args), libraryGetTracksByAlbum: (...args: unknown[]) => libraryGetTracksByAlbumMock(...args), libraryGetTracksBatchChunked: (...args: unknown[]) => libraryGetTracksBatchChunkedMock(...args), diff --git a/src/utils/offline/favoritesOfflineBrowse.ts b/src/features/offline/utils/favoritesOfflineBrowse.ts similarity index 75% rename from src/utils/offline/favoritesOfflineBrowse.ts rename to src/features/offline/utils/favoritesOfflineBrowse.ts index a1e98ae9..f95477b4 100644 --- a/src/utils/offline/favoritesOfflineBrowse.ts +++ b/src/features/offline/utils/favoritesOfflineBrowse.ts @@ -1,7 +1,7 @@ -import { useAuthStore } from '../../store/authStore'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import type { OfflineAlbumMeta } from '../../store/offlineStore'; -import { countFavoriteAutoTracks, hasAnyOfflineAlbums } from './offlineLibraryHelpers'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; +import { countFavoriteAutoTracks, hasAnyOfflineAlbums } from '@/features/offline/utils/offlineLibraryHelpers'; /** Saved servers with a local library index (cross-server favorites scope). */ export function favoritesServerIds(): string[] { diff --git a/src/utils/offline/favoritesOfflineConstants.test.ts b/src/features/offline/utils/favoritesOfflineConstants.test.ts similarity index 68% rename from src/utils/offline/favoritesOfflineConstants.test.ts rename to src/features/offline/utils/favoritesOfflineConstants.test.ts index 2c24a0de..0a55fa77 100644 --- a/src/utils/offline/favoritesOfflineConstants.test.ts +++ b/src/features/offline/utils/favoritesOfflineConstants.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { FAVORITES_OFFLINE_JOB_ID } from './favoritesOfflineConstants'; +import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants'; describe('favoritesOfflineConstants', () => { it('uses a stable virtual job id', () => { diff --git a/src/utils/offline/favoritesOfflineConstants.ts b/src/features/offline/utils/favoritesOfflineConstants.ts similarity index 100% rename from src/utils/offline/favoritesOfflineConstants.ts rename to src/features/offline/utils/favoritesOfflineConstants.ts diff --git a/src/utils/offline/favoritesOfflineSync.test.ts b/src/features/offline/utils/favoritesOfflineSync.test.ts similarity index 87% rename from src/utils/offline/favoritesOfflineSync.test.ts rename to src/features/offline/utils/favoritesOfflineSync.test.ts index 5e94afd2..ff6756ae 100644 --- a/src/utils/offline/favoritesOfflineSync.test.ts +++ b/src/features/offline/utils/favoritesOfflineSync.test.ts @@ -1,12 +1,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { useAuthStore } from '../../store/authStore'; -import { useOfflineJobStore } from '../../store/offlineJobStore'; -import { FAVORITES_OFFLINE_JOB_ID } from './favoritesOfflineConstants'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { useOfflineJobStore } from '@/features/offline/store/offlineJobStore'; +import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants'; import { mergeStarredSongsUnion, onFavoritesOfflineStarChange, -} from './favoritesOfflineSync'; +} from '@/features/offline/utils/favoritesOfflineSync'; const getStarredForServerMock = vi.fn(async (_serverId: string) => ({ artists: [], @@ -16,19 +16,19 @@ const getStarredForServerMock = vi.fn(async (_serverId: string) => ({ const isActiveServerReachableMock = vi.fn(() => true); -vi.mock('../network/activeServerReachability', () => ({ +vi.mock('@/utils/network/activeServerReachability', () => ({ isActiveServerReachable: () => isActiveServerReachableMock(), })); -vi.mock('../../api/subsonicStarRating', () => ({ +vi.mock('@/api/subsonicStarRating', () => ({ getStarredForServer: (serverId: string) => getStarredForServerMock(serverId), })); -vi.mock('../../api/subsonicLibrary', () => ({ +vi.mock('@/api/subsonicLibrary', () => ({ getAlbumForServer: vi.fn(async () => ({ songs: [] })), })); -vi.mock('../../api/subsonicArtists', () => ({ +vi.mock('@/api/subsonicArtists', () => ({ getArtistForServer: vi.fn(async () => ({ albums: [] })), })); diff --git a/src/utils/offline/favoritesOfflineSync.ts b/src/features/offline/utils/favoritesOfflineSync.ts similarity index 91% rename from src/utils/offline/favoritesOfflineSync.ts rename to src/features/offline/utils/favoritesOfflineSync.ts index f6af0fd4..98f874df 100644 --- a/src/utils/offline/favoritesOfflineSync.ts +++ b/src/features/offline/utils/favoritesOfflineSync.ts @@ -1,27 +1,27 @@ -import { libraryUpsertSongsFromApi } from '../../api/library'; -import { librarySqlServerId } from '../../api/coverCache'; -import { getAlbumForServer } from '../../api/subsonicLibrary'; -import { getArtistForServer } from '../../api/subsonicArtists'; -import { getStarredForServer } from '../../api/subsonicStarRating'; -import { buildStreamUrlForServer } from '../../api/subsonicStreamUrl'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import { libraryUpsertSongsFromApi } from '@/api/library'; +import { librarySqlServerId } from '@/api/coverCache'; +import { getAlbumForServer } from '@/api/subsonicLibrary'; +import { getArtistForServer } from '@/api/subsonicArtists'; +import { getStarredForServer } from '@/api/subsonicStarRating'; +import { buildStreamUrlForServer } from '@/api/subsonicStreamUrl'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { invoke } from '@tauri-apps/api/core'; -import i18n from '../../i18n'; -import { useAuthStore } from '../../store/authStore'; -import { cancelledDownloads, useOfflineJobStore } from '../../store/offlineJobStore'; -import { useFavoritesOfflineSyncStore } from '../../store/favoritesOfflineSyncStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { getMediaDir } from '../media/mediaDir'; -import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; -import { FAVORITES_OFFLINE_JOB_ID } from './favoritesOfflineConstants'; -import { isActiveServerReachable } from '../network/activeServerReachability'; -import { favoritesServerIds } from './favoritesOfflineBrowse'; -import { loadAlbumFromLibraryIndex } from './offlineLibraryIndexLoad'; +import i18n from '@/i18n'; +import { useAuthStore } from '@/store/authStore'; +import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore'; +import { useFavoritesOfflineSyncStore } from '@/features/offline/store/favoritesOfflineSyncStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { getMediaDir } from '@/utils/media/mediaDir'; +import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey'; +import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants'; +import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; +import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse'; +import { loadAlbumFromLibraryIndex } from '@/features/offline/utils/offlineLibraryIndexLoad'; import { entryBelongsToServer, hasLocalLibraryBytes, hasLocalFavoriteAutoBytes, -} from './offlineLibraryHelpers'; +} from '@/features/offline/utils/offlineLibraryHelpers'; const CONCURRENCY = 2; const DEBOUNCE_MS = 600; diff --git a/src/utils/migrations/legacyOfflineFileMigration.test.ts b/src/features/offline/utils/legacyOfflineFileMigration.test.ts similarity index 88% rename from src/utils/migrations/legacyOfflineFileMigration.test.ts rename to src/features/offline/utils/legacyOfflineFileMigration.test.ts index 1be15d42..94e9f42e 100644 --- a/src/utils/migrations/legacyOfflineFileMigration.test.ts +++ b/src/features/offline/utils/legacyOfflineFileMigration.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; -import { useOfflineStore } from '../../store/offlineStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; +import { useAuthStore } from '@/store/authStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { entryNeedsFileRelocation, restoreOfflineLibraryPinSources, -} from './legacyOfflineFileMigration'; -import type { LocalPlaybackEntry } from '../../store/localPlaybackStore'; +} from '@/features/offline/utils/legacyOfflineFileMigration'; +import type { LocalPlaybackEntry } from '@/store/localPlaybackStore'; function entry(overrides: Partial): LocalPlaybackEntry { return { diff --git a/src/utils/migrations/legacyOfflineFileMigration.ts b/src/features/offline/utils/legacyOfflineFileMigration.ts similarity index 92% rename from src/utils/migrations/legacyOfflineFileMigration.ts rename to src/features/offline/utils/legacyOfflineFileMigration.ts index 9b549a48..fca156e6 100644 --- a/src/utils/migrations/legacyOfflineFileMigration.ts +++ b/src/features/offline/utils/legacyOfflineFileMigration.ts @@ -1,13 +1,13 @@ import { invoke } from '@tauri-apps/api/core'; -import { libraryGetTracksBatch } from '../../api/library'; -import { useAuthStore } from '../../store/authStore'; -import { useOfflineStore, type OfflineAlbumMeta } from '../../store/offlineStore'; -import { useLocalPlaybackStore, type LocalPlaybackEntry, type PinSource } from '../../store/localPlaybackStore'; -import { localPlaybackEntryKey } from '../../store/localPlaybackKeys'; -import { importLegacyLocalPlayback } from '../../store/localPlaybackMigration'; -import { getMediaDir } from '../media/mediaDir'; -import { resolveServerIdForIndexKey } from '../server/serverLookup'; -import { resolveIndexKey } from '../server/serverIndexKey'; +import { libraryGetTracksBatch } from '@/api/library'; +import { useAuthStore } from '@/store/authStore'; +import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; +import { useLocalPlaybackStore, type LocalPlaybackEntry, type PinSource } from '@/store/localPlaybackStore'; +import { localPlaybackEntryKey } from '@/store/localPlaybackKeys'; +import { importLegacyLocalPlayback } from '@/store/localPlaybackMigration'; +import { getMediaDir } from '@/utils/media/mediaDir'; +import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup'; +import { resolveIndexKey } from '@/utils/server/serverIndexKey'; interface LegacyOfflineMigrationResult { trackId: string; diff --git a/src/utils/offline/libraryTierReconcile.ts b/src/features/offline/utils/libraryTierReconcile.ts similarity index 93% rename from src/utils/offline/libraryTierReconcile.ts rename to src/features/offline/utils/libraryTierReconcile.ts index 88bd6c48..3d319278 100644 --- a/src/utils/offline/libraryTierReconcile.ts +++ b/src/features/offline/utils/libraryTierReconcile.ts @@ -1,17 +1,17 @@ -import { libraryUpsertSongsFromApi } from '../../api/library'; -import { librarySqlServerId } from '../../api/coverCache'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import { libraryUpsertSongsFromApi } from '@/api/library'; +import { librarySqlServerId } from '@/api/coverCache'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { invoke } from '@tauri-apps/api/core'; -import { useAuthStore } from '../../store/authStore'; -import type { LocalPlaybackEntry, PinSource } from '../../store/localPlaybackStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { getMediaDir } from '../media/mediaDir'; -import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; +import { useAuthStore } from '@/store/authStore'; +import type { LocalPlaybackEntry, PinSource } from '@/store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { getMediaDir } from '@/utils/media/mediaDir'; +import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey'; import { entryBelongsToServer, findLocalPlaybackEntry, indexKeyBelongsToServer, -} from './offlineLibraryHelpers'; +} from '@/features/offline/utils/offlineLibraryHelpers'; interface LibraryTrackProbeResult { path: string; diff --git a/src/utils/offline/offlineActionPolicy.test.ts b/src/features/offline/utils/offlineActionPolicy.test.ts similarity index 93% rename from src/utils/offline/offlineActionPolicy.test.ts rename to src/features/offline/utils/offlineActionPolicy.test.ts index e2e24988..8c0dfa89 100644 --- a/src/utils/offline/offlineActionPolicy.test.ts +++ b/src/features/offline/utils/offlineActionPolicy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { offlineActionPolicy } from './offlineActionPolicy'; +import { offlineActionPolicy } from '@/features/offline/utils/offlineActionPolicy'; describe('offlineActionPolicy', () => { it('allows all mutations when offline browse is inactive', () => { diff --git a/src/utils/offline/offlineActionPolicy.ts b/src/features/offline/utils/offlineActionPolicy.ts similarity index 100% rename from src/utils/offline/offlineActionPolicy.ts rename to src/features/offline/utils/offlineActionPolicy.ts diff --git a/src/utils/offline/offlineAlbumBrowseCatalog.ts b/src/features/offline/utils/offlineAlbumBrowseCatalog.ts similarity index 82% rename from src/utils/offline/offlineAlbumBrowseCatalog.ts rename to src/features/offline/utils/offlineAlbumBrowseCatalog.ts index d5eb4a8e..d265aa79 100644 --- a/src/utils/offline/offlineAlbumBrowseCatalog.ts +++ b/src/features/offline/utils/offlineAlbumBrowseCatalog.ts @@ -1,10 +1,10 @@ -import type { SubsonicAlbum } from '../../api/subsonicTypes'; -import type { AlbumBrowseQuery } from '../library/albumBrowseTypes'; -import { isOfflineBrowseActive } from './offlineBrowseMode'; +import type { SubsonicAlbum } from '@/api/subsonicTypes'; +import type { AlbumBrowseQuery } from '@/utils/library/albumBrowseTypes'; +import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; import { fetchOfflineLocalAlbumCatalogChunk, offlineLocalBrowseEnabled, -} from './offlineLocalBrowse'; +} from '@/features/offline/utils/offlineLocalBrowse'; type OfflineAlbumCatalogChunk = { albums: SubsonicAlbum[]; diff --git a/src/utils/offline/offlineBrowseContext.test.ts b/src/features/offline/utils/offlineBrowseContext.test.ts similarity index 83% rename from src/utils/offline/offlineBrowseContext.test.ts rename to src/features/offline/utils/offlineBrowseContext.test.ts index 284d21e0..a8d514ff 100644 --- a/src/utils/offline/offlineBrowseContext.test.ts +++ b/src/features/offline/utils/offlineBrowseContext.test.ts @@ -1,24 +1,24 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { buildOfflineBrowseContext, computeOfflineBrowseCapabilities, offlineBrowseNavFlags, -} from './offlineBrowseContext'; +} from '@/features/offline/utils/offlineBrowseContext'; -vi.mock('./offlineLocalBrowse', () => ({ +vi.mock('@/features/offline/utils/offlineLocalBrowse', () => ({ offlineLocalBrowseEnabled: vi.fn(() => false), countLocalBrowsableTracks: vi.fn(() => 0), })); -vi.mock('./offlinePlaylistBrowse', () => ({ +vi.mock('@/features/offline/utils/offlinePlaylistBrowse', () => ({ playlistsOfflineBrowseEnabled: vi.fn(() => false), })); -import { offlineLocalBrowseEnabled } from './offlineLocalBrowse'; -import { playlistsOfflineBrowseEnabled } from './offlinePlaylistBrowse'; +import { offlineLocalBrowseEnabled } from '@/features/offline/utils/offlineLocalBrowse'; +import { playlistsOfflineBrowseEnabled } from '@/features/offline/utils/offlinePlaylistBrowse'; describe('offlineBrowseContext', () => { beforeEach(() => { diff --git a/src/utils/offline/offlineBrowseContext.ts b/src/features/offline/utils/offlineBrowseContext.ts similarity index 81% rename from src/utils/offline/offlineBrowseContext.ts rename to src/features/offline/utils/offlineBrowseContext.ts index d5af55a2..c6030c5f 100644 --- a/src/utils/offline/offlineBrowseContext.ts +++ b/src/features/offline/utils/offlineBrowseContext.ts @@ -1,10 +1,10 @@ -import type { ConnectionStatus } from '../../hooks/useConnectionStatus'; -import type { OfflineAlbumMeta } from '../../store/offlineStore'; -import { favoritesOfflineBrowseEnabled } from './favoritesOfflineBrowse'; -import { hasOfflineBrowseCapability } from './offlineBrowseRouting'; -import { offlineLocalBrowseEnabled } from './offlineLocalBrowse'; -import { playlistsOfflineBrowseEnabled } from './offlinePlaylistBrowse'; -import { hasAnyOfflineAlbums } from './offlineLibraryHelpers'; +import type { ConnectionStatus } from '@/hooks/useConnectionStatus'; +import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; +import { favoritesOfflineBrowseEnabled } from '@/features/offline/utils/favoritesOfflineBrowse'; +import { hasOfflineBrowseCapability } from '@/features/offline/utils/offlineBrowseRouting'; +import { offlineLocalBrowseEnabled } from '@/features/offline/utils/offlineLocalBrowse'; +import { playlistsOfflineBrowseEnabled } from '@/features/offline/utils/offlinePlaylistBrowse'; +import { hasAnyOfflineAlbums } from '@/features/offline/utils/offlineLibraryHelpers'; export type OfflineBrowseCapabilities = { localLibrary: boolean; diff --git a/src/utils/offline/offlineBrowseMode.test.ts b/src/features/offline/utils/offlineBrowseMode.test.ts similarity index 69% rename from src/utils/offline/offlineBrowseMode.test.ts rename to src/features/offline/utils/offlineBrowseMode.test.ts index a25e081d..4700ba88 100644 --- a/src/utils/offline/offlineBrowseMode.test.ts +++ b/src/features/offline/utils/offlineBrowseMode.test.ts @@ -1,8 +1,8 @@ import { renderHook, act } from '@testing-library/react'; import { describe, expect, it, beforeEach } from 'vitest'; -import { useDevOfflineBrowseStore } from '../../store/devOfflineBrowseStore'; -import { resetActiveServerConnectionSnapshot } from '../network/activeServerReachability'; -import { useOfflineBrowseActive } from './offlineBrowseMode'; +import { useDevOfflineBrowseStore } from '@/features/offline/store/devOfflineBrowseStore'; +import { resetActiveServerConnectionSnapshot } from '@/utils/network/activeServerReachability'; +import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; describe('useOfflineBrowseActive', () => { beforeEach(() => { diff --git a/src/utils/offline/offlineBrowseMode.ts b/src/features/offline/utils/offlineBrowseMode.ts similarity index 81% rename from src/utils/offline/offlineBrowseMode.ts rename to src/features/offline/utils/offlineBrowseMode.ts index cddffd4d..babe822a 100644 --- a/src/utils/offline/offlineBrowseMode.ts +++ b/src/features/offline/utils/offlineBrowseMode.ts @@ -1,9 +1,9 @@ import { isDevOfflineBrowseForced, useDevOfflineBrowseStore, -} from '../../store/devOfflineBrowseStore'; -import { useConnectionStatus } from '../../hooks/useConnectionStatus'; -import { isActiveServerReachable } from '../network/activeServerReachability'; +} from '@/features/offline/store/devOfflineBrowseStore'; +import { useConnectionStatus } from '@/hooks/useConnectionStatus'; +import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; /** True when browse/detail pages should use local-bytes-only data sources. */ export function isOfflineBrowseActive(): boolean { diff --git a/src/utils/offline/offlineBrowseRouting.test.ts b/src/features/offline/utils/offlineBrowseRouting.test.ts similarity index 97% rename from src/utils/offline/offlineBrowseRouting.test.ts rename to src/features/offline/utils/offlineBrowseRouting.test.ts index 324a0454..1bf2ec6a 100644 --- a/src/utils/offline/offlineBrowseRouting.test.ts +++ b/src/features/offline/utils/offlineBrowseRouting.test.ts @@ -3,7 +3,7 @@ import { hasOfflineBrowseCapability, isPathOfflineBrowsable, resolveOfflineDisconnectNavAction, -} from './offlineBrowseRouting'; +} from '@/features/offline/utils/offlineBrowseRouting'; describe('offlineBrowseRouting', () => { it('hasOfflineBrowseCapability is true when any offline surface exists', () => { diff --git a/src/utils/offline/offlineBrowseRouting.ts b/src/features/offline/utils/offlineBrowseRouting.ts similarity index 96% rename from src/utils/offline/offlineBrowseRouting.ts rename to src/features/offline/utils/offlineBrowseRouting.ts index 28d11224..0b52a709 100644 --- a/src/utils/offline/offlineBrowseRouting.ts +++ b/src/features/offline/utils/offlineBrowseRouting.ts @@ -1,4 +1,4 @@ -import { isOfflineSidebarNavAllowed } from './offlineNavPolicy'; +import { isOfflineSidebarNavAllowed } from '@/features/offline/utils/offlineNavPolicy'; /** Any offline browse surface the disconnect fork may use. */ export function hasOfflineBrowseCapability( diff --git a/src/utils/offline/offlineLibraryFilterSuspend.test.ts b/src/features/offline/utils/offlineLibraryFilterSuspend.test.ts similarity index 90% rename from src/utils/offline/offlineLibraryFilterSuspend.test.ts rename to src/features/offline/utils/offlineLibraryFilterSuspend.test.ts index 2572d106..763145d8 100644 --- a/src/utils/offline/offlineLibraryFilterSuspend.test.ts +++ b/src/features/offline/utils/offlineLibraryFilterSuspend.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { resetOfflineLibraryFilterSuspendState, restoreMusicLibraryFiltersAfterOffline, suspendMusicLibraryFiltersForOffline, -} from './offlineLibraryFilterSuspend'; +} from '@/features/offline/utils/offlineLibraryFilterSuspend'; describe('offlineLibraryFilterSuspend', () => { beforeEach(() => { diff --git a/src/utils/offline/offlineLibraryFilterSuspend.ts b/src/features/offline/utils/offlineLibraryFilterSuspend.ts similarity index 95% rename from src/utils/offline/offlineLibraryFilterSuspend.ts rename to src/features/offline/utils/offlineLibraryFilterSuspend.ts index 35702fc0..70bc43a4 100644 --- a/src/utils/offline/offlineLibraryFilterSuspend.ts +++ b/src/features/offline/utils/offlineLibraryFilterSuspend.ts @@ -1,4 +1,4 @@ -import { useAuthStore } from '../../store/authStore'; +import { useAuthStore } from '@/store/authStore'; let savedFilterByServer: Record | null = null; diff --git a/src/utils/offline/offlineLibraryHelpers.test.ts b/src/features/offline/utils/offlineLibraryHelpers.test.ts similarity index 94% rename from src/utils/offline/offlineLibraryHelpers.test.ts rename to src/features/offline/utils/offlineLibraryHelpers.test.ts index 817804b4..01a768d7 100644 --- a/src/utils/offline/offlineLibraryHelpers.test.ts +++ b/src/features/offline/utils/offlineLibraryHelpers.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineStore } from '../../store/offlineStore'; -import { switchActiveServer } from '../server/switchActiveServer'; +import { useAuthStore } from '@/store/authStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { switchActiveServer } from '@/utils/server/switchActiveServer'; import { buildOfflineCacheQueueTracks, countFavoriteAutoTracks, @@ -15,17 +15,17 @@ import { offlineAlbumCoverScope, offlineTrackCount, type OfflineLibraryCard, -} from './offlineLibraryHelpers'; -import * as libraryApi from '../../api/library'; -import { coverStorageKey } from '../../cover/storageKeys'; -import { resolveCoverDisplayTier } from '../../cover/tiers'; +} from '@/features/offline/utils/offlineLibraryHelpers'; +import * as libraryApi from '@/api/library'; +import { coverStorageKey } from '@/cover/storageKeys'; +import { resolveCoverDisplayTier } from '@/cover/tiers'; -vi.mock('../server/switchActiveServer', () => ({ +vi.mock('@/utils/server/switchActiveServer', () => ({ switchActiveServer: vi.fn(async () => true), })); -vi.mock('../../api/library', async importOriginal => { - const actual = await importOriginal(); +vi.mock('@/api/library', async importOriginal => { + const actual = await importOriginal(); const libraryGetTracksBatch = vi.fn(); return { ...actual, diff --git a/src/utils/offline/offlineLibraryHelpers.ts b/src/features/offline/utils/offlineLibraryHelpers.ts similarity index 95% rename from src/utils/offline/offlineLibraryHelpers.ts rename to src/features/offline/utils/offlineLibraryHelpers.ts index cdd11915..bfca9012 100644 --- a/src/utils/offline/offlineLibraryHelpers.ts +++ b/src/features/offline/utils/offlineLibraryHelpers.ts @@ -1,16 +1,16 @@ -import type { LibraryTrackDto } from '../../api/library'; -import { libraryGetTrack, libraryGetTracksBatchChunked } from '../../api/library'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import type { CoverServerScope } from '../../cover/types'; -import { useAuthStore } from '../../store/authStore'; -import type { LocalPlaybackEntry, PinnedGroup, PinSource } from '../../store/localPlaybackStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineStore, type OfflineAlbumMeta } from '../../store/offlineStore'; -import { resolveTrackCoverArtId, trackToSong } from '../library/advancedSearchLocal'; -import { canonicalQueueServerKey, resolveIndexKey } from '../server/serverIndexKey'; -import type { Track } from '../../store/playerStoreTypes'; -import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '../server/serverLookup'; -import { serverIndexKeyForProfile } from '../server/serverIndexKey'; +import type { LibraryTrackDto } from '@/api/library'; +import { libraryGetTrack, libraryGetTracksBatchChunked } from '@/api/library'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { CoverServerScope } from '@/cover/types'; +import { useAuthStore } from '@/store/authStore'; +import type { LocalPlaybackEntry, PinnedGroup, PinSource } from '@/store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; +import { resolveTrackCoverArtId, trackToSong } from '@/utils/library/advancedSearchLocal'; +import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey'; +import type { Track } from '@/store/playerStoreTypes'; +import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '@/utils/server/serverLookup'; +import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey'; export interface OfflineLibraryCard { serverIndexKey: string; @@ -165,7 +165,7 @@ export async function syncAlbumLibraryTierFromDisk( songs: SubsonicSong[], pinSource?: PinSource, ): Promise { - const { reconcileLibraryTierForAlbum } = await import('./libraryTierReconcile'); + const { reconcileLibraryTierForAlbum } = await import('@/features/offline/utils/libraryTierReconcile'); const result = await reconcileLibraryTierForAlbum(serverId, songs, pinSource ?? { kind: 'album', sourceId: albumId, diff --git a/src/utils/offline/offlineLibraryIndexLoad.ts b/src/features/offline/utils/offlineLibraryIndexLoad.ts similarity index 96% rename from src/utils/offline/offlineLibraryIndexLoad.ts rename to src/features/offline/utils/offlineLibraryIndexLoad.ts index 7555faf8..0079a5cb 100644 --- a/src/utils/offline/offlineLibraryIndexLoad.ts +++ b/src/features/offline/utils/offlineLibraryIndexLoad.ts @@ -1,14 +1,14 @@ -import { libraryAdvancedSearch, libraryGetTracksByAlbum } from '../../api/library'; +import { libraryAdvancedSearch, libraryGetTracksByAlbum } from '@/api/library'; import type { SubsonicAlbum, SubsonicArtist, SubsonicSong, -} from '../../api/subsonicTypes'; +} from '@/api/subsonicTypes'; import { albumToAlbum, artistToArtist, trackToSong, -} from '../library/advancedSearchLocal'; +} from '@/utils/library/advancedSearchLocal'; export async function loadAlbumFromLibraryIndex( serverId: string, diff --git a/src/utils/offline/offlineLocalBrowse.test.ts b/src/features/offline/utils/offlineLocalBrowse.test.ts similarity index 89% rename from src/utils/offline/offlineLocalBrowse.test.ts rename to src/features/offline/utils/offlineLocalBrowse.test.ts index a08c1f1b..4b6e6555 100644 --- a/src/utils/offline/offlineLocalBrowse.test.ts +++ b/src/features/offline/utils/offlineLocalBrowse.test.ts @@ -1,19 +1,19 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { LibraryTrackDto } from '../../api/library'; -import { useAuthStore } from '../../store/authStore'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; +import type { LibraryTrackDto } from '@/api/library'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { countLocalBrowsableTracks, fetchOfflineLocalBrowsableSongPage, offlineLocalBrowseEnabled, -} from './offlineLocalBrowse'; +} from '@/features/offline/utils/offlineLocalBrowse'; const { libraryGetTracksBatchChunkedMock } = vi.hoisted(() => ({ libraryGetTracksBatchChunkedMock: vi.fn(async (): Promise => []), })); -vi.mock('../../api/library', () => ({ +vi.mock('@/api/library', () => ({ libraryGetTracksBatchChunked: libraryGetTracksBatchChunkedMock, libraryGetTracksByAlbum: vi.fn(async () => []), libraryAdvancedSearch: vi.fn(async () => ({ albums: [], artists: [], tracks: [] })), diff --git a/src/utils/offline/offlineLocalBrowse.ts b/src/features/offline/utils/offlineLocalBrowse.ts similarity index 92% rename from src/utils/offline/offlineLocalBrowse.ts rename to src/features/offline/utils/offlineLocalBrowse.ts index cda28336..47557e9f 100644 --- a/src/utils/offline/offlineLocalBrowse.ts +++ b/src/features/offline/utils/offlineLocalBrowse.ts @@ -1,26 +1,26 @@ -import type { LibraryTrackDto } from '../../api/library'; -import { libraryAdvancedSearch, libraryGetTracksBatchChunked, libraryGetTracksByAlbum } from '../../api/library'; -import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subsonicTypes'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import type { LocalPlaybackEntry } from '../../store/localPlaybackStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; +import type { LibraryTrackDto } from '@/api/library'; +import { libraryAdvancedSearch, libraryGetTracksBatchChunked, libraryGetTracksByAlbum } from '@/api/library'; +import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/api/subsonicTypes'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import type { LocalPlaybackEntry } from '@/store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { albumToAlbum, artistToArtist, resolveTrackCoverArtId, trackToSong, -} from '../library/advancedSearchLocal'; -import { albumIsCompilationFromTrackDtos } from '../library/albumCompilation'; +} from '@/utils/library/advancedSearchLocal'; +import { albumIsCompilationFromTrackDtos } from '@/utils/library/albumCompilation'; import { filterAlbumsByCompilation, filterAlbumsByGenres, filterAlbumsByStarred, filterAlbumsByYearBounds, -} from '../library/albumBrowseFilters'; -import type { AlbumBrowseQuery } from '../library/albumBrowseTypes'; -import { sortSubsonicAlbums } from '../library/albumBrowseSort'; -import { isLosslessSuffix } from '../library/losslessFormats'; -import { entryBelongsToServer } from './offlineLibraryHelpers'; +} from '@/utils/library/albumBrowseFilters'; +import type { AlbumBrowseQuery } from '@/utils/library/albumBrowseTypes'; +import { sortSubsonicAlbums } from '@/utils/library/albumBrowseSort'; +import { isLosslessSuffix } from '@/utils/library/losslessFormats'; +import { entryBelongsToServer } from '@/features/offline/utils/offlineLibraryHelpers'; function sortBrowsableSongs(songs: SubsonicSong[]): SubsonicSong[] { return [...songs].sort((a, b) => a.title.localeCompare(b.title)); diff --git a/src/utils/offline/offlineMediaResolve.test.ts b/src/features/offline/utils/offlineMediaResolve.test.ts similarity index 92% rename from src/utils/offline/offlineMediaResolve.test.ts rename to src/features/offline/utils/offlineMediaResolve.test.ts index bc0f62c1..bf5b0458 100644 --- a/src/utils/offline/offlineMediaResolve.test.ts +++ b/src/features/offline/utils/offlineMediaResolve.test.ts @@ -1,11 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { resolveAlbum, resolveAlbumForActiveServer, resolveArtist, resolvePlaylist, -} from './offlineMediaResolve'; +} from '@/features/offline/utils/offlineMediaResolve'; const isOfflineBrowseActiveMock = vi.fn(() => false); const offlineLocalBrowseEnabledMock = vi.fn((_serverId: string) => false); @@ -21,11 +21,11 @@ const getArtistForServerMock = vi.fn((_serverId: string, _artistId: string) => ( const getPlaylistForServerMock = vi.fn((_serverId: string, _playlistId: string) => ({})); const libraryIsReadyMock = vi.fn(async (_serverId: string) => false); -vi.mock('./offlineBrowseMode', () => ({ +vi.mock('@/features/offline/utils/offlineBrowseMode', () => ({ isOfflineBrowseActive: () => isOfflineBrowseActiveMock(), })); -vi.mock('./offlineLocalBrowse', () => ({ +vi.mock('@/features/offline/utils/offlineLocalBrowse', () => ({ offlineLocalBrowseEnabled: (id: string) => offlineLocalBrowseEnabledMock(id), loadAlbumFromLocalPlayback: (serverId: string, albumId: string) => loadAlbumFromLocalPlaybackMock(serverId, albumId), @@ -33,36 +33,36 @@ vi.mock('./offlineLocalBrowse', () => ({ loadArtistFromLocalPlaybackMock(serverId, artistId), })); -vi.mock('./offlineLibraryIndexLoad', () => ({ +vi.mock('@/features/offline/utils/offlineLibraryIndexLoad', () => ({ loadAlbumFromLibraryIndex: (...args: unknown[]) => loadAlbumFromLibraryIndexMock(...args), loadArtistFromLibraryIndex: (...args: unknown[]) => loadArtistFromLibraryIndexMock(...args), })); -vi.mock('./offlinePlaylistBrowse', () => ({ +vi.mock('@/features/offline/utils/offlinePlaylistBrowse', () => ({ playlistsOfflineBrowseEnabled: (id: string) => playlistsOfflineBrowseEnabledMock(id), loadOfflineBrowsablePlaylist: (playlistId: string, serverId: string) => loadOfflineBrowsablePlaylistMock(playlistId, serverId), })); -vi.mock('../network/subsonicNetworkGuard', () => ({ +vi.mock('@/utils/network/subsonicNetworkGuard', () => ({ shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) => shouldAttemptSubsonicForServerMock(serverId, trackId), })); -vi.mock('../../api/subsonicLibrary', () => ({ +vi.mock('@/api/subsonicLibrary', () => ({ getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId), })); -vi.mock('../../api/subsonicArtists', () => ({ +vi.mock('@/api/subsonicArtists', () => ({ getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId), })); -vi.mock('../../api/subsonicPlaylists', () => ({ +vi.mock('@/api/subsonicPlaylists', () => ({ getPlaylistForServer: (serverId: string, playlistId: string) => getPlaylistForServerMock(serverId, playlistId), })); -vi.mock('../library/libraryReady', () => ({ +vi.mock('@/utils/library/libraryReady', () => ({ libraryIsReady: (serverId: string) => libraryIsReadyMock(serverId), })); diff --git a/src/utils/offline/offlineMediaResolve.ts b/src/features/offline/utils/offlineMediaResolve.ts similarity index 83% rename from src/utils/offline/offlineMediaResolve.ts rename to src/features/offline/utils/offlineMediaResolve.ts index 96aa71d3..808f4b20 100644 --- a/src/utils/offline/offlineMediaResolve.ts +++ b/src/features/offline/utils/offlineMediaResolve.ts @@ -1,29 +1,29 @@ -import { getAlbumForServer } from '../../api/subsonicLibrary'; -import { getArtistForServer } from '../../api/subsonicArtists'; -import { getPlaylistForServer } from '../../api/subsonicPlaylists'; +import { getAlbumForServer } from '@/api/subsonicLibrary'; +import { getArtistForServer } from '@/api/subsonicArtists'; +import { getPlaylistForServer } from '@/api/subsonicPlaylists'; import type { SubsonicAlbum, SubsonicArtist, SubsonicPlaylist, SubsonicSong, -} from '../../api/subsonicTypes'; -import { useAuthStore } from '../../store/authStore'; -import { shouldAttemptSubsonicForServer } from '../network/subsonicNetworkGuard'; -import { isOfflineBrowseActive } from './offlineBrowseMode'; -import { libraryIsReady } from '../library/libraryReady'; +} from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; +import { libraryIsReady } from '@/utils/library/libraryReady'; import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex, -} from './offlineLibraryIndexLoad'; +} from '@/features/offline/utils/offlineLibraryIndexLoad'; import { loadAlbumFromLocalPlayback, loadArtistFromLocalPlayback, offlineLocalBrowseEnabled, -} from './offlineLocalBrowse'; +} from '@/features/offline/utils/offlineLocalBrowse'; import { loadOfflineBrowsablePlaylist, playlistsOfflineBrowseEnabled, -} from './offlinePlaylistBrowse'; +} from '@/features/offline/utils/offlinePlaylistBrowse'; export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] }; diff --git a/src/utils/offline/offlineNavPolicy.ts b/src/features/offline/utils/offlineNavPolicy.ts similarity index 100% rename from src/utils/offline/offlineNavPolicy.ts rename to src/features/offline/utils/offlineNavPolicy.ts diff --git a/src/utils/offline/offlinePinQueue.test.ts b/src/features/offline/utils/offlinePinQueue.test.ts similarity index 97% rename from src/utils/offline/offlinePinQueue.test.ts rename to src/features/offline/utils/offlinePinQueue.test.ts index 2beafd0b..c5080d8b 100644 --- a/src/utils/offline/offlinePinQueue.test.ts +++ b/src/features/offline/utils/offlinePinQueue.test.ts @@ -1,12 +1,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { cancelledDownloads, useOfflineJobStore } from '../../store/offlineJobStore'; +import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore'; import { clearOfflinePinTasks, dequeueOfflinePin, enqueueOfflinePin, isAlbumPinQueued, registerOfflinePinExecutor, -} from './offlinePinQueue'; +} from '@/features/offline/utils/offlinePinQueue'; describe('offlinePinQueue', () => { beforeEach(() => { diff --git a/src/utils/offline/offlinePinQueue.ts b/src/features/offline/utils/offlinePinQueue.ts similarity index 96% rename from src/utils/offline/offlinePinQueue.ts rename to src/features/offline/utils/offlinePinQueue.ts index 88a34849..8394c7f1 100644 --- a/src/utils/offline/offlinePinQueue.ts +++ b/src/features/offline/utils/offlinePinQueue.ts @@ -1,10 +1,10 @@ -import type { SubsonicSong } from '../../api/subsonicTypes'; -import type { PinSource } from '../../store/localPlaybackStore'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { PinSource } from '@/store/localPlaybackStore'; import { cancelledDownloads, useOfflineJobStore, type OfflinePinQueueEntry, -} from '../../store/offlineJobStore'; +} from '@/features/offline/store/offlineJobStore'; export type OfflinePinKind = PinSource['kind']; diff --git a/src/utils/offline/offlinePlaylistBrowse.test.ts b/src/features/offline/utils/offlinePlaylistBrowse.test.ts similarity index 90% rename from src/utils/offline/offlinePlaylistBrowse.test.ts rename to src/features/offline/utils/offlinePlaylistBrowse.test.ts index 155da394..8a57cd7c 100644 --- a/src/utils/offline/offlinePlaylistBrowse.test.ts +++ b/src/features/offline/utils/offlinePlaylistBrowse.test.ts @@ -1,16 +1,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import * as libraryApi from '../../api/library'; -import { useAuthStore } from '../../store/authStore'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineStore } from '../../store/offlineStore'; +import * as libraryApi from '@/api/library'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; import { fetchOfflineBrowsablePlaylists, loadOfflineBrowsablePlaylist, playlistsOfflineBrowseEnabled, -} from './offlinePlaylistBrowse'; +} from '@/features/offline/utils/offlinePlaylistBrowse'; -vi.mock('../../api/library', () => ({ +vi.mock('@/api/library', () => ({ libraryGetTracksBatchChunked: vi.fn(), })); diff --git a/src/utils/offline/offlinePlaylistBrowse.ts b/src/features/offline/utils/offlinePlaylistBrowse.ts similarity index 87% rename from src/utils/offline/offlinePlaylistBrowse.ts rename to src/features/offline/utils/offlinePlaylistBrowse.ts index 6e8a52c4..77af2675 100644 --- a/src/utils/offline/offlinePlaylistBrowse.ts +++ b/src/features/offline/utils/offlinePlaylistBrowse.ts @@ -1,15 +1,15 @@ -import { libraryGetTracksBatchChunked } from '../../api/library'; -import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; -import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import type { PinnedGroup } from '../../store/localPlaybackStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { trackToSong } from '../library/advancedSearchLocal'; -import { isManualOfflinePlaylist } from './pinnedOfflineSync'; +import { libraryGetTracksBatchChunked } from '@/api/library'; +import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import type { PinnedGroup } from '@/store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { trackToSong } from '@/utils/library/advancedSearchLocal'; +import { isManualOfflinePlaylist } from '@/features/offline/utils/pinnedOfflineSync'; import { hasLocalLibraryBytes, indexKeyBelongsToServer, resolveOfflineAlbumMeta, -} from './offlineLibraryHelpers'; +} from '@/features/offline/utils/offlineLibraryHelpers'; function listPlaylistPinnedGroupsForServer(serverId: string): PinnedGroup[] { return useLocalPlaybackStore.getState() diff --git a/src/utils/offline/offlineStarredLoad.ts b/src/features/offline/utils/offlineStarredLoad.ts similarity index 90% rename from src/utils/offline/offlineStarredLoad.ts rename to src/features/offline/utils/offlineStarredLoad.ts index 71ad51ec..9be9ed44 100644 --- a/src/utils/offline/offlineStarredLoad.ts +++ b/src/features/offline/utils/offlineStarredLoad.ts @@ -1,24 +1,24 @@ -import { getStarredForServer } from '../../api/subsonicStarRating'; -import { libraryAdvancedSearch } from '../../api/library'; +import { getStarredForServer } from '@/api/subsonicStarRating'; +import { libraryAdvancedSearch } from '@/api/library'; import type { StarredResults, SubsonicAlbum, SubsonicArtist, SubsonicSong, -} from '../../api/subsonicTypes'; -import { isActiveServerReachable } from '../network/activeServerReachability'; +} from '@/api/subsonicTypes'; +import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; import { albumToAlbum, trackToSong, -} from '../library/advancedSearchLocal'; -import { dedupeById } from '../dedupeById'; -import { isOfflineBrowseActive } from './offlineBrowseMode'; -import { favoritesServerIds } from './favoritesOfflineBrowse'; +} from '@/utils/library/advancedSearchLocal'; +import { dedupeById } from '@/utils/dedupeById'; +import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode'; +import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse'; import { buildAlbumFromTracks, fetchBrowsableLocalTrackDtos, offlineLocalBrowseEnabled, -} from './offlineLocalBrowse'; +} from '@/features/offline/utils/offlineLocalBrowse'; function tagStarredWithServer(starred: StarredResults, serverId: string): StarredResults { const withServer = (items: T[]): (T & { serverId: string })[] => diff --git a/src/utils/offline/pinnedOfflineSync.test.ts b/src/features/offline/utils/pinnedOfflineSync.test.ts similarity index 94% rename from src/utils/offline/pinnedOfflineSync.test.ts rename to src/features/offline/utils/pinnedOfflineSync.test.ts index 73b1a4bb..18e74ef7 100644 --- a/src/utils/offline/pinnedOfflineSync.test.ts +++ b/src/features/offline/utils/pinnedOfflineSync.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { useAuthStore } from '../../store/authStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineJobStore } from '../../store/offlineJobStore'; -import { useOfflineStore } from '../../store/offlineStore'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineJobStore } from '@/features/offline/store/offlineJobStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; import { isManualOfflinePlaylist, isPlaylistPinnedOffline, @@ -13,8 +13,8 @@ import { scheduleSyncPinnedAlbumsAndArtists, syncAllPinnedPlaylists, syncPinnedArtistIfNeeded, -} from './pinnedOfflineSync'; -import { SMART_PREFIX } from '../componentHelpers/playlistDetailHelpers'; +} from '@/features/offline/utils/pinnedOfflineSync'; +import { SMART_PREFIX } from '@/utils/componentHelpers/playlistDetailHelpers'; const getPlaylistMock = vi.fn(); const getAlbumForServerMock = vi.fn(); @@ -24,31 +24,31 @@ const isReachableMock = vi.fn(() => true); const enqueueMock = vi.fn((_task: unknown) => true); const invokeMock = vi.fn(async (_cmd: string, _args?: unknown) => ({})); -vi.mock('../network/activeServerReachability', () => ({ +vi.mock('@/utils/network/activeServerReachability', () => ({ isActiveServerReachable: () => isReachableMock(), onActiveServerBecameReachable: () => () => {}, })); -vi.mock('../../api/subsonicPlaylists', () => ({ +vi.mock('@/api/subsonicPlaylists', () => ({ getPlaylistForServer: (serverId: string, id: string) => getPlaylistMock(serverId, id), })); -vi.mock('../../api/subsonicLibrary', () => ({ +vi.mock('@/api/subsonicLibrary', () => ({ getAlbumForServer: (serverId: string, id: string) => getAlbumForServerMock(serverId, id), filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs), })); -vi.mock('../../api/subsonicArtists', () => ({ +vi.mock('@/api/subsonicArtists', () => ({ getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId), })); -vi.mock('../../api/library', () => ({ +vi.mock('@/api/library', () => ({ libraryGetTracksByAlbum: vi.fn(async () => []), subscribeLibrarySyncIdle: vi.fn(async () => () => {}), })); -vi.mock('./offlinePinQueue', async (importOriginal) => { - const actual = await importOriginal(); +vi.mock('@/features/offline/utils/offlinePinQueue', async (importOriginal) => { + const actual = await importOriginal(); return { ...actual, enqueueOfflinePin: (task: unknown) => enqueueMock(task), diff --git a/src/utils/offline/pinnedOfflineSync.ts b/src/features/offline/utils/pinnedOfflineSync.ts similarity index 95% rename from src/utils/offline/pinnedOfflineSync.ts rename to src/features/offline/utils/pinnedOfflineSync.ts index d3b202ab..62ac8a82 100644 --- a/src/utils/offline/pinnedOfflineSync.ts +++ b/src/features/offline/utils/pinnedOfflineSync.ts @@ -1,24 +1,24 @@ -import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '../../api/library'; -import { getAlbumForServer, filterSongsToServerLibrary } from '../../api/subsonicLibrary'; -import { getPlaylistForServer } from '../../api/subsonicPlaylists'; -import { getArtistForServer } from '../../api/subsonicArtists'; -import type { SubsonicSong } from '../../api/subsonicTypes'; +import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/api/library'; +import { getAlbumForServer, filterSongsToServerLibrary } from '@/api/subsonicLibrary'; +import { getPlaylistForServer } from '@/api/subsonicPlaylists'; +import { getArtistForServer } from '@/api/subsonicArtists'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { invoke } from '@tauri-apps/api/core'; -import { useAuthStore } from '../../store/authStore'; -import type { PinSource } from '../../store/localPlaybackStore'; -import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineStore } from '../../store/offlineStore'; -import { usePlaylistStore } from '../../store/playlistStore'; -import { isSmartPlaylistName } from '../componentHelpers/playlistDetailHelpers'; -import { getMediaDir } from '../media/mediaDir'; +import { useAuthStore } from '@/store/authStore'; +import type { PinSource } from '@/store/localPlaybackStore'; +import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { usePlaylistStore } from '@/store/playlistStore'; +import { isSmartPlaylistName } from '@/utils/componentHelpers/playlistDetailHelpers'; +import { getMediaDir } from '@/utils/media/mediaDir'; import { isActiveServerReachable, onActiveServerBecameReachable, -} from '../network/activeServerReachability'; -import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; -import { resolveServerIdForIndexKey } from '../server/serverLookup'; -import { findLocalPlaybackEntry } from './offlineLibraryHelpers'; -import { enqueueOfflinePin } from './offlinePinQueue'; +} from '@/utils/network/activeServerReachability'; +import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey'; +import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup'; +import { findLocalPlaybackEntry } from '@/features/offline/utils/offlineLibraryHelpers'; +import { enqueueOfflinePin } from '@/features/offline/utils/offlinePinQueue'; export type OfflinePinKind = PinSource['kind']; diff --git a/src/utils/offline/resumeIncompleteOfflinePins.test.ts b/src/features/offline/utils/resumeIncompleteOfflinePins.test.ts similarity index 86% rename from src/utils/offline/resumeIncompleteOfflinePins.test.ts rename to src/features/offline/utils/resumeIncompleteOfflinePins.test.ts index febf700f..7497bb3b 100644 --- a/src/utils/offline/resumeIncompleteOfflinePins.test.ts +++ b/src/features/offline/utils/resumeIncompleteOfflinePins.test.ts @@ -1,29 +1,29 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { useAuthStore } from '../../store/authStore'; -import { useOfflineStore } from '../../store/offlineStore'; -import { resumeIncompleteOfflinePins } from './resumeIncompleteOfflinePins'; +import { useAuthStore } from '@/store/authStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { resumeIncompleteOfflinePins } from '@/features/offline/utils/resumeIncompleteOfflinePins'; const isActiveServerReachableMock = vi.fn(() => true); const isOfflinePinCompleteMock = vi.fn((_albumId: string, _serverId: string) => false); const resolveAlbumForServerMock = vi.fn(); const downloadAlbumMock = vi.fn(); -vi.mock('../network/activeServerReachability', () => ({ +vi.mock('@/utils/network/activeServerReachability', () => ({ isActiveServerReachable: () => isActiveServerReachableMock(), onActiveServerBecameReachable: () => () => {}, })); -vi.mock('./offlineLibraryHelpers', () => ({ +vi.mock('@/features/offline/utils/offlineLibraryHelpers', () => ({ isOfflinePinComplete: (albumId: string, serverId: string) => isOfflinePinCompleteMock(albumId, serverId), })); -vi.mock('./offlineMediaResolve', () => ({ +vi.mock('@/features/offline/utils/offlineMediaResolve', () => ({ resolveAlbumForServer: (serverId: string, albumId: string) => resolveAlbumForServerMock(serverId, albumId), })); -vi.mock('../../api/library', () => ({ +vi.mock('@/api/library', () => ({ libraryGetTracksBatchChunked: vi.fn(async () => []), LIBRARY_TRACKS_BATCH_LIMIT: 100, })); diff --git a/src/utils/offline/resumeIncompleteOfflinePins.ts b/src/features/offline/utils/resumeIncompleteOfflinePins.ts similarity index 77% rename from src/utils/offline/resumeIncompleteOfflinePins.ts rename to src/features/offline/utils/resumeIncompleteOfflinePins.ts index bf27713d..69b6b80f 100644 --- a/src/utils/offline/resumeIncompleteOfflinePins.ts +++ b/src/features/offline/utils/resumeIncompleteOfflinePins.ts @@ -1,15 +1,15 @@ -import { libraryGetTracksBatchChunked } from '../../api/library'; -import { getPlaylist } from '../../api/subsonicPlaylists'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { useAuthStore } from '../../store/authStore'; -import type { OfflineAlbumMeta } from '../../store/offlineStore'; -import { useOfflineStore } from '../../store/offlineStore'; -import { trackToSong } from '../library/advancedSearchLocal'; -import { isActiveServerReachable, onActiveServerBecameReachable } from '../network/activeServerReachability'; -import { shouldAttemptSubsonicForServer } from '../network/subsonicNetworkGuard'; -import { resolveServerIdForIndexKey } from '../server/serverLookup'; -import { isOfflinePinComplete } from './offlineLibraryHelpers'; -import { resolveAlbumForServer } from './offlineMediaResolve'; +import { libraryGetTracksBatchChunked } from '@/api/library'; +import { getPlaylist } from '@/api/subsonicPlaylists'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { useAuthStore } from '@/store/authStore'; +import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore'; +import { useOfflineStore } from '@/features/offline/store/offlineStore'; +import { trackToSong } from '@/utils/library/advancedSearchLocal'; +import { isActiveServerReachable, onActiveServerBecameReachable } from '@/utils/network/activeServerReachability'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup'; +import { isOfflinePinComplete } from '@/features/offline/utils/offlineLibraryHelpers'; +import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaResolve'; const DEBOUNCE_MS = 800; diff --git a/src/features/sidebar/components/Sidebar.tsx b/src/features/sidebar/components/Sidebar.tsx index 3decb2f2..323e938b 100644 --- a/src/features/sidebar/components/Sidebar.tsx +++ b/src/features/sidebar/components/Sidebar.tsx @@ -1,8 +1,8 @@ import { useState, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { usePlayerStore } from '@/store/playerStore'; -import { useOfflineJobStore } from '@/store/offlineJobStore'; -import { clearOfflinePinTasks } from '@/utils/offline/offlinePinQueue'; +import { useOfflineJobStore } from '@/features/offline'; +import { clearOfflinePinTasks } from '@/features/offline'; import { useDeviceSyncJobStore } from '@/features/deviceSync'; import { useAuthStore } from '@/store/authStore'; import { useSidebarStore } from '@/features/sidebar/store/sidebarStore'; @@ -23,9 +23,9 @@ import { useSidebarNewReleasesUnread } from '@/features/sidebar/hooks/useSidebar import { useSidebarNavDnd } from '@/features/sidebar/hooks/useSidebarNavDnd'; import { useSidebarLibraryDropdown } from '@/features/sidebar/hooks/useSidebarLibraryDropdown'; import { useSidebarScrollVisible } from '@/features/sidebar/hooks/useSidebarScrollVisible'; -import { isOfflineSidebarNavAllowed } from '@/utils/offline/offlineNavPolicy'; -import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; -import { offlineBrowseNavFlags } from '@/utils/offline/offlineBrowseContext'; +import { isOfflineSidebarNavAllowed } from '@/features/offline'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineBrowseNavFlags } from '@/features/offline'; import { useSidebarPerfProbe } from '@/features/sidebar/hooks/useSidebarPerfProbe'; import SidebarPerfProbeModal from '@/features/sidebar/components/SidebarPerfProbeModal'; import SidebarNavBody from '@/features/sidebar/components/SidebarNavBody'; diff --git a/src/features/stats/components/StatisticsTabBar.tsx b/src/features/stats/components/StatisticsTabBar.tsx index 687b3782..cf734c3d 100644 --- a/src/features/stats/components/StatisticsTabBar.tsx +++ b/src/features/stats/components/StatisticsTabBar.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled'; export default function StatisticsTabBar() { diff --git a/src/features/stats/pages/Statistics.tsx b/src/features/stats/pages/Statistics.tsx index 0ae0ea8f..974c86f9 100644 --- a/src/features/stats/pages/Statistics.tsx +++ b/src/features/stats/pages/Statistics.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'; import { useAuthStore } from '@/store/authStore'; import { useLocation } from 'react-router-dom'; import { getMusicNetworkRuntime, type RecentTrack, type StatsPeriod, type TopItem } from '@/music-network'; -import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled'; import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel'; diff --git a/src/hooks/useAlbumBrowseData.ts b/src/hooks/useAlbumBrowseData.ts index 31613c76..9a509032 100644 --- a/src/hooks/useAlbumBrowseData.ts +++ b/src/hooks/useAlbumBrowseData.ts @@ -30,13 +30,13 @@ import { ALBUM_YEAR_FILTER_DEBOUNCE_MS, resolveAlbumYearBounds, } from '../utils/library/albumYearFilter'; -import { loadOfflineAlbumBrowseInitial } from '../utils/offline/offlineAlbumBrowseCatalog'; -import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; +import { loadOfflineAlbumBrowseInitial } from '@/features/offline'; +import { useOfflineBrowseReloadToken } from '@/features/offline'; import { fetchAlbumBrowseCatalogChunk, mergeAlbumCatalogChunk, } from '../utils/library/albumBrowseCatalogChunk'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; import { useClientSliceInfiniteScroll } from './useClientSliceInfiniteScroll'; import { useDebouncedValue } from './useDebouncedValue'; import { useInpageScrollSentinel } from './useInpageScrollSentinel'; diff --git a/src/hooks/useAlbumDetailData.ts b/src/hooks/useAlbumDetailData.ts index aa0284ab..76b064c3 100644 --- a/src/hooks/useAlbumDetailData.ts +++ b/src/hooks/useAlbumDetailData.ts @@ -5,17 +5,17 @@ import { useAuthStore } from '../store/authStore'; import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex, -} from '../utils/offline/offlineLibraryIndexLoad'; +} from '@/features/offline'; import { resolveAlbum, resolveArtist, type ResolvedAlbum, -} from '../utils/offline/offlineMediaResolve'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; +} from '@/features/offline'; +import { useOfflineBrowseContext } from '@/features/offline'; import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled, -} from '../utils/offline/offlineLocalBrowse'; +} from '@/features/offline'; import { readDetailServerId } from '../utils/navigation/detailServerScope'; import { libraryIsReady } from '../utils/library/libraryReady'; import { diff --git a/src/hooks/useAlbumOfflineState.test.ts b/src/hooks/useAlbumOfflineState.test.ts index 554e2c08..d288bbaa 100644 --- a/src/hooks/useAlbumOfflineState.test.ts +++ b/src/hooks/useAlbumOfflineState.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { renderHook } from '@testing-library/react'; -import { useOfflineJobStore } from '../store/offlineJobStore'; +import { useOfflineJobStore } from '@/features/offline'; import { useAlbumOfflineState } from './useAlbumOfflineState'; describe('useAlbumOfflineState', () => { diff --git a/src/hooks/useAlbumOfflineState.ts b/src/hooks/useAlbumOfflineState.ts index 2191fba6..3f2c79dd 100644 --- a/src/hooks/useAlbumOfflineState.ts +++ b/src/hooks/useAlbumOfflineState.ts @@ -1,6 +1,6 @@ import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { useOfflineJobStore } from '../store/offlineJobStore'; -import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; +import { useOfflineJobStore } from '@/features/offline'; +import { isOfflinePinComplete } from '@/features/offline'; export type AlbumOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached'; diff --git a/src/hooks/useArtistDetailData.ts b/src/hooks/useArtistDetailData.ts index 3e4f79a7..9ce6c7ac 100644 --- a/src/hooks/useArtistDetailData.ts +++ b/src/hooks/useArtistDetailData.ts @@ -7,9 +7,9 @@ import type { } from '../api/subsonicTypes'; import { useAuthStore } from '../store/authStore'; import { useConnectionStatus } from './useConnectionStatus'; -import { loadArtistFromLibraryIndex } from '../utils/offline/offlineLibraryIndexLoad'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '../utils/offline/offlineLocalBrowse'; +import { loadArtistFromLibraryIndex } from '@/features/offline'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '@/features/offline'; import { readDetailServerId } from '../utils/navigation/detailServerScope'; import { runLocalArtistLosslessBrowse } from '../utils/library/browseTextSearch'; import { isLosslessSuffix } from '../utils/library/losslessFormats'; diff --git a/src/hooks/useArtistOfflineState.test.ts b/src/hooks/useArtistOfflineState.test.ts index b25ec088..8d5c1508 100644 --- a/src/hooks/useArtistOfflineState.test.ts +++ b/src/hooks/useArtistOfflineState.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { renderHook } from '@testing-library/react'; import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { useOfflineJobStore } from '../store/offlineJobStore'; +import { useOfflineJobStore } from '@/features/offline'; import { useArtistOfflineState } from './useArtistOfflineState'; describe('useArtistOfflineState', () => { diff --git a/src/hooks/useArtistOfflineState.ts b/src/hooks/useArtistOfflineState.ts index e88dd0b4..5588ab98 100644 --- a/src/hooks/useArtistOfflineState.ts +++ b/src/hooks/useArtistOfflineState.ts @@ -1,6 +1,6 @@ import { useLocalPlaybackStore } from '../store/localPlaybackStore'; -import { useOfflineJobStore } from '../store/offlineJobStore'; -import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; +import { useOfflineJobStore } from '@/features/offline'; +import { isOfflinePinComplete } from '@/features/offline'; export type ArtistOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached'; diff --git a/src/hooks/useArtistsBrowseCatalog.ts b/src/hooks/useArtistsBrowseCatalog.ts index 81916291..f49348ce 100644 --- a/src/hooks/useArtistsBrowseCatalog.ts +++ b/src/hooks/useArtistsBrowseCatalog.ts @@ -6,13 +6,13 @@ import { fetchLocalArtistCatalogChunk, fetchNetworkStarredArtists, } from '../utils/library/browseTextSearch'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { useOfflineBrowseReloadToken } from '@/features/offline'; import { fetchOfflineLocalArtistCatalogChunk, fetchOfflineLocalStarredArtists, offlineLocalBrowseEnabled, -} from '../utils/offline/offlineLocalBrowse'; +} from '@/features/offline'; /** Local-index artist catalog buffer grows by this many rows per background SQL chunk. */ export const ARTIST_CATALOG_CHUNK_SIZE = 200; diff --git a/src/hooks/useBrowseAlbumTextSearch.ts b/src/hooks/useBrowseAlbumTextSearch.ts index 017d3fc8..d1c80b43 100644 --- a/src/hooks/useBrowseAlbumTextSearch.ts +++ b/src/hooks/useBrowseAlbumTextSearch.ts @@ -8,8 +8,8 @@ import { runLocalBrowseAlbums, runNetworkBrowseAlbums, } from '../utils/library/browseTextSearch'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { offlineLocalBrowseEnabled, searchOfflineLocalAlbums } from '../utils/offline/offlineLocalBrowse'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineLocalBrowseEnabled, searchOfflineLocalAlbums } from '@/features/offline'; /** * Debounced album title search with local-vs-network race when the diff --git a/src/hooks/useBrowseArtistTextSearch.ts b/src/hooks/useBrowseArtistTextSearch.ts index 1a509ae7..97a6236c 100644 --- a/src/hooks/useBrowseArtistTextSearch.ts +++ b/src/hooks/useBrowseArtistTextSearch.ts @@ -9,8 +9,8 @@ import { runNetworkBrowseArtists, type LibrarySearchSurface, } from '../utils/library/browseTextSearch'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { offlineLocalBrowseEnabled, searchOfflineLocalArtists } from '../utils/offline/offlineLocalBrowse'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineLocalBrowseEnabled, searchOfflineLocalArtists } from '@/features/offline'; /** * Debounced artist/composer name search with local-vs-network race when the diff --git a/src/hooks/useConnectionStatus.test.ts b/src/hooks/useConnectionStatus.test.ts index b0193c2f..f0e4f978 100644 --- a/src/hooks/useConnectionStatus.test.ts +++ b/src/hooks/useConnectionStatus.test.ts @@ -18,7 +18,7 @@ vi.mock('@/utils/perf/perfFlags', () => ({ })); import { pingWithCredentialsForProfile } from '@/api/subsonic'; -import { useDevOfflineBrowseStore } from '@/store/devOfflineBrowseStore'; +import { useDevOfflineBrowseStore } from '@/features/offline'; import { resetActiveServerConnectionSnapshot, setConnectionStatus } from '@/utils/network/activeServerReachability'; import { useConnectionStatus } from './useConnectionStatus'; diff --git a/src/hooks/useConnectionStatus.ts b/src/hooks/useConnectionStatus.ts index 94d79b91..5c7b49dd 100644 --- a/src/hooks/useConnectionStatus.ts +++ b/src/hooks/useConnectionStatus.ts @@ -19,7 +19,7 @@ import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { isDevOfflineBrowseForced, useDevOfflineBrowseStore, -} from '../store/devOfflineBrowseStore'; +} from '@/features/offline'; // Backward-compatible re-export for call sites that still import from the hook. export { isLanUrl }; diff --git a/src/hooks/useLyrics.ts b/src/hooks/useLyrics.ts index 5bd07802..1634b2a1 100644 --- a/src/hooks/useLyrics.ts +++ b/src/hooks/useLyrics.ts @@ -8,7 +8,7 @@ import { fetchLyrics, parseLrc, LrcLine } from '../api/lrclib'; import { fetchNeteaselyrics } from '../api/netease'; import { fetchLyricsPlus, hasWordSync } from '../api/lyricsplus'; import { useAuthStore } from '../store/authStore'; -import { useOfflineStore } from '../store/offlineStore'; +import { useOfflineStore } from '@/features/offline'; import { useHotCacheStore } from '../store/hotCacheStore'; import { getCachedLyrics, putCachedLyrics, lyricsCacheKey } from '../utils/cache/lyricsPersistentCache'; export type LyricsSource = 'server' | 'lrclib' | 'netease' | 'embedded' | 'lyricsplus'; diff --git a/src/hooks/usePlaylistsLibraryScopeCounts.ts b/src/hooks/usePlaylistsLibraryScopeCounts.ts index 99c03d03..5ebc2549 100644 --- a/src/hooks/usePlaylistsLibraryScopeCounts.ts +++ b/src/hooks/usePlaylistsLibraryScopeCounts.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { filterSongsToActiveLibrary } from '../api/subsonicLibrary'; import { getPlaylist } from '../api/subsonicPlaylists'; import type { SubsonicPlaylist } from '../api/subsonicTypes'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; +import { useOfflineBrowseContext } from '@/features/offline'; export interface PlaylistsLibraryScopeCountsResult { filteredSongCountByPlaylist: Record; diff --git a/src/hooks/useQueuePanelDrag.ts b/src/hooks/useQueuePanelDrag.ts index e51028bb..0d80bf5a 100644 --- a/src/hooks/useQueuePanelDrag.ts +++ b/src/hooks/useQueuePanelDrag.ts @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { resolveAlbum, resolveMediaServerId } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; import { songToTrack } from '../utils/playback/songToTrack'; import { useDragDrop, registerQueueDragHitTest } from '../contexts/DragDropContext'; import { usePlayerStore } from '../store/playerStore'; diff --git a/src/hooks/useSongBrowseList.test.ts b/src/hooks/useSongBrowseList.test.ts index 51d30d0b..fa2d61d2 100644 --- a/src/hooks/useSongBrowseList.test.ts +++ b/src/hooks/useSongBrowseList.test.ts @@ -18,7 +18,7 @@ vi.mock('../utils/library/advancedSearchLocal', () => ({ runLocalSongBrowse: vi.fn(async () => []), })); -vi.mock('./useOfflineBrowseReloadToken', () => ({ +vi.mock('@/features/offline', () => ({ useOfflineBrowseReloadToken: () => undefined, })); diff --git a/src/hooks/useSongBrowseList.ts b/src/hooks/useSongBrowseList.ts index 67cedb7b..4eeb8d5a 100644 --- a/src/hooks/useSongBrowseList.ts +++ b/src/hooks/useSongBrowseList.ts @@ -14,13 +14,13 @@ import { } from '../utils/library/browseTextSearch'; import { useAuthStore } from '../store/authStore'; import { useLibraryIndexStore } from '../store/libraryIndexStore'; -import { useOfflineBrowseContext } from './useOfflineBrowseContext'; -import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { useOfflineBrowseReloadToken } from '@/features/offline'; import { fetchOfflineLocalBrowsableSongPage, offlineLocalBrowseEnabled, searchOfflineLocalBrowsableSongs, -} from '../utils/offline/offlineLocalBrowse'; +} from '@/features/offline'; const PAGE_SIZE = 50; diff --git a/src/hotCachePrefetch.ts b/src/hotCachePrefetch.ts index 5e06cc10..be81f696 100644 --- a/src/hotCachePrefetch.ts +++ b/src/hotCachePrefetch.ts @@ -28,7 +28,7 @@ import { resetAnalysisPruneState, } from './hotCachePrefetch/analysisPrune'; import { reconcileEphemeralCache } from './utils/cache/ephemeralTierReconcile'; -import { hasLocalPersistentPlaybackBytes } from './utils/offline/offlineLibraryHelpers'; +import { hasLocalPersistentPlaybackBytes } from '@/features/offline'; /** Periodic index↔disk sync (stale rows + empty dirs); unindexed files evicted only on budget pressure. */ const EPHEMERAL_MAINTENANCE_MS = 10 * 60 * 1000; diff --git a/src/localPlaybackInvalidation.ts b/src/localPlaybackInvalidation.ts index 15afa8f6..415ade52 100644 --- a/src/localPlaybackInvalidation.ts +++ b/src/localPlaybackInvalidation.ts @@ -5,8 +5,8 @@ import { useAuthStore } from './store/authStore'; import { useLocalPlaybackStore } from './store/localPlaybackStore'; import { layoutFingerprintFromLibraryTrack } from './utils/media/mediaLayout'; import { getMediaDir } from './utils/media/mediaDir'; -import { runLegacyOfflineFileMigration } from './utils/migrations/legacyOfflineFileMigration'; -import { reconcileLibraryTierForServer } from './utils/offline/libraryTierReconcile'; +import { runLegacyOfflineFileMigration } from '@/features/offline'; +import { reconcileLibraryTierForServer } from '@/features/offline'; import { resolveServerIdForIndexKey } from './utils/server/serverLookup'; import { serverIndexKeyFromUrl } from './utils/server/serverIndexKey'; diff --git a/src/pages/AlbumDetail.tsx b/src/pages/AlbumDetail.tsx index b19542c3..979f0f86 100644 --- a/src/pages/AlbumDetail.tsx +++ b/src/pages/AlbumDetail.tsx @@ -15,15 +15,15 @@ import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior'; import { useAlbumDetailData } from '../hooks/useAlbumDetailData'; import { useAlbumOfflineState } from '../hooks/useAlbumOfflineState'; import { useAlbumDetailSort } from '../hooks/useAlbumDetailSort'; -import { useDownloadModalStore } from '../store/downloadModalStore'; -import { useOfflineStore } from '../store/offlineStore'; -import { useOfflineJobStore } from '../store/offlineJobStore'; -import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; -import { dequeueOfflinePin } from '../utils/offline/offlinePinQueue'; -import { reconcileLibraryTierForAlbum } from '../utils/offline/libraryTierReconcile'; +import { useDownloadModalStore } from '@/features/offline'; +import { useOfflineStore } from '@/features/offline'; +import { useOfflineJobStore } from '@/features/offline'; +import { isOfflinePinComplete } from '@/features/offline'; +import { dequeueOfflinePin } from '@/features/offline'; +import { reconcileLibraryTierForAlbum } from '@/features/offline'; import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard'; import { join } from '@tauri-apps/api/path'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import AlbumCard from '../components/AlbumCard'; import AlbumHeader from '../components/AlbumHeader'; import AlbumTrackList from '../components/AlbumTrackList'; @@ -46,8 +46,8 @@ import LosslessModeBanner from '../components/LosslessModeBanner'; import { isLosslessSuffix } from '../utils/library/losslessFormats'; import { isLosslessMode } from '../utils/library/losslessMode'; import { readDetailServerId } from '../utils/navigation/detailServerScope'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineActionPolicy } from '@/features/offline'; export default function AlbumDetail() { const { t } = useTranslation(); diff --git a/src/pages/Albums.tsx b/src/pages/Albums.tsx index d9ca0eb0..ff0d6a42 100644 --- a/src/pages/Albums.tsx +++ b/src/pages/Albums.tsx @@ -1,5 +1,5 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl'; -import { resolveAlbum } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum } from '@/features/offline'; import { songToTrack } from '../utils/playback/songToTrack'; import { useState, useEffect, useLayoutEffect, useRef, useMemo } from 'react'; import AlbumCard from '../components/AlbumCard'; @@ -15,13 +15,13 @@ import LosslessFilterButton from '../components/LosslessFilterButton'; import SortDropdown from '../components/SortDropdown'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; -import { useOfflineStore } from '../store/offlineStore'; -import { useDownloadModalStore } from '../store/downloadModalStore'; +import { useOfflineStore } from '@/features/offline'; +import { useDownloadModalStore } from '@/features/offline'; import { usePlayerStore } from '../store/playerStore'; import { invoke } from '@tauri-apps/api/core'; import { join } from '@tauri-apps/api/path'; import { showToast } from '../utils/ui/toast'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import { Download, HardDriveDownload, Disc3, ListPlus } from 'lucide-react'; import SelectionToggleButton from '../components/SelectionToggleButton'; import FilterQuickClear from '../components/FilterQuickClear'; diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 1ba13495..f468ed7a 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -20,8 +20,8 @@ import { useArtistSimilarArtists } from '../hooks/useArtistSimilarArtists'; import { runArtistDetailPlayAll, runArtistDetailPlayTopSong, runArtistDetailShuffle, runArtistDetailStartRadio, } from '../utils/componentHelpers/runArtistDetailPlay'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineActionPolicy } from '@/features/offline'; import { runArtistEntityRating, runArtistToggleStar, runArtistShare, runArtistImageUpload, } from '../utils/componentHelpers/runArtistDetailActions'; diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 7adcbae0..a968eec1 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -29,9 +29,9 @@ import { type HomeFeedSnapshot, } from '../store/homeFeedCache'; import { useConnectionStatus } from '../hooks/useConnectionStatus'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { useOfflineBrowseReloadToken } from '../hooks/useOfflineBrowseReloadToken'; -import { useDevOfflineBrowseStore } from '../store/devOfflineBrowseStore'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { useOfflineBrowseReloadToken } from '@/features/offline'; +import { useDevOfflineBrowseStore } from '@/features/offline'; /** Match Random Albums overshoot when mix filter uses album/artist axes so hero + discover row can still fill. */ const HOME_RANDOM_FETCH = 100; diff --git a/src/pages/LosslessAlbums.tsx b/src/pages/LosslessAlbums.tsx index a2dcebe6..8f7b9fc2 100644 --- a/src/pages/LosslessAlbums.tsx +++ b/src/pages/LosslessAlbums.tsx @@ -1,5 +1,5 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl'; -import { resolveAlbum } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -8,10 +8,10 @@ import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode'; import { ndListLosslessAlbumsPage } from '../api/navidromeBrowse'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../store/authStore'; -import { useOfflineStore } from '../store/offlineStore'; -import { useDownloadModalStore } from '../store/downloadModalStore'; +import { useOfflineStore } from '@/features/offline'; +import { useDownloadModalStore } from '@/features/offline'; import { usePlayerStore } from '../store/playerStore'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import { useRangeSelection } from '../hooks/useRangeSelection'; import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; diff --git a/src/pages/MostPlayed.tsx b/src/pages/MostPlayed.tsx index 83712a36..fdb9fce4 100644 --- a/src/pages/MostPlayed.tsx +++ b/src/pages/MostPlayed.tsx @@ -1,5 +1,5 @@ import { getAlbumList } from '../api/subsonicLibrary'; -import { resolveAlbum } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { songToTrack } from '../utils/playback/songToTrack'; import React, { useEffect, useState, useCallback } from 'react'; diff --git a/src/pages/NewReleases.tsx b/src/pages/NewReleases.tsx index 28610930..9e399d65 100644 --- a/src/pages/NewReleases.tsx +++ b/src/pages/NewReleases.tsx @@ -1,7 +1,7 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl'; import { getAlbumsByGenre } from '../api/subsonicGenres'; import { getAlbumList } from '../api/subsonicLibrary'; -import { resolveAlbum } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { dedupeById } from '../utils/dedupeById'; import { useEffect, useLayoutEffect, useState, useCallback, useRef, useMemo } from 'react'; @@ -12,12 +12,12 @@ import GenreFilterBar from '../components/GenreFilterBar'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useAuthStore } from '../store/authStore'; -import { useOfflineStore } from '../store/offlineStore'; -import { useDownloadModalStore } from '../store/downloadModalStore'; +import { useOfflineStore } from '@/features/offline'; +import { useDownloadModalStore } from '@/features/offline'; import { invoke } from '@tauri-apps/api/core'; import { join } from '@tauri-apps/api/path'; import { showToast } from '../utils/ui/toast'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import { useRangeSelection } from '../hooks/useRangeSelection'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight'; diff --git a/src/pages/PlaylistDetail.tsx b/src/pages/PlaylistDetail.tsx index 2c15a536..144067fb 100644 --- a/src/pages/PlaylistDetail.tsx +++ b/src/pages/PlaylistDetail.tsx @@ -6,12 +6,12 @@ import { useTracklistColumns, type ColDef } from '../utils/useTracklistColumns'; import { usePlayerStore } from '../store/playerStore'; import { useShallow } from 'zustand/react/shallow'; import { usePlaylistStore } from '../store/playlistStore'; -import { useOfflineStore } from '../store/offlineStore'; +import { useOfflineStore } from '@/features/offline'; import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useAlbumOfflineState } from '../hooks/useAlbumOfflineState'; import { useAuthStore } from '../store/authStore'; -import { useDownloadModalStore } from '../store/downloadModalStore'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useDownloadModalStore } from '@/features/offline'; +import { useZipDownloadStore } from '@/features/offline'; import { useDragDrop } from '../contexts/DragDropContext'; import { useTranslation } from 'react-i18next'; import type { SpotifyCsvTrack } from '../utils/playlist/spotifyCsvImport'; @@ -40,8 +40,8 @@ import { usePlaylistDerived } from '../hooks/usePlaylistDerived'; import { usePlaylistRouteEffects } from '../hooks/usePlaylistRouteEffects'; import { useBulkPlPickerOutsideClick } from '../hooks/useBulkPlPickerOutsideClick'; import { usePlaylistDnDReorder } from '../hooks/usePlaylistDnDReorder'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineActionPolicy } from '@/features/offline'; // ── Column configuration ────────────────────────────────────────────────────── const PL_COLUMNS: readonly ColDef[] = [ diff --git a/src/pages/Playlists.tsx b/src/pages/Playlists.tsx index 056fb8b6..347b81f3 100644 --- a/src/pages/Playlists.tsx +++ b/src/pages/Playlists.tsx @@ -1,4 +1,4 @@ -import { resolveMediaServerId, resolvePlaylist } from '../utils/offline/offlineMediaResolve'; +import { resolveMediaServerId, resolvePlaylist } from '@/features/offline'; import { getGenres } from '../api/subsonicGenres'; import { filterSongsToActiveLibrary } from '../api/subsonicLibrary'; import type { SubsonicPlaylist, SubsonicGenre } from '../api/subsonicTypes'; @@ -27,8 +27,8 @@ import PlaylistsHeader from '../components/playlists/PlaylistsHeader'; import PlaylistCard from '../components/playlists/PlaylistCard'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { VirtualCardGrid } from '../components/VirtualCardGrid'; -import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; -import { offlineActionPolicy } from '../utils/offline/offlineActionPolicy'; +import { useOfflineBrowseContext } from '@/features/offline'; +import { offlineActionPolicy } from '@/features/offline'; import { Info } from 'lucide-react'; import PlaylistsFolderView from '../components/playlists/PlaylistsFolderView'; import { usePlaylistFolderStore } from '../store/playlistFolderStore'; diff --git a/src/pages/RandomAlbums.tsx b/src/pages/RandomAlbums.tsx index d8ac66f2..17cfd35f 100644 --- a/src/pages/RandomAlbums.tsx +++ b/src/pages/RandomAlbums.tsx @@ -1,7 +1,7 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl'; import { getAlbumsByGenre } from '../api/subsonicGenres'; import { getAlbumList } from '../api/subsonicLibrary'; -import { resolveAlbum } from '../utils/offline/offlineMediaResolve'; +import { resolveAlbum } from '@/features/offline'; import type { SubsonicAlbum } from '../api/subsonicTypes'; import { dedupeById } from '../utils/dedupeById'; import { shuffleArray } from '../utils/playback/shuffleArray'; @@ -16,12 +16,12 @@ import { useAuthStore } from '../store/authStore'; import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter'; import { runLocalRandomAlbums, runLocalAlbumsByGenres } from '../utils/library/browseTextSearch'; -import { useOfflineStore } from '../store/offlineStore'; -import { useDownloadModalStore } from '../store/downloadModalStore'; +import { useOfflineStore } from '@/features/offline'; +import { useDownloadModalStore } from '@/features/offline'; import { invoke } from '@tauri-apps/api/core'; import { join } from '@tauri-apps/api/path'; import { showToast } from '../utils/ui/toast'; -import { useZipDownloadStore } from '../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import { useRangeSelection } from '../hooks/useRangeSelection'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes'; diff --git a/src/store/crossfadePreload.ts b/src/store/crossfadePreload.ts index b9f0397b..64e9ef66 100644 --- a/src/store/crossfadePreload.ts +++ b/src/store/crossfadePreload.ts @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useAuthStore } from './authStore'; import { autodjMaxOverlapCapSec } from '../utils/playback/autodjOverlapCap'; import { computeWaveformSilence, planCrossfadeTransition } from '../utils/waveform/waveformSilence'; -import { findLocalPlaybackUrl } from '../utils/offline/offlineLibraryHelpers'; +import { findLocalPlaybackUrl } from '@/features/offline'; import { playbackCacheKeyForRef } from '../utils/playback/playbackServer'; import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl'; import { resolveQueueTrack } from '../utils/library/queueTrackView'; diff --git a/src/store/hotCacheStore.ts b/src/store/hotCacheStore.ts index 2a1dc72b..299b401c 100644 --- a/src/store/hotCacheStore.ts +++ b/src/store/hotCacheStore.ts @@ -2,7 +2,7 @@ import type { QueueItemRef } from './playerStoreTypes'; import { create } from 'zustand'; import type { HotCacheEntry } from './hotCacheStoreTypes'; import { useLocalPlaybackStore, type LocalPlaybackEntry } from './localPlaybackStore'; -import { entryBelongsToServer } from '../utils/offline/offlineLibraryHelpers'; +import { entryBelongsToServer } from '@/features/offline'; import { invoke } from '@tauri-apps/api/core'; import { getMediaDir } from '../utils/media/mediaDir'; diff --git a/src/store/localPlaybackMigration.ts b/src/store/localPlaybackMigration.ts index 91aed0fe..702ef669 100644 --- a/src/store/localPlaybackMigration.ts +++ b/src/store/localPlaybackMigration.ts @@ -2,7 +2,7 @@ import type { ServerProfile } from './authStoreTypes'; import type { HotCacheEntry } from './hotCacheStoreTypes'; import { localPlaybackEntryKey } from './localPlaybackKeys'; import type { LocalPlaybackEntry, PinSource } from './localPlaybackStore'; -import type { OfflineAlbumMeta, OfflineTrackMeta } from './offlineStore'; +import type { OfflineAlbumMeta, OfflineTrackMeta } from '@/features/offline'; import { resolveIndexKey } from '../utils/server/serverIndexKey'; const MIGRATION_FLAG = 'psysonic-local-playback-migrated-v1'; diff --git a/src/store/playTrackAction.ts b/src/store/playTrackAction.ts index 8b7e53bf..f7daccc0 100644 --- a/src/store/playTrackAction.ts +++ b/src/store/playTrackAction.ts @@ -30,7 +30,7 @@ import { stampTrackServerId, stampTrackServerIds } from '../utils/playback/track import { findLocalPlaybackUrl, hasLocalPersistentPlaybackBytes, -} from '../utils/offline/offlineLibraryHelpers'; +} from '@/features/offline'; import { resolvePlaybackUrl } from '../utils/playback/resolvePlaybackUrl'; import { resolveReplayGainDb } from '../utils/audio/resolveReplayGainDb'; import { audioPlayHiResBlendArgs } from '../utils/audio/hiResCrossfadeResample'; diff --git a/src/store/playbackUrlRouting.test.ts b/src/store/playbackUrlRouting.test.ts index a3d20300..905813e1 100644 --- a/src/store/playbackUrlRouting.test.ts +++ b/src/store/playbackUrlRouting.test.ts @@ -11,7 +11,7 @@ const { offlineStoreState } = vi.hoisted(() => ({ offlineStoreState: { localUrlByKey: new Map() }, })); -vi.mock('../utils/offline/offlineLibraryHelpers', () => ({ +vi.mock('@/features/offline', () => ({ findLocalPlaybackUrl: (trackId: string, serverId: string, tier: string) => { if (tier !== 'library') return null; return offlineStoreState.localUrlByKey.has(`${serverId}:${trackId}`) diff --git a/src/store/playbackUrlRouting.ts b/src/store/playbackUrlRouting.ts index 4b82fabf..8897f2a3 100644 --- a/src/store/playbackUrlRouting.ts +++ b/src/store/playbackUrlRouting.ts @@ -1,4 +1,4 @@ -import { findLocalPlaybackUrl } from '../utils/offline/offlineLibraryHelpers'; +import { findLocalPlaybackUrl } from '@/features/offline'; import { resolvePlaybackUrl, type PlaybackSourceKind } from '../utils/playback/resolvePlaybackUrl'; import { resolveServerIdForIndexKey } from '../utils/server/serverLookup'; import { sameQueueTrackId } from '../utils/playback/queueIdentity'; diff --git a/src/store/playlistStore.ts b/src/store/playlistStore.ts index e965c1a8..e8ee45bd 100644 --- a/src/store/playlistStore.ts +++ b/src/store/playlistStore.ts @@ -4,8 +4,8 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { createPlaylist as apiCreatePlaylist } from '../api/subsonicPlaylists'; import { useAuthStore } from './authStore'; -import { isOfflineBrowseActive } from '../utils/offline/offlineBrowseMode'; -import { fetchOfflineBrowsablePlaylists } from '../utils/offline/offlinePlaylistBrowse'; +import { isOfflineBrowseActive } from '@/features/offline'; +import { fetchOfflineBrowsablePlaylists } from '@/features/offline'; interface PlaylistStore { recentIds: string[]; playlists: SubsonicPlaylist[]; diff --git a/src/store/promoteStreamCache.test.ts b/src/store/promoteStreamCache.test.ts index 41b80238..c7a86623 100644 --- a/src/store/promoteStreamCache.test.ts +++ b/src/store/promoteStreamCache.test.ts @@ -28,7 +28,7 @@ vi.mock('../api/coverCache', () => ({ const hasLocalPersistentPlaybackBytesMock = vi.fn((_trackId: string, _serverId: string) => false); -vi.mock('../utils/offline/offlineLibraryHelpers', () => ({ +vi.mock('@/features/offline', () => ({ hasLocalPersistentPlaybackBytes: (trackId: string, serverId: string) => hasLocalPersistentPlaybackBytesMock(trackId, serverId), })); diff --git a/src/store/promoteStreamCache.ts b/src/store/promoteStreamCache.ts index d197fe78..178a7153 100644 --- a/src/store/promoteStreamCache.ts +++ b/src/store/promoteStreamCache.ts @@ -4,7 +4,7 @@ import { invoke } from '@tauri-apps/api/core'; import { useHotCacheStore } from './hotCacheStore'; import { getMediaDir } from '../utils/media/mediaDir'; import { librarySqlServerId } from '../api/coverCache'; -import { hasLocalPersistentPlaybackBytes } from '../utils/offline/offlineLibraryHelpers'; +import { hasLocalPersistentPlaybackBytes } from '@/features/offline'; /** * Promote a track whose stream cache is full to the on-disk ephemeral tier. diff --git a/src/utils/componentHelpers/contextMenuActions.ts b/src/utils/componentHelpers/contextMenuActions.ts index 59388cdf..dcfa268c 100644 --- a/src/utils/componentHelpers/contextMenuActions.ts +++ b/src/utils/componentHelpers/contextMenuActions.ts @@ -7,8 +7,8 @@ import { useAuthStore } from '../../store/authStore'; import { usePlayerStore } from '../../store/playerStore'; import type { Track } from '../../store/playerStoreTypes'; import { resolveQueueTrack } from '../library/queueTrackView'; -import { useZipDownloadStore } from '../../store/zipDownloadStore'; -import { useDownloadModalStore } from '../../store/downloadModalStore'; +import { useZipDownloadStore } from '@/features/offline'; +import { useDownloadModalStore } from '@/features/offline'; import type { EntityShareKind } from '../share/shareLink'; import { copyEntityShareLink } from '../share/copyEntityShareLink'; import { sanitizeFilename, shuffleArray } from './contextMenuHelpers'; diff --git a/src/utils/componentHelpers/runArtistDetailPlay.test.ts b/src/utils/componentHelpers/runArtistDetailPlay.test.ts index c3ce9559..839e13e9 100644 --- a/src/utils/componentHelpers/runArtistDetailPlay.test.ts +++ b/src/utils/componentHelpers/runArtistDetailPlay.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SubsonicAlbum } from '../../api/subsonicTypes'; -import * as offlineMediaResolve from '../offline/offlineMediaResolve'; +import * as offlineMediaResolve from '@/features/offline'; import { fetchArtistDetailTracks, runArtistDetailPlayTopSong } from './runArtistDetailPlay'; -vi.mock('../offline/offlineMediaResolve', () => ({ +vi.mock('@/features/offline', () => ({ resolveAlbum: vi.fn(), resolveMediaServerId: vi.fn((id?: string | null) => id ?? 'srv-1'), })); diff --git a/src/utils/componentHelpers/runArtistDetailPlay.ts b/src/utils/componentHelpers/runArtistDetailPlay.ts index 7e069eee..7f5342ab 100644 --- a/src/utils/componentHelpers/runArtistDetailPlay.ts +++ b/src/utils/componentHelpers/runArtistDetailPlay.ts @@ -4,7 +4,7 @@ import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subs import type { Track } from '../../store/playerStoreTypes'; import { songToTrack } from '../playback/songToTrack'; import { runBulkPlayAll, runBulkShuffle } from '../playback/runBulkPlay'; -import { resolveAlbum, resolveMediaServerId } from '../offline/offlineMediaResolve'; +import { resolveAlbum, resolveMediaServerId } from '@/features/offline'; /** Ordered artist discography tracks for play-all / shuffle (network or local bytes). */ export async function fetchArtistDetailTracks( diff --git a/src/utils/library/albumBrowseCatalogChunk.ts b/src/utils/library/albumBrowseCatalogChunk.ts index aa5d2e19..7aa5a85e 100644 --- a/src/utils/library/albumBrowseCatalogChunk.ts +++ b/src/utils/library/albumBrowseCatalogChunk.ts @@ -1,7 +1,7 @@ import type { SubsonicAlbum } from '../../api/subsonicTypes'; import { dedupeById } from '../dedupeById'; -import { isOfflineBrowseActive } from '../offline/offlineBrowseMode'; -import { loadOfflineAlbumCatalogChunk } from '../offline/offlineAlbumBrowseCatalog'; +import { isOfflineBrowseActive } from '@/features/offline'; +import { loadOfflineAlbumCatalogChunk } from '@/features/offline'; import type { AlbumBrowseQuery } from './albumBrowseTypes'; import { fetchLocalAlbumCatalogChunk } from './albumBrowseLoad'; diff --git a/src/utils/mix/luckyMixHelpers.ts b/src/utils/mix/luckyMixHelpers.ts index da8cb020..d83528b0 100644 --- a/src/utils/mix/luckyMixHelpers.ts +++ b/src/utils/mix/luckyMixHelpers.ts @@ -1,6 +1,6 @@ import { getTopSongs } from '../../api/subsonicArtists'; import { filterSongsToActiveLibrary, getAlbumList, getRandomSongs } from '../../api/subsonicLibrary'; -import { resolveAlbumForActiveServer } from '../offline/offlineMediaResolve'; +import { resolveAlbumForActiveServer } from '@/features/offline'; import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes'; import { filterSongsForLuckyMixRatings, diff --git a/src/utils/network/activeServerReachability.test.ts b/src/utils/network/activeServerReachability.test.ts index 4dda1cb1..e712da47 100644 --- a/src/utils/network/activeServerReachability.test.ts +++ b/src/utils/network/activeServerReachability.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { useDevOfflineBrowseStore } from '../../store/devOfflineBrowseStore'; +import { useDevOfflineBrowseStore } from '@/features/offline'; import { getActiveServerReachable, isActiveServerReachable, diff --git a/src/utils/network/activeServerReachability.ts b/src/utils/network/activeServerReachability.ts index 44614713..e2da6440 100644 --- a/src/utils/network/activeServerReachability.ts +++ b/src/utils/network/activeServerReachability.ts @@ -1,4 +1,4 @@ -import { isDevOfflineBrowseForced } from '../../store/devOfflineBrowseStore'; +import { isDevOfflineBrowseForced } from '@/features/offline'; export type ConnectionStatus = 'connected' | 'disconnected' | 'checking'; diff --git a/src/utils/network/subsonicNetworkGuard.ts b/src/utils/network/subsonicNetworkGuard.ts index 37e636e3..6f51b900 100644 --- a/src/utils/network/subsonicNetworkGuard.ts +++ b/src/utils/network/subsonicNetworkGuard.ts @@ -1,6 +1,6 @@ import { useAuthStore } from '../../store/authStore'; import { resolvePlaybackUrl } from '../playback/resolvePlaybackUrl'; -import { isDevOfflineBrowseForced } from '../../store/devOfflineBrowseStore'; +import { isDevOfflineBrowseForced } from '@/features/offline'; import { resolveServerIdForIndexKey } from '../server/serverLookup'; import { isActiveServerReachable } from './activeServerReachability'; diff --git a/src/utils/playback/fetchTracksForSource.ts b/src/utils/playback/fetchTracksForSource.ts index 4033daff..aa0cf01e 100644 --- a/src/utils/playback/fetchTracksForSource.ts +++ b/src/utils/playback/fetchTracksForSource.ts @@ -5,7 +5,7 @@ import { resolveArtist, resolveMediaServerId, resolvePlaylist, -} from '../offline/offlineMediaResolve'; +} from '@/features/offline'; export async function fetchTracksForSource(source: DeviceSyncSource): Promise { const serverId = resolveMediaServerId(); diff --git a/src/utils/playback/playAlbum.test.ts b/src/utils/playback/playAlbum.test.ts index 45844169..df9ab0c8 100644 --- a/src/utils/playback/playAlbum.test.ts +++ b/src/utils/playback/playAlbum.test.ts @@ -4,7 +4,7 @@ import { onInvoke } from '@/test/mocks/tauri'; import { resetOrbitStore, resetPlayerStore } from '@/test/helpers/storeReset'; import type { Track } from '../../store/playerStoreTypes'; -vi.mock('../offline/offlineMediaResolve', () => ({ +vi.mock('@/features/offline', () => ({ resolveAlbumForActiveServer: vi.fn(), })); @@ -12,7 +12,7 @@ vi.mock('./fadeOut', () => ({ fadeOut: vi.fn(async () => undefined), })); -import { resolveAlbumForActiveServer } from '../offline/offlineMediaResolve'; +import { resolveAlbumForActiveServer } from '@/features/offline'; import { useOrbitStore } from '../../store/orbitStore'; import { usePlayerStore } from '../../store/playerStore'; import { playAlbum, playAlbumShuffled } from './playAlbum'; diff --git a/src/utils/playback/playAlbum.ts b/src/utils/playback/playAlbum.ts index af66bda3..12343f56 100644 --- a/src/utils/playback/playAlbum.ts +++ b/src/utils/playback/playAlbum.ts @@ -1,5 +1,5 @@ import { usePlayerStore } from '../../store/playerStore'; -import { resolveAlbumForActiveServer } from '../offline/offlineMediaResolve'; +import { resolveAlbumForActiveServer } from '@/features/offline'; import { songToTrack } from './songToTrack'; import { useOrbitStore } from '../../store/orbitStore'; import { fadeOut } from './fadeOut'; diff --git a/src/utils/playback/playArtistShuffled.ts b/src/utils/playback/playArtistShuffled.ts index 8f22ec57..6636eb5e 100644 --- a/src/utils/playback/playArtistShuffled.ts +++ b/src/utils/playback/playArtistShuffled.ts @@ -1,4 +1,4 @@ -import { resolveAlbum, resolveArtist, resolveMediaServerId } from '../offline/offlineMediaResolve'; +import { resolveAlbum, resolveArtist, resolveMediaServerId } from '@/features/offline'; import { songToTrack } from './songToTrack'; import { shuffleArray } from './shuffleArray'; import { usePlayerStore } from '../../store/playerStore'; diff --git a/src/utils/playback/playByOpaqueId.ts b/src/utils/playback/playByOpaqueId.ts index b5864ee5..a4099193 100644 --- a/src/utils/playback/playByOpaqueId.ts +++ b/src/utils/playback/playByOpaqueId.ts @@ -1,5 +1,5 @@ import { getSong } from '../../api/subsonicLibrary'; -import { resolveAlbumForActiveServer } from '../offline/offlineMediaResolve'; +import { resolveAlbumForActiveServer } from '@/features/offline'; import { songToTrack } from './songToTrack'; import { playAlbum } from './playAlbum'; import { playArtistShuffled } from './playArtistShuffled'; diff --git a/src/utils/playback/resolvePlaybackUrl.ts b/src/utils/playback/resolvePlaybackUrl.ts index 788599d6..72f34a71 100644 --- a/src/utils/playback/resolvePlaybackUrl.ts +++ b/src/utils/playback/resolvePlaybackUrl.ts @@ -1,5 +1,5 @@ import { buildStreamUrlForServer } from '../../api/subsonicStreamUrl'; -import { findLocalPlaybackUrl } from '../offline/offlineLibraryHelpers'; +import { findLocalPlaybackUrl } from '@/features/offline'; import { resolveServerIdForIndexKey } from '../server/serverLookup'; import { getPlaybackCacheServerKey, getPlaybackServerId } from './playbackServer'; diff --git a/src/utils/playlist/runPlaylistLoad.ts b/src/utils/playlist/runPlaylistLoad.ts index 549bc811..f220bf17 100644 --- a/src/utils/playlist/runPlaylistLoad.ts +++ b/src/utils/playlist/runPlaylistLoad.ts @@ -4,8 +4,8 @@ import { filterSongsToActiveLibrary } from '../../api/subsonicLibrary'; import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; import { useAuthStore } from '../../store/authStore'; import { usePlaylistStore } from '../../store/playlistStore'; -import { isOfflineBrowseActive } from '../offline/offlineBrowseMode'; -import { resolvePlaylist } from '../offline/offlineMediaResolve'; +import { isOfflineBrowseActive } from '@/features/offline'; +import { resolvePlaylist } from '@/features/offline'; export interface RunPlaylistLoadDeps { id: string; diff --git a/src/utils/playlist/runPlaylistZipDownload.ts b/src/utils/playlist/runPlaylistZipDownload.ts index e6ebd416..abdab8e9 100644 --- a/src/utils/playlist/runPlaylistZipDownload.ts +++ b/src/utils/playlist/runPlaylistZipDownload.ts @@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { join } from '@tauri-apps/api/path'; import { buildDownloadUrl } from '../../api/subsonicStreamUrl'; import type { SubsonicPlaylist } from '../../api/subsonicTypes'; -import { useZipDownloadStore } from '../../store/zipDownloadStore'; +import { useZipDownloadStore } from '@/features/offline'; import { sanitizeFilename } from '../componentHelpers/playlistDetailHelpers'; export interface RunPlaylistZipDownloadDeps { diff --git a/src/utils/server/rewriteFrontendStoreKeys.test.ts b/src/utils/server/rewriteFrontendStoreKeys.test.ts index 6c0894e1..1c6fde1a 100644 --- a/src/utils/server/rewriteFrontendStoreKeys.test.ts +++ b/src/utils/server/rewriteFrontendStoreKeys.test.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { useAnalysisStrategyStore } from '../../store/analysisStrategyStore'; import { useCoverStrategyStore } from '../../store/coverStrategyStore'; import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; -import { useOfflineStore } from '../../store/offlineStore'; +import { useOfflineStore } from '@/features/offline'; import { usePlayerStore } from '../../store/playerStore'; import { rewriteFrontendStoreKeysForRemap } from './rewriteFrontendStoreKeys'; diff --git a/src/utils/server/rewriteFrontendStoreKeys.ts b/src/utils/server/rewriteFrontendStoreKeys.ts index 53c92bc2..0b289e56 100644 --- a/src/utils/server/rewriteFrontendStoreKeys.ts +++ b/src/utils/server/rewriteFrontendStoreKeys.ts @@ -3,7 +3,7 @@ import { useAnalysisStrategyStore } from '../../store/analysisStrategyStore'; import { useCoverStrategyStore } from '../../store/coverStrategyStore'; import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLibraryIndexStore } from '../../store/libraryIndexStore'; -import { useOfflineStore } from '../../store/offlineStore'; +import { useOfflineStore } from '@/features/offline'; import { usePlayerStore } from '../../store/playerStore'; import { serverIndexKeyFromUrl } from './serverIndexKey'; diff --git a/src/utils/share/applySharePaste.ts b/src/utils/share/applySharePaste.ts index d413270c..e4c20f23 100644 --- a/src/utils/share/applySharePaste.ts +++ b/src/utils/share/applySharePaste.ts @@ -1,5 +1,5 @@ import { getSong } from '../../api/subsonicLibrary'; -import { resolveAlbum, resolveArtist } from '../offline/offlineMediaResolve'; +import { resolveAlbum, resolveArtist } from '@/features/offline'; import type { SubsonicSong } from '../../api/subsonicTypes'; import { songToTrack } from '../playback/songToTrack'; import type { Location, NavigateFunction } from 'react-router-dom'; diff --git a/src/utils/share/enqueueShareSearchPayload.ts b/src/utils/share/enqueueShareSearchPayload.ts index 075795af..92a8eeba 100644 --- a/src/utils/share/enqueueShareSearchPayload.ts +++ b/src/utils/share/enqueueShareSearchPayload.ts @@ -5,7 +5,7 @@ import { getSongWithCredentials, } from '../../api/subsonicEntityWithCredentials'; import { getSong } from '../../api/subsonicLibrary'; -import { resolveAlbum, resolveArtist } from '../offline/offlineMediaResolve'; +import { resolveAlbum, resolveArtist } from '@/features/offline'; import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subsonicTypes'; import { useAuthStore } from '../../store/authStore'; import type { ServerProfile } from '../../store/authStoreTypes';