refactor(offline): co-locate offline feature into features/offline

This commit is contained in:
Psychotoxical
2026-06-30 00:56:08 +02:00
parent 70c145db06
commit d6dbb615fd
149 changed files with 521 additions and 466 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ export async function updatePlaylist(id: string, songIds: string[], prevCount =
songIndexToRemove: Array.from({ length: prevCount }, (_, i) => i), songIndexToRemove: Array.from({ length: prevCount }, (_, i) => i),
}); });
} }
void import('../utils/offline/pinnedOfflineSync') void import('@/features/offline')
.then(m => m.schedulePinnedPlaylistSync(id)) .then(m => m.schedulePinnedPlaylistSync(id))
.catch(() => {}); .catch(() => {});
} }
+2 -2
View File
@@ -85,7 +85,7 @@ export async function star(
const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId); const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId);
void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {}); void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {});
} }
void import('../utils/offline/favoritesOfflineSync') void import('@/features/offline')
.then(m => m.onFavoritesOfflineStarChange(id, type, true, serverId ?? undefined)) .then(m => m.onFavoritesOfflineStarChange(id, type, true, serverId ?? undefined))
.catch(() => {}); .catch(() => {});
} }
@@ -108,7 +108,7 @@ export async function unstar(
const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId); const indexEnabled = useLibraryIndexStore.getState().isIndexEnabled(serverId);
void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {}); void refreshStarredAlbumIndexFromServer(serverId, indexEnabled).catch(() => {});
} }
void import('../utils/offline/favoritesOfflineSync') void import('@/features/offline')
.then(m => m.onFavoritesOfflineStarChange(id, type, false, serverId ?? undefined)) .then(m => m.onFavoritesOfflineStarChange(id, type, false, serverId ?? undefined))
.catch(() => {}); .catch(() => {});
} }
+1 -1
View File
@@ -32,7 +32,7 @@ const Statistics = lazy(() => import('@/features/stats/pages/Statistics'));
const Help = lazy(() => import('../pages/Help')); const Help = lazy(() => import('../pages/Help'));
const WhatsNew = lazy(() => import('../pages/WhatsNew')); const WhatsNew = lazy(() => import('../pages/WhatsNew'));
const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync')); 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 LabelAlbums = lazy(() => import('../pages/LabelAlbums'));
const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage')); const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage'));
const FolderBrowser = lazy(() => import('../pages/FolderBrowser')); const FolderBrowser = lazy(() => import('../pages/FolderBrowser'));
+6 -6
View File
@@ -17,7 +17,7 @@ import AppRoutes from './AppRoutes';
import FullscreenPlayer from '@/features/fullscreenPlayer'; import FullscreenPlayer from '@/features/fullscreenPlayer';
import ContextMenu from '../components/ContextMenu'; import ContextMenu from '../components/ContextMenu';
import SongInfoModal from '../components/SongInfoModal'; import SongInfoModal from '../components/SongInfoModal';
import DownloadFolderModal from '../components/DownloadFolderModal'; import { DownloadFolderModal } from '@/features/offline';
import GlobalConfirmModal from '../components/GlobalConfirmModal'; import GlobalConfirmModal from '../components/GlobalConfirmModal';
import ThemeMigrationNotice from '../components/ThemeMigrationNotice'; import ThemeMigrationNotice from '../components/ThemeMigrationNotice';
import OrbitAccountPicker from '../components/OrbitAccountPicker'; import OrbitAccountPicker from '../components/OrbitAccountPicker';
@@ -30,7 +30,7 @@ import {
} from '../constants/appScroll'; } from '../constants/appScroll';
import ConnectionIndicator from '../components/ConnectionIndicator'; import ConnectionIndicator from '../components/ConnectionIndicator';
import MusicNetworkIndicator from '../components/MusicNetworkIndicator'; import MusicNetworkIndicator from '../components/MusicNetworkIndicator';
import OfflineBanner from '../components/OfflineBanner'; import { OfflineBanner } from '@/features/offline';
import AppUpdater from '../components/AppUpdater'; import AppUpdater from '../components/AppUpdater';
import TitleBar from '../components/TitleBar'; import TitleBar from '../components/TitleBar';
import OrbitSessionBar from '../components/OrbitSessionBar'; import OrbitSessionBar from '../components/OrbitSessionBar';
@@ -39,8 +39,8 @@ import { useOrbitHost } from '../hooks/useOrbitHost';
import { useOrbitGuest } from '../hooks/useOrbitGuest'; import { useOrbitGuest } from '../hooks/useOrbitGuest';
import { useOrbitBodyAttrs } from '../hooks/useOrbitBodyAttrs'; import { useOrbitBodyAttrs } from '../hooks/useOrbitBodyAttrs';
import { usePlatformShellSetup } from '../hooks/usePlatformShellSetup'; import { usePlatformShellSetup } from '../hooks/usePlatformShellSetup';
import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineBrowseNavFlags } from '../utils/offline/offlineBrowseContext'; import { offlineBrowseNavFlags } from '@/features/offline';
import { useWindowFullscreenState } from '../hooks/useWindowFullscreenState'; import { useWindowFullscreenState } from '../hooks/useWindowFullscreenState';
import { useNowPlayingTrayTitle } from '../hooks/useNowPlayingTrayTitle'; import { useNowPlayingTrayTitle } from '../hooks/useNowPlayingTrayTitle';
import { usePrefetchReleaseNotes } from '../hooks/usePrefetchReleaseNotes'; import { usePrefetchReleaseNotes } from '../hooks/usePrefetchReleaseNotes';
@@ -53,8 +53,8 @@ import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator';
import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority'; import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority';
import { useLiveSearchRouteScope } from '@/features/search'; import { useLiveSearchRouteScope } from '@/features/search';
import { useNowPlayingPrewarm } from '@/features/nowPlaying'; import { useNowPlayingPrewarm } from '@/features/nowPlaying';
import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; import { useOfflineAutoNav } from '@/features/offline';
import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; import { useOfflineLibraryFilterSuspend } from '@/features/offline';
import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam'; import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam';
import { IS_LINUX, IS_MACOS } from '../utils/platform'; import { IS_LINUX, IS_MACOS } from '../utils/platform';
import { useConnectionStatus } from '../hooks/useConnectionStatus'; import { useConnectionStatus } from '../hooks/useConnectionStatus';
+6 -6
View File
@@ -7,18 +7,18 @@ import { WindowVisibilityProvider } from '../hooks/useWindowVisibility';
import { DragDropProvider } from '../contexts/DragDropContext'; import { DragDropProvider } from '../contexts/DragDropContext';
import PasteClipboardHandler from '../components/PasteClipboardHandler'; import PasteClipboardHandler from '../components/PasteClipboardHandler';
import ExportPickerModal from '../components/ExportPickerModal'; import ExportPickerModal from '../components/ExportPickerModal';
import ZipDownloadOverlay from '../components/ZipDownloadOverlay'; import { ZipDownloadOverlay } from '@/features/offline';
import FpsOverlay from '../components/FpsOverlay'; import FpsOverlay from '../components/FpsOverlay';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useLibraryIndexStore } from '../store/libraryIndexStore';
import { useGlobalShortcutsStore } from '../store/globalShortcutsStore'; import { useGlobalShortcutsStore } from '../store/globalShortcutsStore';
import { initHotCachePrefetch } from '../hotCachePrefetch'; import { initHotCachePrefetch } from '../hotCachePrefetch';
import { initLocalPlaybackInvalidation } from '../localPlaybackInvalidation'; import { initLocalPlaybackInvalidation } from '../localPlaybackInvalidation';
import { initFavoritesOfflineSync } from '../utils/offline/favoritesOfflineSync'; import { initFavoritesOfflineSync } from '@/features/offline';
import { initPinnedOfflineSync } from '../utils/offline/pinnedOfflineSync'; import { initPinnedOfflineSync } from '@/features/offline';
import { initResumeIncompleteOfflinePins, scheduleResumeIncompleteOfflinePins } from '../utils/offline/resumeIncompleteOfflinePins'; import { initResumeIncompleteOfflinePins, scheduleResumeIncompleteOfflinePins } from '@/features/offline';
import { runLegacyOfflineFileMigration } from '../utils/migrations/legacyOfflineFileMigration'; import { runLegacyOfflineFileMigration } from '@/features/offline';
import { reconcileLibraryTierForServer } from '../utils/offline/libraryTierReconcile'; import { reconcileLibraryTierForServer } from '@/features/offline';
import { initMiniPlayerBridgeOnMain } from '@/features/miniPlayer'; import { initMiniPlayerBridgeOnMain } from '@/features/miniPlayer';
import { runAdvancedModeMigration } from '../utils/migrations/advancedModeMigration'; import { runAdvancedModeMigration } from '../utils/migrations/advancedModeMigration';
import { bootstrapAllIndexedServers } from '../utils/library/librarySession'; import { bootstrapAllIndexedServers } from '../utils/library/librarySession';
+1 -1
View File
@@ -1,5 +1,5 @@
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useZipDownloadBridge } from '../hooks/tauriBridge/useZipDownloadBridge'; import { useZipDownloadBridge } from '@/features/offline';
import { usePreviewBridge } from '../hooks/tauriBridge/usePreviewBridge'; import { usePreviewBridge } from '../hooks/tauriBridge/usePreviewBridge';
import { useAudioDeviceBridge } from '../hooks/tauriBridge/useAudioDeviceBridge'; import { useAudioDeviceBridge } from '../hooks/tauriBridge/useAudioDeviceBridge';
import { useCliBridge } from '../hooks/tauriBridge/useCliBridge'; import { useCliBridge } from '../hooks/tauriBridge/useCliBridge';
+1 -1
View File
@@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; import { isOfflinePinComplete } from '@/features/offline';
import { CoverArtImage } from '../cover/CoverArtImage'; import { CoverArtImage } from '../cover/CoverArtImage';
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef'; import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
import { coverStorageKeyFromRef } from '../cover/storageKeys'; import { coverStorageKeyFromRef } from '../cover/storageKeys';
+1 -1
View File
@@ -19,7 +19,7 @@ import { formatMb } from '../utils/format/formatBytes';
import { sanitizeHtml } from '../utils/sanitizeHtml'; import { sanitizeHtml } from '../utils/sanitizeHtml';
import { OpenArtistRefInline } from './OpenArtistRefInline'; import { OpenArtistRefInline } from './OpenArtistRefInline';
import { tooltipAttrs } from '@/ui/tooltipAttrs'; 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 { deriveAlbumGenreTags } from '../utils/library/genreTags';
import { genreColor } from '../utils/library/genreColor'; import { genreColor } from '../utils/library/genreColor';
+1 -1
View File
@@ -15,7 +15,7 @@ import { TrackRow } from './albumTrackList/TrackRow';
import { AlbumTrackListMobile } from './albumTrackList/AlbumTrackListMobile'; import { AlbumTrackListMobile } from './albumTrackList/AlbumTrackListMobile';
import { TracklistColumnPicker } from './albumTrackList/TracklistColumnPicker'; import { TracklistColumnPicker } from './albumTrackList/TracklistColumnPicker';
import { TracklistHeaderRow } from './albumTrackList/TracklistHeaderRow'; 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'; export type { SortKey } from '../utils/componentHelpers/albumTrackListHelpers';
+1 -1
View File
@@ -1,6 +1,6 @@
import { getArtist, getArtistInfo } from '../api/subsonicArtists'; import { getArtist, getArtistInfo } from '../api/subsonicArtists';
import { filterAlbumsToActiveLibrary } from '../api/subsonicLibrary'; 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 type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack'; import { songToTrack } from '../utils/playback/songToTrack';
import { shuffleArray } from '../utils/playback/shuffleArray'; import { shuffleArray } from '../utils/playback/shuffleArray';
+1 -1
View File
@@ -34,7 +34,7 @@ vi.mock('@/utils/orbitBulkGuard', () => ({
orbitBulkGuard: vi.fn(async () => true), orbitBulkGuard: vi.fn(async () => true),
})); }));
vi.mock('@/hooks/useOfflineBrowseContext', () => ({ vi.mock('@/features/offline', () => ({
useOfflineBrowseContext: () => ({ useOfflineBrowseContext: () => ({
active: false, active: false,
serverId: 'srv-1', serverId: 'srv-1',
+2 -2
View File
@@ -17,11 +17,11 @@ import { useContextMenuKeyboardNav } from '../hooks/useContextMenuKeyboardNav';
import { useContextMenuRating } from '../hooks/useContextMenuRating'; import { useContextMenuRating } from '../hooks/useContextMenuRating';
import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate'; import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { import {
offlineActionPolicy, offlineActionPolicy,
type OfflineSurface, type OfflineSurface,
} from '../utils/offline/offlineActionPolicy'; } from '@/features/offline';
import ContextMenuItems from './contextMenu/ContextMenuItems'; import ContextMenuItems from './contextMenu/ContextMenuItems';
function contextMenuSurfaceForType(type: string | null): OfflineSurface { function contextMenuSurfaceForType(type: string | null): OfflineSurface {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Cloud, CloudOff } from 'lucide-react'; 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). */ /** DEV-only: simulate full offline (disconnect UI, block Subsonic, local playback only). */
export default function DevNetworkModeToggle() { export default function DevNetworkModeToggle() {
+1 -1
View File
@@ -1,5 +1,5 @@
import { getRandomAlbums } from '../api/subsonicLibrary'; 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 type { SubsonicAlbum } from '../api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack'; import { songToTrack } from '../utils/playback/songToTrack';
import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react';
+3 -3
View File
@@ -6,9 +6,9 @@ import { useSidebarStore } from '@/features/sidebar';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { ALL_NAV_ITEMS } from '../config/navItems'; import { ALL_NAV_ITEMS } from '../config/navItems';
import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable';
import { isOfflineSidebarNavAllowed } from '../utils/offline/offlineNavPolicy'; import { isOfflineSidebarNavAllowed } from '@/features/offline';
import { useOfflineBrowseContext } from '../hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineBrowseNavFlags } from '../utils/offline/offlineBrowseContext'; import { offlineBrowseNavFlags } from '@/features/offline';
const BOTTOM_NAV_ROUTES = new Set(['/', '/albums', '/now-playing']); const BOTTOM_NAV_ROUTES = new Set(['/', '/albums', '/now-playing']);
+1 -1
View File
@@ -1,6 +1,6 @@
import { Play } from 'lucide-react'; import { Play } from 'lucide-react';
import { updatePlaylist } from '../api/subsonicPlaylists'; 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 { songToTrack } from '../utils/playback/songToTrack';
import type { Track } from '../store/playerStoreTypes'; import type { Track } from '../store/playerStoreTypes';
import { useState, useRef, useMemo } from 'react'; import { useState, useRef, useMemo } from 'react';
@@ -3,7 +3,7 @@ import { ListPlus, Search, X } from 'lucide-react';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import { useSelectionStore } from '../../store/selectionStore'; import { useSelectionStore } from '../../store/selectionStore';
import { AddToPlaylistSubmenu } from '../ContextMenu'; import { AddToPlaylistSubmenu } from '../ContextMenu';
import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
interface Props { interface Props {
filterText: string; filterText: string;
+1 -1
View File
@@ -14,7 +14,7 @@ import { codecLabel, type ColKey } from '../../utils/componentHelpers/albumTrack
import { formatLongDuration } from '../../utils/format/formatDuration'; import { formatLongDuration } from '../../utils/format/formatDuration';
import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers';
import i18n from '../../i18n'; 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'; import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
type ContextMenuFn = ( type ContextMenuFn = (
@@ -6,7 +6,7 @@ import {
Loader2, Play, Radio, Share2, Shuffle, Users, Loader2, Play, Radio, Share2, Shuffle, Users,
} from 'lucide-react'; } from 'lucide-react';
import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes'; import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '../../store/authStore';
import { useThemeStore } from '../../store/themeStore'; import { useThemeStore } from '../../store/themeStore';
import { useArtistOfflineState } from '../../hooks/useArtistOfflineState'; import { useArtistOfflineState } from '../../hooks/useArtistOfflineState';
@@ -22,7 +22,7 @@ import LastfmIcon from '../LastfmIcon';
import WikipediaIcon from '../WikipediaIcon'; import WikipediaIcon from '../WikipediaIcon';
import StarRating from '../StarRating'; import StarRating from '../StarRating';
import { tooltipAttrs } from '@/ui/tooltipAttrs'; import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
interface Props { interface Props {
artist: SubsonicArtist; artist: SubsonicArtist;
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; 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'; import { AddToPlaylistSubmenu } from './AddToPlaylistSubmenu';
interface AlbumProps { interface AlbumProps {
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Play, ListPlus, Heart, Download, ChevronRight, ChevronsRight, User, ListMusic, Star, Share2 } from 'lucide-react'; import { Play, ListPlus, Heart, Download, ChevronRight, ChevronsRight, User, ListMusic, Star, Share2 } from 'lucide-react';
import { useNavigate } from 'react-router-dom'; 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 { star, unstar } from '../../api/subsonicStarRating';
import type { SubsonicAlbum } from '../../api/subsonicTypes'; import type { SubsonicAlbum } from '../../api/subsonicTypes';
import { songToTrack } from '../../utils/playback/songToTrack'; import { songToTrack } from '../../utils/playback/songToTrack';
@@ -1,7 +1,7 @@
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ListMusic, Plus } from 'lucide-react'; 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 type { SubsonicPlaylist } from '../../api/subsonicTypes';
import { usePlaylistStore } from '../../store/playlistStore'; import { usePlaylistStore } from '../../store/playlistStore';
import { showToast } from '../../utils/ui/toast'; import { showToast } from '../../utils/ui/toast';
@@ -1,7 +1,7 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ListMusic, Plus } from 'lucide-react'; 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 { getPlaylists } from '../../api/subsonicPlaylists';
import type { SubsonicPlaylist } from '../../api/subsonicTypes'; import type { SubsonicPlaylist } from '../../api/subsonicTypes';
import { usePlaylistStore } from '../../store/playlistStore'; import { usePlaylistStore } from '../../store/playlistStore';
@@ -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 { 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 { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
import { useNavigateToArtist } from '../../hooks/useNavigateToArtist'; 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 { queueSongStar } from '../../store/pendingStarSync';
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network'; import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network';
import type { Track } from '../../store/playerStoreTypes'; import type { Track } from '../../store/playerStoreTypes';
@@ -2,7 +2,7 @@ import type React from 'react';
import type { SubsonicAlbum, SubsonicArtist } from '../../api/subsonicTypes'; import type { SubsonicAlbum, SubsonicArtist } from '../../api/subsonicTypes';
import type { QueueItemRef, Track } from '../../store/playerStoreTypes'; import type { QueueItemRef, Track } from '../../store/playerStoreTypes';
import type { EntityShareKind } from '../../utils/share/shareLink'; 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'; export type RatingKind = 'song' | 'album' | 'artist';
+2 -2
View File
@@ -21,8 +21,8 @@ import {
usePlayerBarLayoutStore, usePlayerBarLayoutStore,
type PlayerBarLayoutItemId, type PlayerBarLayoutItemId,
} from '../../store/playerBarLayoutStore'; } from '../../store/playerBarLayoutStore';
import { useOfflineBrowseContext } from '../../hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineActionPolicy } from '../../utils/offline/offlineActionPolicy'; import { offlineActionPolicy } from '@/features/offline';
interface Props { interface Props {
currentTrack: Track | null; currentTrack: Track | null;
+3 -3
View File
@@ -6,9 +6,9 @@ import {
Loader2, Lock, Pencil, Play, Search, Shuffle, Sparkles, Trash2, Loader2, Lock, Pencil, Play, Search, Shuffle, Sparkles, Trash2,
} from 'lucide-react'; } from 'lucide-react';
import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; 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 type { AlbumOfflineStatus } from '../../hooks/useAlbumOfflineState';
import { dequeueOfflinePin } from '../../utils/offline/offlinePinQueue'; import { dequeueOfflinePin } from '@/features/offline';
import { useThemeStore } from '../../store/themeStore'; import { useThemeStore } from '../../store/themeStore';
import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore'; import { usePlaylistLayoutStore, type PlaylistLayoutItemId } from '../../store/playlistLayoutStore';
import { import {
@@ -18,7 +18,7 @@ import type { CoverArtId } from '../../cover/types';
import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage';
import { PLAYLIST_MAIN_COVER_CSS_PX } from '../../hooks/usePlaylistCovers'; import { PLAYLIST_MAIN_COVER_CSS_PX } from '../../hooks/usePlaylistCovers';
import { PlaylistSmartCoverCell } from '../playlists/PlaylistCoverImages'; import { PlaylistSmartCoverCell } from '../playlists/PlaylistCoverImages';
import type { OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; import type { OfflineActionPolicy } from '@/features/offline';
interface Props { interface Props {
playlist: SubsonicPlaylist; playlist: SubsonicPlaylist;
+1 -1
View File
@@ -5,7 +5,7 @@ import type { SubsonicPlaylist } from '../../api/subsonicTypes';
import { import {
defaultSmartFilters, type SmartFilters, defaultSmartFilters, type SmartFilters,
} from '../../utils/playlist/playlistsSmart'; } from '../../utils/playlist/playlistsSmart';
import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy'; import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
import PlaylistsNewFolderButton from './PlaylistsNewFolderButton'; import PlaylistsNewFolderButton from './PlaylistsNewFolderButton';
import PlaylistsFolderViewToggle from './PlaylistsFolderViewToggle'; import PlaylistsFolderViewToggle from './PlaylistsFolderViewToggle';
@@ -8,7 +8,7 @@ import {
import { import {
disableFavoritesOfflineSync, disableFavoritesOfflineSync,
scheduleFavoritesOfflineSync, scheduleFavoritesOfflineSync,
} from '@/utils/offline/favoritesOfflineSync'; } from '@/features/offline';
function semaphoreTooltipKey(semaphore: FavoritesOfflineSemaphore): string { function semaphoreTooltipKey(semaphore: FavoritesOfflineSemaphore): string {
switch (semaphore) { switch (semaphore) {
@@ -9,12 +9,12 @@ import { usePlayerStore } from '@/store/playerStore';
import type { TopFavoriteArtist } from '@/features/favorites/components/TopFavoriteArtists'; import type { TopFavoriteArtist } from '@/features/favorites/components/TopFavoriteArtists';
import { useConnectionStatus } from '@/hooks/useConnectionStatus'; import { useConnectionStatus } from '@/hooks/useConnectionStatus';
import { isActiveServerReachable } from '@/utils/network/activeServerReachability'; import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { useOfflineBrowseReloadToken } from '@/hooks/useOfflineBrowseReloadToken'; import { useOfflineBrowseReloadToken } from '@/features/offline';
import { import {
loadStarredFromAllLibraryIndexes, loadStarredFromAllLibraryIndexes,
loadStarredFromAllServersOnline, loadStarredFromAllServersOnline,
} from '@/utils/offline/offlineStarredLoad'; } from '@/features/offline';
export interface FavoritesDataResult { export interface FavoritesDataResult {
albums: SubsonicAlbum[]; albums: SubsonicAlbum[];
@@ -1,10 +1,10 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useFavoritesOfflineSyncStore } from '@/store/favoritesOfflineSyncStore'; import { useFavoritesOfflineSyncStore } from '@/features/offline';
import { useLocalPlaybackStore } from '@/store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineJobStore } from '@/store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { FAVORITES_OFFLINE_JOB_ID } from '@/utils/offline/favoritesOfflineConstants'; import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline';
import { entryBelongsToServer } from '@/utils/offline/offlineLibraryHelpers'; import { entryBelongsToServer } from '@/features/offline';
export type FavoritesOfflineUiStatus = export type FavoritesOfflineUiStatus =
| 'disabled' | 'disabled'
@@ -21,7 +21,7 @@ import { getArtistForServer, getTopSongsForServer } from '@/api/subsonicArtists'
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary'; import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes'; import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; 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 { trackToSong } from '@/utils/library/advancedSearchLocal';
import { libraryIsReady } from '@/utils/library/libraryReady'; import { libraryIsReady } from '@/utils/library/libraryReady';
@@ -1,8 +1,8 @@
import { FolderOpen } from 'lucide-react'; import { FolderOpen } from 'lucide-react';
import { open as openDialog } from '@tauri-apps/plugin-dialog'; import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDownloadModalStore } from '../store/downloadModalStore'; import { useDownloadModalStore } from '@/features/offline/store/downloadModalStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
export default function DownloadFolderModal() { export default function DownloadFolderModal() {
const { isOpen, folder, remember, setFolder, setRemember, confirm, cancel } = useDownloadModalStore(); const { isOpen, folder, remember, setFolder, setRemember, confirm, cancel } = useDownloadModalStore();
@@ -2,7 +2,7 @@ import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { HardDriveDownload, Heart } from 'lucide-react'; import { HardDriveDownload, Heart } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { formatBytes } from '../utils/format/formatBytes'; import { formatBytes } from '@/utils/format/formatBytes';
const OPEN_DELAY_MS = 450; const OPEN_DELAY_MS = 450;
@@ -1,8 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { HardDriveDownload, Check, X } from 'lucide-react'; import { HardDriveDownload, Check, X } from 'lucide-react';
import { useZipDownloadStore } from '../store/zipDownloadStore'; import { useZipDownloadStore } from '@/features/offline/store/zipDownloadStore';
import { formatBytes } from '../utils/format/formatBytes'; import { formatBytes } from '@/utils/format/formatBytes';
function ZipDownloadItem({ id }: { id: string }) { function ZipDownloadItem({ id }: { id: string }) {
const dismiss = useZipDownloadStore(s => s.dismiss); const dismiss = useZipDownloadStore(s => s.dismiss);
@@ -1,6 +1,6 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import type { Location, NavigateFunction } from 'react-router-dom'; 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'; type ConnStatus = 'connected' | 'disconnected' | 'connecting' | 'unknown';
@@ -1,14 +1,14 @@
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useOfflineStore } from '../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { useConnectionStatus } from './useConnectionStatus'; import { useConnectionStatus } from '@/hooks/useConnectionStatus';
import { usePlayerStatsRecordingEnabled } from '@/features/stats'; import { usePlayerStatsRecordingEnabled } from '@/features/stats';
import { hasOfflineBrowsingContent } from '../utils/offline/favoritesOfflineBrowse'; import { hasOfflineBrowsingContent } from '@/features/offline/utils/favoritesOfflineBrowse';
import { useOfflineBrowseActive } from '../utils/offline/offlineBrowseMode'; import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { import {
buildOfflineBrowseContext, buildOfflineBrowseContext,
computeOfflineBrowseCapabilities, computeOfflineBrowseCapabilities,
type OfflineBrowseContext, type OfflineBrowseContext,
} from '../utils/offline/offlineBrowseContext'; } from '@/features/offline/utils/offlineBrowseContext';
/** Single subscription for shell and pages: offline browse mode + capabilities. */ /** Single subscription for shell and pages: offline browse mode + capabilities. */
export function useOfflineBrowseContext(): OfflineBrowseContext { export function useOfflineBrowseContext(): OfflineBrowseContext {
@@ -1,9 +1,9 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef } from 'react';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline/hooks/useOfflineBrowseContext';
import { import {
restoreMusicLibraryFiltersAfterOffline, restoreMusicLibraryFiltersAfterOffline,
suspendMusicLibraryFiltersForOffline, suspendMusicLibraryFiltersForOffline,
} from '../utils/offline/offlineLibraryFilterSuspend'; } from '@/features/offline/utils/offlineLibraryFilterSuspend';
/** Disable scoped library browse offline; restore the picker value when back online. */ /** Disable scoped library browse offline; restore the picker value when back online. */
export function useOfflineLibraryFilterSuspend(): void { export function useOfflineLibraryFilterSuspend(): void {
@@ -1,6 +1,6 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { listen } from '@tauri-apps/api/event'; 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. */ /** ZIP download progress events from Rust. */
export function useZipDownloadBridge() { export function useZipDownloadBridge() {
+55
View File
@@ -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 featurefeature 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';
@@ -3,16 +3,16 @@ import { useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { Play, HardDriveDownload, Trash2, ListPlus, ListMusic, Heart } from 'lucide-react'; import { Play, HardDriveDownload, Trash2, ListPlus, ListMusic, Heart } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useOfflineStore } from '../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { coverServerScopeForServerId } from '../cover/serverScope'; import { coverServerScopeForServerId } from '@/cover/serverScope';
import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { GRID_COVER_WARM_LIMIT } from '../cover/layoutSizes'; import { GRID_COVER_WARM_LIMIT } from '@/cover/layoutSizes';
import { VirtualCardGrid } from '../components/VirtualCardGrid'; import { VirtualCardGrid } from '@/components/VirtualCardGrid';
import { OfflineLibraryDiskStat } from '../components/OfflineLibraryDiskStat'; import { OfflineLibraryDiskStat } from '@/features/offline/components/OfflineLibraryDiskStat';
import { import {
buildOfflineCacheQueueTracks, buildOfflineCacheQueueTracks,
buildOfflineFavoritesQueueTracks, buildOfflineFavoritesQueueTracks,
@@ -31,16 +31,16 @@ import {
offlineTrackCount, offlineTrackCount,
type OfflineCoverQuadCell, type OfflineCoverQuadCell,
type OfflineLibraryCard, type OfflineLibraryCard,
} from '../utils/offline/offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
import { showToast } from '../utils/ui/toast'; import { showToast } from '@/utils/ui/toast';
import { shuffleArray } from '../utils/playback/shuffleArray'; import { shuffleArray } from '@/utils/playback/shuffleArray';
import { getMediaDir } from '../utils/media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { canonicalQueueServerKey, resolveIndexKey } from '../utils/server/serverIndexKey'; import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey';
import { reconcileAllLibraryTiersFromDisk } from '../utils/offline/libraryTierReconcile'; import { reconcileAllLibraryTiersFromDisk } from '@/features/offline/utils/libraryTierReconcile';
import { import {
inferPinSourcesFromLibraryIndex, inferPinSourcesFromLibraryIndex,
restoreOfflineLibraryPinSources, restoreOfflineLibraryPinSources,
} from '../utils/migrations/legacyOfflineFileMigration'; } from '@/features/offline/utils/legacyOfflineFileMigration';
const OFFLINE_CARD_COVER_CSS_PX = 300; const OFFLINE_CARD_COVER_CSS_PX = 300;
const OFFLINE_CACHE_GRID_KEY = '__offline_cache__'; const OFFLINE_CACHE_GRID_KEY = '__offline_cache__';
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { onInvoke } from '@/test/mocks/tauri'; 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>): DownloadJob { function job(over: Partial<DownloadJob>): DownloadJob {
return { return {
@@ -1,30 +1,30 @@
import { libraryUpsertSongsFromApi } from '../api/library'; import { libraryUpsertSongsFromApi } from '@/api/library';
import { buildStreamUrl } from '../api/subsonicStreamUrl'; import { buildStreamUrl } from '@/api/subsonicStreamUrl';
import { getAlbum } from '../api/subsonicLibrary'; import { getAlbum } from '@/api/subsonicLibrary';
import { getArtist } from '../api/subsonicArtists'; import { getArtist } from '@/api/subsonicArtists';
import type { SubsonicSong } from '../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { create } from 'zustand'; import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware'; import { persist, createJSONStorage } from 'zustand/middleware';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from './authStore'; import { useAuthStore } from '@/store/authStore';
import { showToast } from '../utils/ui/toast'; import { showToast } from '@/utils/ui/toast';
import { useOfflineJobStore, cancelledDownloads } from './offlineJobStore'; import { useOfflineJobStore, cancelledDownloads } from '@/features/offline/store/offlineJobStore';
import { useLocalPlaybackStore, type PinSource } from './localPlaybackStore'; import { useLocalPlaybackStore, type PinSource } from '@/store/localPlaybackStore';
import { getMediaDir } from '../utils/media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { import {
findLocalPlaybackEntry, findLocalPlaybackEntry,
isOfflinePinComplete, isOfflinePinComplete,
pendingOfflinePinSongs, pendingOfflinePinSongs,
} from '../utils/offline/offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
import { librarySqlServerId } from '../api/coverCache'; import { librarySqlServerId } from '@/api/coverCache';
import { resolveIndexKey, serverIndexKeyForProfile } from '../utils/server/serverIndexKey'; import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { isSmartPlaylistName } from '../utils/componentHelpers/playlistDetailHelpers'; import { isSmartPlaylistName } from '@/utils/componentHelpers/playlistDetailHelpers';
import { import {
enqueueOfflinePin, enqueueOfflinePin,
registerOfflinePinExecutor, registerOfflinePinExecutor,
removeOfflinePinTask, removeOfflinePinTask,
type OfflinePinTask, type OfflinePinTask,
} from '../utils/offline/offlinePinQueue'; } from '@/features/offline/utils/offlinePinQueue';
/** @deprecated Metadata lives in the library index; kept for type-compat during transition. */ /** @deprecated Metadata lives in the library index; kept for type-compat during transition. */
export interface OfflineTrackMeta { export interface OfflineTrackMeta {
@@ -60,7 +60,7 @@ export interface OfflineAlbumMeta {
type?: 'album' | 'playlist' | 'artist' | 'track'; type?: 'album' | 'playlist' | 'artist' | 'track';
} }
export type { DownloadJob } from './offlineJobStore'; export type { DownloadJob } from '@/features/offline/store/offlineJobStore';
function serverIndexKeyForOffline(serverId: string): string { function serverIndexKeyForOffline(serverId: string): string {
const server = useAuthStore.getState().servers.find(s => s.id === serverId); const server = useAuthStore.getState().servers.find(s => s.id === serverId);
@@ -1,28 +1,28 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { import {
favoritesOfflineBrowseEnabled, favoritesOfflineBrowseEnabled,
hasOfflineBrowsingContent, hasOfflineBrowsingContent,
} from './favoritesOfflineBrowse'; } from '@/features/offline/utils/favoritesOfflineBrowse';
import { import {
isOfflineSidebarLibraryNavAllowed, isOfflineSidebarLibraryNavAllowed,
isOfflineSidebarNavAllowed, isOfflineSidebarNavAllowed,
isOfflineSidebarSystemNavAllowed, isOfflineSidebarSystemNavAllowed,
} from './offlineNavPolicy'; } from '@/features/offline/utils/offlineNavPolicy';
import { import {
loadStarredFromLibraryIndex, loadStarredFromLibraryIndex,
mergeStarredFromServers, mergeStarredFromServers,
} from './offlineStarredLoad'; } from '@/features/offline/utils/offlineStarredLoad';
import { resolveAlbumForServer } from './offlineMediaResolve'; import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaResolve';
const isActiveServerReachableMock = vi.fn(() => true); const isActiveServerReachableMock = vi.fn(() => true);
const shouldAttemptSubsonicForServerMock = vi.fn((_serverId: string, _trackId?: string) => true); const shouldAttemptSubsonicForServerMock = vi.fn((_serverId: string, _trackId?: string) => true);
vi.mock('../network/activeServerReachability', () => ({ vi.mock('@/utils/network/activeServerReachability', () => ({
isActiveServerReachable: () => isActiveServerReachableMock(), isActiveServerReachable: () => isActiveServerReachableMock(),
})); }));
vi.mock('../network/subsonicNetworkGuard', () => ({ vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) => shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) =>
shouldAttemptSubsonicForServerMock(serverId, trackId), shouldAttemptSubsonicForServerMock(serverId, trackId),
})); }));
@@ -32,11 +32,11 @@ const libraryAdvancedSearchMock = vi.fn();
const libraryGetTracksByAlbumMock = vi.fn(); const libraryGetTracksByAlbumMock = vi.fn();
const libraryGetTracksBatchChunkedMock = vi.fn(); const libraryGetTracksBatchChunkedMock = vi.fn();
vi.mock('../../api/subsonicLibrary', () => ({ vi.mock('@/api/subsonicLibrary', () => ({
getAlbumForServer: (...args: unknown[]) => getAlbumForServerMock(...args), getAlbumForServer: (...args: unknown[]) => getAlbumForServerMock(...args),
})); }));
vi.mock('../../api/library', () => ({ vi.mock('@/api/library', () => ({
libraryAdvancedSearch: (...args: unknown[]) => libraryAdvancedSearchMock(...args), libraryAdvancedSearch: (...args: unknown[]) => libraryAdvancedSearchMock(...args),
libraryGetTracksByAlbum: (...args: unknown[]) => libraryGetTracksByAlbumMock(...args), libraryGetTracksByAlbum: (...args: unknown[]) => libraryGetTracksByAlbumMock(...args),
libraryGetTracksBatchChunked: (...args: unknown[]) => libraryGetTracksBatchChunkedMock(...args), libraryGetTracksBatchChunked: (...args: unknown[]) => libraryGetTracksBatchChunkedMock(...args),
@@ -1,7 +1,7 @@
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import type { OfflineAlbumMeta } from '../../store/offlineStore'; import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { countFavoriteAutoTracks, hasAnyOfflineAlbums } from './offlineLibraryHelpers'; import { countFavoriteAutoTracks, hasAnyOfflineAlbums } from '@/features/offline/utils/offlineLibraryHelpers';
/** Saved servers with a local library index (cross-server favorites scope). */ /** Saved servers with a local library index (cross-server favorites scope). */
export function favoritesServerIds(): string[] { export function favoritesServerIds(): string[] {
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; 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', () => { describe('favoritesOfflineConstants', () => {
it('uses a stable virtual job id', () => { it('uses a stable virtual job id', () => {
@@ -1,12 +1,12 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useOfflineJobStore } from '../../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { FAVORITES_OFFLINE_JOB_ID } from './favoritesOfflineConstants'; import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants';
import { import {
mergeStarredSongsUnion, mergeStarredSongsUnion,
onFavoritesOfflineStarChange, onFavoritesOfflineStarChange,
} from './favoritesOfflineSync'; } from '@/features/offline/utils/favoritesOfflineSync';
const getStarredForServerMock = vi.fn(async (_serverId: string) => ({ const getStarredForServerMock = vi.fn(async (_serverId: string) => ({
artists: [], artists: [],
@@ -16,19 +16,19 @@ const getStarredForServerMock = vi.fn(async (_serverId: string) => ({
const isActiveServerReachableMock = vi.fn(() => true); const isActiveServerReachableMock = vi.fn(() => true);
vi.mock('../network/activeServerReachability', () => ({ vi.mock('@/utils/network/activeServerReachability', () => ({
isActiveServerReachable: () => isActiveServerReachableMock(), isActiveServerReachable: () => isActiveServerReachableMock(),
})); }));
vi.mock('../../api/subsonicStarRating', () => ({ vi.mock('@/api/subsonicStarRating', () => ({
getStarredForServer: (serverId: string) => getStarredForServerMock(serverId), getStarredForServer: (serverId: string) => getStarredForServerMock(serverId),
})); }));
vi.mock('../../api/subsonicLibrary', () => ({ vi.mock('@/api/subsonicLibrary', () => ({
getAlbumForServer: vi.fn(async () => ({ songs: [] })), getAlbumForServer: vi.fn(async () => ({ songs: [] })),
})); }));
vi.mock('../../api/subsonicArtists', () => ({ vi.mock('@/api/subsonicArtists', () => ({
getArtistForServer: vi.fn(async () => ({ albums: [] })), getArtistForServer: vi.fn(async () => ({ albums: [] })),
})); }));
@@ -1,27 +1,27 @@
import { libraryUpsertSongsFromApi } from '../../api/library'; import { libraryUpsertSongsFromApi } from '@/api/library';
import { librarySqlServerId } from '../../api/coverCache'; import { librarySqlServerId } from '@/api/coverCache';
import { getAlbumForServer } from '../../api/subsonicLibrary'; import { getAlbumForServer } from '@/api/subsonicLibrary';
import { getArtistForServer } from '../../api/subsonicArtists'; import { getArtistForServer } from '@/api/subsonicArtists';
import { getStarredForServer } from '../../api/subsonicStarRating'; import { getStarredForServer } from '@/api/subsonicStarRating';
import { buildStreamUrlForServer } from '../../api/subsonicStreamUrl'; import { buildStreamUrlForServer } from '@/api/subsonicStreamUrl';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import i18n from '../../i18n'; import i18n from '@/i18n';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { cancelledDownloads, useOfflineJobStore } from '../../store/offlineJobStore'; import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { useFavoritesOfflineSyncStore } from '../../store/favoritesOfflineSyncStore'; import { useFavoritesOfflineSyncStore } from '@/features/offline/store/favoritesOfflineSyncStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { getMediaDir } from '../media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { FAVORITES_OFFLINE_JOB_ID } from './favoritesOfflineConstants'; import { FAVORITES_OFFLINE_JOB_ID } from '@/features/offline/utils/favoritesOfflineConstants';
import { isActiveServerReachable } from '../network/activeServerReachability'; import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
import { favoritesServerIds } from './favoritesOfflineBrowse'; import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse';
import { loadAlbumFromLibraryIndex } from './offlineLibraryIndexLoad'; import { loadAlbumFromLibraryIndex } from '@/features/offline/utils/offlineLibraryIndexLoad';
import { import {
entryBelongsToServer, entryBelongsToServer,
hasLocalLibraryBytes, hasLocalLibraryBytes,
hasLocalFavoriteAutoBytes, hasLocalFavoriteAutoBytes,
} from './offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
const CONCURRENCY = 2; const CONCURRENCY = 2;
const DEBOUNCE_MS = 600; const DEBOUNCE_MS = 600;
@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { import {
entryNeedsFileRelocation, entryNeedsFileRelocation,
restoreOfflineLibraryPinSources, restoreOfflineLibraryPinSources,
} from './legacyOfflineFileMigration'; } from '@/features/offline/utils/legacyOfflineFileMigration';
import type { LocalPlaybackEntry } from '../../store/localPlaybackStore'; import type { LocalPlaybackEntry } from '@/store/localPlaybackStore';
function entry(overrides: Partial<LocalPlaybackEntry>): LocalPlaybackEntry { function entry(overrides: Partial<LocalPlaybackEntry>): LocalPlaybackEntry {
return { return {
@@ -1,13 +1,13 @@
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { libraryGetTracksBatch } from '../../api/library'; import { libraryGetTracksBatch } from '@/api/library';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useOfflineStore, type OfflineAlbumMeta } from '../../store/offlineStore'; import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { useLocalPlaybackStore, type LocalPlaybackEntry, type PinSource } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore, type LocalPlaybackEntry, type PinSource } from '@/store/localPlaybackStore';
import { localPlaybackEntryKey } from '../../store/localPlaybackKeys'; import { localPlaybackEntryKey } from '@/store/localPlaybackKeys';
import { importLegacyLocalPlayback } from '../../store/localPlaybackMigration'; import { importLegacyLocalPlayback } from '@/store/localPlaybackMigration';
import { getMediaDir } from '../media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { resolveServerIdForIndexKey } from '../server/serverLookup'; import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { resolveIndexKey } from '../server/serverIndexKey'; import { resolveIndexKey } from '@/utils/server/serverIndexKey';
interface LegacyOfflineMigrationResult { interface LegacyOfflineMigrationResult {
trackId: string; trackId: string;
@@ -1,17 +1,17 @@
import { libraryUpsertSongsFromApi } from '../../api/library'; import { libraryUpsertSongsFromApi } from '@/api/library';
import { librarySqlServerId } from '../../api/coverCache'; import { librarySqlServerId } from '@/api/coverCache';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import type { LocalPlaybackEntry, PinSource } from '../../store/localPlaybackStore'; import type { LocalPlaybackEntry, PinSource } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { getMediaDir } from '../media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { import {
entryBelongsToServer, entryBelongsToServer,
findLocalPlaybackEntry, findLocalPlaybackEntry,
indexKeyBelongsToServer, indexKeyBelongsToServer,
} from './offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
interface LibraryTrackProbeResult { interface LibraryTrackProbeResult {
path: string; path: string;
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { offlineActionPolicy } from './offlineActionPolicy'; import { offlineActionPolicy } from '@/features/offline/utils/offlineActionPolicy';
describe('offlineActionPolicy', () => { describe('offlineActionPolicy', () => {
it('allows all mutations when offline browse is inactive', () => { it('allows all mutations when offline browse is inactive', () => {
@@ -1,10 +1,10 @@
import type { SubsonicAlbum } from '../../api/subsonicTypes'; import type { SubsonicAlbum } from '@/api/subsonicTypes';
import type { AlbumBrowseQuery } from '../library/albumBrowseTypes'; import type { AlbumBrowseQuery } from '@/utils/library/albumBrowseTypes';
import { isOfflineBrowseActive } from './offlineBrowseMode'; import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { import {
fetchOfflineLocalAlbumCatalogChunk, fetchOfflineLocalAlbumCatalogChunk,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from './offlineLocalBrowse'; } from '@/features/offline/utils/offlineLocalBrowse';
type OfflineAlbumCatalogChunk = { type OfflineAlbumCatalogChunk = {
albums: SubsonicAlbum[]; albums: SubsonicAlbum[];
@@ -1,24 +1,24 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { import {
buildOfflineBrowseContext, buildOfflineBrowseContext,
computeOfflineBrowseCapabilities, computeOfflineBrowseCapabilities,
offlineBrowseNavFlags, offlineBrowseNavFlags,
} from './offlineBrowseContext'; } from '@/features/offline/utils/offlineBrowseContext';
vi.mock('./offlineLocalBrowse', () => ({ vi.mock('@/features/offline/utils/offlineLocalBrowse', () => ({
offlineLocalBrowseEnabled: vi.fn(() => false), offlineLocalBrowseEnabled: vi.fn(() => false),
countLocalBrowsableTracks: vi.fn(() => 0), countLocalBrowsableTracks: vi.fn(() => 0),
})); }));
vi.mock('./offlinePlaylistBrowse', () => ({ vi.mock('@/features/offline/utils/offlinePlaylistBrowse', () => ({
playlistsOfflineBrowseEnabled: vi.fn(() => false), playlistsOfflineBrowseEnabled: vi.fn(() => false),
})); }));
import { offlineLocalBrowseEnabled } from './offlineLocalBrowse'; import { offlineLocalBrowseEnabled } from '@/features/offline/utils/offlineLocalBrowse';
import { playlistsOfflineBrowseEnabled } from './offlinePlaylistBrowse'; import { playlistsOfflineBrowseEnabled } from '@/features/offline/utils/offlinePlaylistBrowse';
describe('offlineBrowseContext', () => { describe('offlineBrowseContext', () => {
beforeEach(() => { beforeEach(() => {
@@ -1,10 +1,10 @@
import type { ConnectionStatus } from '../../hooks/useConnectionStatus'; import type { ConnectionStatus } from '@/hooks/useConnectionStatus';
import type { OfflineAlbumMeta } from '../../store/offlineStore'; import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { favoritesOfflineBrowseEnabled } from './favoritesOfflineBrowse'; import { favoritesOfflineBrowseEnabled } from '@/features/offline/utils/favoritesOfflineBrowse';
import { hasOfflineBrowseCapability } from './offlineBrowseRouting'; import { hasOfflineBrowseCapability } from '@/features/offline/utils/offlineBrowseRouting';
import { offlineLocalBrowseEnabled } from './offlineLocalBrowse'; import { offlineLocalBrowseEnabled } from '@/features/offline/utils/offlineLocalBrowse';
import { playlistsOfflineBrowseEnabled } from './offlinePlaylistBrowse'; import { playlistsOfflineBrowseEnabled } from '@/features/offline/utils/offlinePlaylistBrowse';
import { hasAnyOfflineAlbums } from './offlineLibraryHelpers'; import { hasAnyOfflineAlbums } from '@/features/offline/utils/offlineLibraryHelpers';
export type OfflineBrowseCapabilities = { export type OfflineBrowseCapabilities = {
localLibrary: boolean; localLibrary: boolean;
@@ -1,8 +1,8 @@
import { renderHook, act } from '@testing-library/react'; import { renderHook, act } from '@testing-library/react';
import { describe, expect, it, beforeEach } from 'vitest'; import { describe, expect, it, beforeEach } from 'vitest';
import { useDevOfflineBrowseStore } from '../../store/devOfflineBrowseStore'; import { useDevOfflineBrowseStore } from '@/features/offline/store/devOfflineBrowseStore';
import { resetActiveServerConnectionSnapshot } from '../network/activeServerReachability'; import { resetActiveServerConnectionSnapshot } from '@/utils/network/activeServerReachability';
import { useOfflineBrowseActive } from './offlineBrowseMode'; import { useOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
describe('useOfflineBrowseActive', () => { describe('useOfflineBrowseActive', () => {
beforeEach(() => { beforeEach(() => {
@@ -1,9 +1,9 @@
import { import {
isDevOfflineBrowseForced, isDevOfflineBrowseForced,
useDevOfflineBrowseStore, useDevOfflineBrowseStore,
} from '../../store/devOfflineBrowseStore'; } from '@/features/offline/store/devOfflineBrowseStore';
import { useConnectionStatus } from '../../hooks/useConnectionStatus'; import { useConnectionStatus } from '@/hooks/useConnectionStatus';
import { isActiveServerReachable } from '../network/activeServerReachability'; import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
/** True when browse/detail pages should use local-bytes-only data sources. */ /** True when browse/detail pages should use local-bytes-only data sources. */
export function isOfflineBrowseActive(): boolean { export function isOfflineBrowseActive(): boolean {
@@ -3,7 +3,7 @@ import {
hasOfflineBrowseCapability, hasOfflineBrowseCapability,
isPathOfflineBrowsable, isPathOfflineBrowsable,
resolveOfflineDisconnectNavAction, resolveOfflineDisconnectNavAction,
} from './offlineBrowseRouting'; } from '@/features/offline/utils/offlineBrowseRouting';
describe('offlineBrowseRouting', () => { describe('offlineBrowseRouting', () => {
it('hasOfflineBrowseCapability is true when any offline surface exists', () => { it('hasOfflineBrowseCapability is true when any offline surface exists', () => {
@@ -1,4 +1,4 @@
import { isOfflineSidebarNavAllowed } from './offlineNavPolicy'; import { isOfflineSidebarNavAllowed } from '@/features/offline/utils/offlineNavPolicy';
/** Any offline browse surface the disconnect fork may use. */ /** Any offline browse surface the disconnect fork may use. */
export function hasOfflineBrowseCapability( export function hasOfflineBrowseCapability(
@@ -1,10 +1,10 @@
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { import {
resetOfflineLibraryFilterSuspendState, resetOfflineLibraryFilterSuspendState,
restoreMusicLibraryFiltersAfterOffline, restoreMusicLibraryFiltersAfterOffline,
suspendMusicLibraryFiltersForOffline, suspendMusicLibraryFiltersForOffline,
} from './offlineLibraryFilterSuspend'; } from '@/features/offline/utils/offlineLibraryFilterSuspend';
describe('offlineLibraryFilterSuspend', () => { describe('offlineLibraryFilterSuspend', () => {
beforeEach(() => { beforeEach(() => {
@@ -1,4 +1,4 @@
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
let savedFilterByServer: Record<string, 'all' | string> | null = null; let savedFilterByServer: Record<string, 'all' | string> | null = null;
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { switchActiveServer } from '../server/switchActiveServer'; import { switchActiveServer } from '@/utils/server/switchActiveServer';
import { import {
buildOfflineCacheQueueTracks, buildOfflineCacheQueueTracks,
countFavoriteAutoTracks, countFavoriteAutoTracks,
@@ -15,17 +15,17 @@ import {
offlineAlbumCoverScope, offlineAlbumCoverScope,
offlineTrackCount, offlineTrackCount,
type OfflineLibraryCard, type OfflineLibraryCard,
} from './offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
import * as libraryApi from '../../api/library'; import * as libraryApi from '@/api/library';
import { coverStorageKey } from '../../cover/storageKeys'; import { coverStorageKey } from '@/cover/storageKeys';
import { resolveCoverDisplayTier } from '../../cover/tiers'; import { resolveCoverDisplayTier } from '@/cover/tiers';
vi.mock('../server/switchActiveServer', () => ({ vi.mock('@/utils/server/switchActiveServer', () => ({
switchActiveServer: vi.fn(async () => true), switchActiveServer: vi.fn(async () => true),
})); }));
vi.mock('../../api/library', async importOriginal => { vi.mock('@/api/library', async importOriginal => {
const actual = await importOriginal<typeof import('../../api/library')>(); const actual = await importOriginal<typeof import('@/api/library')>();
const libraryGetTracksBatch = vi.fn(); const libraryGetTracksBatch = vi.fn();
return { return {
...actual, ...actual,
@@ -1,16 +1,16 @@
import type { LibraryTrackDto } from '../../api/library'; import type { LibraryTrackDto } from '@/api/library';
import { libraryGetTrack, libraryGetTracksBatchChunked } from '../../api/library'; import { libraryGetTrack, libraryGetTracksBatchChunked } from '@/api/library';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import type { CoverServerScope } from '../../cover/types'; import type { CoverServerScope } from '@/cover/types';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import type { LocalPlaybackEntry, PinnedGroup, PinSource } from '../../store/localPlaybackStore'; import type { LocalPlaybackEntry, PinnedGroup, PinSource } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineStore, type OfflineAlbumMeta } from '../../store/offlineStore'; import { useOfflineStore, type OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { resolveTrackCoverArtId, trackToSong } from '../library/advancedSearchLocal'; import { resolveTrackCoverArtId, trackToSong } from '@/utils/library/advancedSearchLocal';
import { canonicalQueueServerKey, resolveIndexKey } from '../server/serverIndexKey'; import { canonicalQueueServerKey, resolveIndexKey } from '@/utils/server/serverIndexKey';
import type { Track } from '../../store/playerStoreTypes'; import type { Track } from '@/store/playerStoreTypes';
import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '../server/serverLookup'; import { findServerByIdOrIndexKey, resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { serverIndexKeyForProfile } from '../server/serverIndexKey'; import { serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
export interface OfflineLibraryCard { export interface OfflineLibraryCard {
serverIndexKey: string; serverIndexKey: string;
@@ -165,7 +165,7 @@ export async function syncAlbumLibraryTierFromDisk(
songs: SubsonicSong[], songs: SubsonicSong[],
pinSource?: PinSource, pinSource?: PinSource,
): Promise<number> { ): Promise<number> {
const { reconcileLibraryTierForAlbum } = await import('./libraryTierReconcile'); const { reconcileLibraryTierForAlbum } = await import('@/features/offline/utils/libraryTierReconcile');
const result = await reconcileLibraryTierForAlbum(serverId, songs, pinSource ?? { const result = await reconcileLibraryTierForAlbum(serverId, songs, pinSource ?? {
kind: 'album', kind: 'album',
sourceId: albumId, sourceId: albumId,
@@ -1,14 +1,14 @@
import { libraryAdvancedSearch, libraryGetTracksByAlbum } from '../../api/library'; import { libraryAdvancedSearch, libraryGetTracksByAlbum } from '@/api/library';
import type { import type {
SubsonicAlbum, SubsonicAlbum,
SubsonicArtist, SubsonicArtist,
SubsonicSong, SubsonicSong,
} from '../../api/subsonicTypes'; } from '@/api/subsonicTypes';
import { import {
albumToAlbum, albumToAlbum,
artistToArtist, artistToArtist,
trackToSong, trackToSong,
} from '../library/advancedSearchLocal'; } from '@/utils/library/advancedSearchLocal';
export async function loadAlbumFromLibraryIndex( export async function loadAlbumFromLibraryIndex(
serverId: string, serverId: string,
@@ -1,19 +1,19 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { LibraryTrackDto } from '../../api/library'; import type { LibraryTrackDto } from '@/api/library';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { import {
countLocalBrowsableTracks, countLocalBrowsableTracks,
fetchOfflineLocalBrowsableSongPage, fetchOfflineLocalBrowsableSongPage,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from './offlineLocalBrowse'; } from '@/features/offline/utils/offlineLocalBrowse';
const { libraryGetTracksBatchChunkedMock } = vi.hoisted(() => ({ const { libraryGetTracksBatchChunkedMock } = vi.hoisted(() => ({
libraryGetTracksBatchChunkedMock: vi.fn(async (): Promise<LibraryTrackDto[]> => []), libraryGetTracksBatchChunkedMock: vi.fn(async (): Promise<LibraryTrackDto[]> => []),
})); }));
vi.mock('../../api/library', () => ({ vi.mock('@/api/library', () => ({
libraryGetTracksBatchChunked: libraryGetTracksBatchChunkedMock, libraryGetTracksBatchChunked: libraryGetTracksBatchChunkedMock,
libraryGetTracksByAlbum: vi.fn(async () => []), libraryGetTracksByAlbum: vi.fn(async () => []),
libraryAdvancedSearch: vi.fn(async () => ({ albums: [], artists: [], tracks: [] })), libraryAdvancedSearch: vi.fn(async () => ({ albums: [], artists: [], tracks: [] })),
@@ -1,26 +1,26 @@
import type { LibraryTrackDto } from '../../api/library'; import type { LibraryTrackDto } from '@/api/library';
import { libraryAdvancedSearch, libraryGetTracksBatchChunked, libraryGetTracksByAlbum } from '../../api/library'; import { libraryAdvancedSearch, libraryGetTracksBatchChunked, libraryGetTracksByAlbum } from '@/api/library';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/api/subsonicTypes';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import type { LocalPlaybackEntry } from '../../store/localPlaybackStore'; import type { LocalPlaybackEntry } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { import {
albumToAlbum, albumToAlbum,
artistToArtist, artistToArtist,
resolveTrackCoverArtId, resolveTrackCoverArtId,
trackToSong, trackToSong,
} from '../library/advancedSearchLocal'; } from '@/utils/library/advancedSearchLocal';
import { albumIsCompilationFromTrackDtos } from '../library/albumCompilation'; import { albumIsCompilationFromTrackDtos } from '@/utils/library/albumCompilation';
import { import {
filterAlbumsByCompilation, filterAlbumsByCompilation,
filterAlbumsByGenres, filterAlbumsByGenres,
filterAlbumsByStarred, filterAlbumsByStarred,
filterAlbumsByYearBounds, filterAlbumsByYearBounds,
} from '../library/albumBrowseFilters'; } from '@/utils/library/albumBrowseFilters';
import type { AlbumBrowseQuery } from '../library/albumBrowseTypes'; import type { AlbumBrowseQuery } from '@/utils/library/albumBrowseTypes';
import { sortSubsonicAlbums } from '../library/albumBrowseSort'; import { sortSubsonicAlbums } from '@/utils/library/albumBrowseSort';
import { isLosslessSuffix } from '../library/losslessFormats'; import { isLosslessSuffix } from '@/utils/library/losslessFormats';
import { entryBelongsToServer } from './offlineLibraryHelpers'; import { entryBelongsToServer } from '@/features/offline/utils/offlineLibraryHelpers';
function sortBrowsableSongs(songs: SubsonicSong[]): SubsonicSong[] { function sortBrowsableSongs(songs: SubsonicSong[]): SubsonicSong[] {
return [...songs].sort((a, b) => a.title.localeCompare(b.title)); return [...songs].sort((a, b) => a.title.localeCompare(b.title));
@@ -1,11 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { import {
resolveAlbum, resolveAlbum,
resolveAlbumForActiveServer, resolveAlbumForActiveServer,
resolveArtist, resolveArtist,
resolvePlaylist, resolvePlaylist,
} from './offlineMediaResolve'; } from '@/features/offline/utils/offlineMediaResolve';
const isOfflineBrowseActiveMock = vi.fn(() => false); const isOfflineBrowseActiveMock = vi.fn(() => false);
const offlineLocalBrowseEnabledMock = vi.fn((_serverId: string) => 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 getPlaylistForServerMock = vi.fn((_serverId: string, _playlistId: string) => ({}));
const libraryIsReadyMock = vi.fn(async (_serverId: string) => false); const libraryIsReadyMock = vi.fn(async (_serverId: string) => false);
vi.mock('./offlineBrowseMode', () => ({ vi.mock('@/features/offline/utils/offlineBrowseMode', () => ({
isOfflineBrowseActive: () => isOfflineBrowseActiveMock(), isOfflineBrowseActive: () => isOfflineBrowseActiveMock(),
})); }));
vi.mock('./offlineLocalBrowse', () => ({ vi.mock('@/features/offline/utils/offlineLocalBrowse', () => ({
offlineLocalBrowseEnabled: (id: string) => offlineLocalBrowseEnabledMock(id), offlineLocalBrowseEnabled: (id: string) => offlineLocalBrowseEnabledMock(id),
loadAlbumFromLocalPlayback: (serverId: string, albumId: string) => loadAlbumFromLocalPlayback: (serverId: string, albumId: string) =>
loadAlbumFromLocalPlaybackMock(serverId, albumId), loadAlbumFromLocalPlaybackMock(serverId, albumId),
@@ -33,36 +33,36 @@ vi.mock('./offlineLocalBrowse', () => ({
loadArtistFromLocalPlaybackMock(serverId, artistId), loadArtistFromLocalPlaybackMock(serverId, artistId),
})); }));
vi.mock('./offlineLibraryIndexLoad', () => ({ vi.mock('@/features/offline/utils/offlineLibraryIndexLoad', () => ({
loadAlbumFromLibraryIndex: (...args: unknown[]) => loadAlbumFromLibraryIndexMock(...args), loadAlbumFromLibraryIndex: (...args: unknown[]) => loadAlbumFromLibraryIndexMock(...args),
loadArtistFromLibraryIndex: (...args: unknown[]) => loadArtistFromLibraryIndexMock(...args), loadArtistFromLibraryIndex: (...args: unknown[]) => loadArtistFromLibraryIndexMock(...args),
})); }));
vi.mock('./offlinePlaylistBrowse', () => ({ vi.mock('@/features/offline/utils/offlinePlaylistBrowse', () => ({
playlistsOfflineBrowseEnabled: (id: string) => playlistsOfflineBrowseEnabledMock(id), playlistsOfflineBrowseEnabled: (id: string) => playlistsOfflineBrowseEnabledMock(id),
loadOfflineBrowsablePlaylist: (playlistId: string, serverId: string) => loadOfflineBrowsablePlaylist: (playlistId: string, serverId: string) =>
loadOfflineBrowsablePlaylistMock(playlistId, serverId), loadOfflineBrowsablePlaylistMock(playlistId, serverId),
})); }));
vi.mock('../network/subsonicNetworkGuard', () => ({ vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) => shouldAttemptSubsonicForServer: (serverId: string, trackId?: string) =>
shouldAttemptSubsonicForServerMock(serverId, trackId), shouldAttemptSubsonicForServerMock(serverId, trackId),
})); }));
vi.mock('../../api/subsonicLibrary', () => ({ vi.mock('@/api/subsonicLibrary', () => ({
getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId), getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId),
})); }));
vi.mock('../../api/subsonicArtists', () => ({ vi.mock('@/api/subsonicArtists', () => ({
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId), getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
})); }));
vi.mock('../../api/subsonicPlaylists', () => ({ vi.mock('@/api/subsonicPlaylists', () => ({
getPlaylistForServer: (serverId: string, playlistId: string) => getPlaylistForServer: (serverId: string, playlistId: string) =>
getPlaylistForServerMock(serverId, playlistId), getPlaylistForServerMock(serverId, playlistId),
})); }));
vi.mock('../library/libraryReady', () => ({ vi.mock('@/utils/library/libraryReady', () => ({
libraryIsReady: (serverId: string) => libraryIsReadyMock(serverId), libraryIsReady: (serverId: string) => libraryIsReadyMock(serverId),
})); }));
@@ -1,29 +1,29 @@
import { getAlbumForServer } from '../../api/subsonicLibrary'; import { getAlbumForServer } from '@/api/subsonicLibrary';
import { getArtistForServer } from '../../api/subsonicArtists'; import { getArtistForServer } from '@/api/subsonicArtists';
import { getPlaylistForServer } from '../../api/subsonicPlaylists'; import { getPlaylistForServer } from '@/api/subsonicPlaylists';
import type { import type {
SubsonicAlbum, SubsonicAlbum,
SubsonicArtist, SubsonicArtist,
SubsonicPlaylist, SubsonicPlaylist,
SubsonicSong, SubsonicSong,
} from '../../api/subsonicTypes'; } from '@/api/subsonicTypes';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { shouldAttemptSubsonicForServer } from '../network/subsonicNetworkGuard'; import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { isOfflineBrowseActive } from './offlineBrowseMode'; import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { libraryIsReady } from '../library/libraryReady'; import { libraryIsReady } from '@/utils/library/libraryReady';
import { import {
loadAlbumFromLibraryIndex, loadAlbumFromLibraryIndex,
loadArtistFromLibraryIndex, loadArtistFromLibraryIndex,
} from './offlineLibraryIndexLoad'; } from '@/features/offline/utils/offlineLibraryIndexLoad';
import { import {
loadAlbumFromLocalPlayback, loadAlbumFromLocalPlayback,
loadArtistFromLocalPlayback, loadArtistFromLocalPlayback,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from './offlineLocalBrowse'; } from '@/features/offline/utils/offlineLocalBrowse';
import { import {
loadOfflineBrowsablePlaylist, loadOfflineBrowsablePlaylist,
playlistsOfflineBrowseEnabled, playlistsOfflineBrowseEnabled,
} from './offlinePlaylistBrowse'; } from '@/features/offline/utils/offlinePlaylistBrowse';
export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] }; export type ResolvedAlbum = { album: SubsonicAlbum; songs: SubsonicSong[] };
@@ -1,12 +1,12 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { cancelledDownloads, useOfflineJobStore } from '../../store/offlineJobStore'; import { cancelledDownloads, useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { import {
clearOfflinePinTasks, clearOfflinePinTasks,
dequeueOfflinePin, dequeueOfflinePin,
enqueueOfflinePin, enqueueOfflinePin,
isAlbumPinQueued, isAlbumPinQueued,
registerOfflinePinExecutor, registerOfflinePinExecutor,
} from './offlinePinQueue'; } from '@/features/offline/utils/offlinePinQueue';
describe('offlinePinQueue', () => { describe('offlinePinQueue', () => {
beforeEach(() => { beforeEach(() => {
@@ -1,10 +1,10 @@
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import type { PinSource } from '../../store/localPlaybackStore'; import type { PinSource } from '@/store/localPlaybackStore';
import { import {
cancelledDownloads, cancelledDownloads,
useOfflineJobStore, useOfflineJobStore,
type OfflinePinQueueEntry, type OfflinePinQueueEntry,
} from '../../store/offlineJobStore'; } from '@/features/offline/store/offlineJobStore';
export type OfflinePinKind = PinSource['kind']; export type OfflinePinKind = PinSource['kind'];
@@ -1,16 +1,16 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as libraryApi from '../../api/library'; import * as libraryApi from '@/api/library';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { import {
fetchOfflineBrowsablePlaylists, fetchOfflineBrowsablePlaylists,
loadOfflineBrowsablePlaylist, loadOfflineBrowsablePlaylist,
playlistsOfflineBrowseEnabled, playlistsOfflineBrowseEnabled,
} from './offlinePlaylistBrowse'; } from '@/features/offline/utils/offlinePlaylistBrowse';
vi.mock('../../api/library', () => ({ vi.mock('@/api/library', () => ({
libraryGetTracksBatchChunked: vi.fn(), libraryGetTracksBatchChunked: vi.fn(),
})); }));
@@ -1,15 +1,15 @@
import { libraryGetTracksBatchChunked } from '../../api/library'; import { libraryGetTracksBatchChunked } from '@/api/library';
import type { SubsonicPlaylist, SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicPlaylist, SubsonicSong } from '@/api/subsonicTypes';
import { useLibraryIndexStore } from '../../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import type { PinnedGroup } from '../../store/localPlaybackStore'; import type { PinnedGroup } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { trackToSong } from '../library/advancedSearchLocal'; import { trackToSong } from '@/utils/library/advancedSearchLocal';
import { isManualOfflinePlaylist } from './pinnedOfflineSync'; import { isManualOfflinePlaylist } from '@/features/offline/utils/pinnedOfflineSync';
import { import {
hasLocalLibraryBytes, hasLocalLibraryBytes,
indexKeyBelongsToServer, indexKeyBelongsToServer,
resolveOfflineAlbumMeta, resolveOfflineAlbumMeta,
} from './offlineLibraryHelpers'; } from '@/features/offline/utils/offlineLibraryHelpers';
function listPlaylistPinnedGroupsForServer(serverId: string): PinnedGroup[] { function listPlaylistPinnedGroupsForServer(serverId: string): PinnedGroup[] {
return useLocalPlaybackStore.getState() return useLocalPlaybackStore.getState()
@@ -1,24 +1,24 @@
import { getStarredForServer } from '../../api/subsonicStarRating'; import { getStarredForServer } from '@/api/subsonicStarRating';
import { libraryAdvancedSearch } from '../../api/library'; import { libraryAdvancedSearch } from '@/api/library';
import type { import type {
StarredResults, StarredResults,
SubsonicAlbum, SubsonicAlbum,
SubsonicArtist, SubsonicArtist,
SubsonicSong, SubsonicSong,
} from '../../api/subsonicTypes'; } from '@/api/subsonicTypes';
import { isActiveServerReachable } from '../network/activeServerReachability'; import { isActiveServerReachable } from '@/utils/network/activeServerReachability';
import { import {
albumToAlbum, albumToAlbum,
trackToSong, trackToSong,
} from '../library/advancedSearchLocal'; } from '@/utils/library/advancedSearchLocal';
import { dedupeById } from '../dedupeById'; import { dedupeById } from '@/utils/dedupeById';
import { isOfflineBrowseActive } from './offlineBrowseMode'; import { isOfflineBrowseActive } from '@/features/offline/utils/offlineBrowseMode';
import { favoritesServerIds } from './favoritesOfflineBrowse'; import { favoritesServerIds } from '@/features/offline/utils/favoritesOfflineBrowse';
import { import {
buildAlbumFromTracks, buildAlbumFromTracks,
fetchBrowsableLocalTrackDtos, fetchBrowsableLocalTrackDtos,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from './offlineLocalBrowse'; } from '@/features/offline/utils/offlineLocalBrowse';
function tagStarredWithServer(starred: StarredResults, serverId: string): StarredResults { function tagStarredWithServer(starred: StarredResults, serverId: string): StarredResults {
const withServer = <T extends { id: string }>(items: T[]): (T & { serverId: string })[] => const withServer = <T extends { id: string }>(items: T[]): (T & { serverId: string })[] =>
@@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineJobStore } from '../../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline/store/offlineJobStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { import {
isManualOfflinePlaylist, isManualOfflinePlaylist,
isPlaylistPinnedOffline, isPlaylistPinnedOffline,
@@ -13,8 +13,8 @@ import {
scheduleSyncPinnedAlbumsAndArtists, scheduleSyncPinnedAlbumsAndArtists,
syncAllPinnedPlaylists, syncAllPinnedPlaylists,
syncPinnedArtistIfNeeded, syncPinnedArtistIfNeeded,
} from './pinnedOfflineSync'; } from '@/features/offline/utils/pinnedOfflineSync';
import { SMART_PREFIX } from '../componentHelpers/playlistDetailHelpers'; import { SMART_PREFIX } from '@/utils/componentHelpers/playlistDetailHelpers';
const getPlaylistMock = vi.fn(); const getPlaylistMock = vi.fn();
const getAlbumForServerMock = vi.fn(); const getAlbumForServerMock = vi.fn();
@@ -24,31 +24,31 @@ const isReachableMock = vi.fn(() => true);
const enqueueMock = vi.fn((_task: unknown) => true); const enqueueMock = vi.fn((_task: unknown) => true);
const invokeMock = vi.fn(async (_cmd: string, _args?: unknown) => ({})); const invokeMock = vi.fn(async (_cmd: string, _args?: unknown) => ({}));
vi.mock('../network/activeServerReachability', () => ({ vi.mock('@/utils/network/activeServerReachability', () => ({
isActiveServerReachable: () => isReachableMock(), isActiveServerReachable: () => isReachableMock(),
onActiveServerBecameReachable: () => () => {}, onActiveServerBecameReachable: () => () => {},
})); }));
vi.mock('../../api/subsonicPlaylists', () => ({ vi.mock('@/api/subsonicPlaylists', () => ({
getPlaylistForServer: (serverId: string, id: string) => getPlaylistMock(serverId, id), getPlaylistForServer: (serverId: string, id: string) => getPlaylistMock(serverId, id),
})); }));
vi.mock('../../api/subsonicLibrary', () => ({ vi.mock('@/api/subsonicLibrary', () => ({
getAlbumForServer: (serverId: string, id: string) => getAlbumForServerMock(serverId, id), getAlbumForServer: (serverId: string, id: string) => getAlbumForServerMock(serverId, id),
filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs), filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs),
})); }));
vi.mock('../../api/subsonicArtists', () => ({ vi.mock('@/api/subsonicArtists', () => ({
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId), getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
})); }));
vi.mock('../../api/library', () => ({ vi.mock('@/api/library', () => ({
libraryGetTracksByAlbum: vi.fn(async () => []), libraryGetTracksByAlbum: vi.fn(async () => []),
subscribeLibrarySyncIdle: vi.fn(async () => () => {}), subscribeLibrarySyncIdle: vi.fn(async () => () => {}),
})); }));
vi.mock('./offlinePinQueue', async (importOriginal) => { vi.mock('@/features/offline/utils/offlinePinQueue', async (importOriginal) => {
const actual = await importOriginal<typeof import('./offlinePinQueue')>(); const actual = await importOriginal<typeof import('@/features/offline/utils/offlinePinQueue')>();
return { return {
...actual, ...actual,
enqueueOfflinePin: (task: unknown) => enqueueMock(task), enqueueOfflinePin: (task: unknown) => enqueueMock(task),
@@ -1,24 +1,24 @@
import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '../../api/library'; import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/api/library';
import { getAlbumForServer, filterSongsToServerLibrary } from '../../api/subsonicLibrary'; import { getAlbumForServer, filterSongsToServerLibrary } from '@/api/subsonicLibrary';
import { getPlaylistForServer } from '../../api/subsonicPlaylists'; import { getPlaylistForServer } from '@/api/subsonicPlaylists';
import { getArtistForServer } from '../../api/subsonicArtists'; import { getArtistForServer } from '@/api/subsonicArtists';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import type { PinSource } from '../../store/localPlaybackStore'; import type { PinSource } from '@/store/localPlaybackStore';
import { useLocalPlaybackStore } from '../../store/localPlaybackStore'; import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { usePlaylistStore } from '../../store/playlistStore'; import { usePlaylistStore } from '@/store/playlistStore';
import { isSmartPlaylistName } from '../componentHelpers/playlistDetailHelpers'; import { isSmartPlaylistName } from '@/utils/componentHelpers/playlistDetailHelpers';
import { getMediaDir } from '../media/mediaDir'; import { getMediaDir } from '@/utils/media/mediaDir';
import { import {
isActiveServerReachable, isActiveServerReachable,
onActiveServerBecameReachable, onActiveServerBecameReachable,
} from '../network/activeServerReachability'; } from '@/utils/network/activeServerReachability';
import { resolveIndexKey, serverIndexKeyForProfile } from '../server/serverIndexKey'; import { resolveIndexKey, serverIndexKeyForProfile } from '@/utils/server/serverIndexKey';
import { resolveServerIdForIndexKey } from '../server/serverLookup'; import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { findLocalPlaybackEntry } from './offlineLibraryHelpers'; import { findLocalPlaybackEntry } from '@/features/offline/utils/offlineLibraryHelpers';
import { enqueueOfflinePin } from './offlinePinQueue'; import { enqueueOfflinePin } from '@/features/offline/utils/offlinePinQueue';
export type OfflinePinKind = PinSource['kind']; export type OfflinePinKind = PinSource['kind'];
@@ -1,29 +1,29 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { resumeIncompleteOfflinePins } from './resumeIncompleteOfflinePins'; import { resumeIncompleteOfflinePins } from '@/features/offline/utils/resumeIncompleteOfflinePins';
const isActiveServerReachableMock = vi.fn(() => true); const isActiveServerReachableMock = vi.fn(() => true);
const isOfflinePinCompleteMock = vi.fn((_albumId: string, _serverId: string) => false); const isOfflinePinCompleteMock = vi.fn((_albumId: string, _serverId: string) => false);
const resolveAlbumForServerMock = vi.fn(); const resolveAlbumForServerMock = vi.fn();
const downloadAlbumMock = vi.fn(); const downloadAlbumMock = vi.fn();
vi.mock('../network/activeServerReachability', () => ({ vi.mock('@/utils/network/activeServerReachability', () => ({
isActiveServerReachable: () => isActiveServerReachableMock(), isActiveServerReachable: () => isActiveServerReachableMock(),
onActiveServerBecameReachable: () => () => {}, onActiveServerBecameReachable: () => () => {},
})); }));
vi.mock('./offlineLibraryHelpers', () => ({ vi.mock('@/features/offline/utils/offlineLibraryHelpers', () => ({
isOfflinePinComplete: (albumId: string, serverId: string) => isOfflinePinComplete: (albumId: string, serverId: string) =>
isOfflinePinCompleteMock(albumId, serverId), isOfflinePinCompleteMock(albumId, serverId),
})); }));
vi.mock('./offlineMediaResolve', () => ({ vi.mock('@/features/offline/utils/offlineMediaResolve', () => ({
resolveAlbumForServer: (serverId: string, albumId: string) => resolveAlbumForServer: (serverId: string, albumId: string) =>
resolveAlbumForServerMock(serverId, albumId), resolveAlbumForServerMock(serverId, albumId),
})); }));
vi.mock('../../api/library', () => ({ vi.mock('@/api/library', () => ({
libraryGetTracksBatchChunked: vi.fn(async () => []), libraryGetTracksBatchChunked: vi.fn(async () => []),
LIBRARY_TRACKS_BATCH_LIMIT: 100, LIBRARY_TRACKS_BATCH_LIMIT: 100,
})); }));
@@ -1,15 +1,15 @@
import { libraryGetTracksBatchChunked } from '../../api/library'; import { libraryGetTracksBatchChunked } from '@/api/library';
import { getPlaylist } from '../../api/subsonicPlaylists'; import { getPlaylist } from '@/api/subsonicPlaylists';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { useAuthStore } from '../../store/authStore'; import { useAuthStore } from '@/store/authStore';
import type { OfflineAlbumMeta } from '../../store/offlineStore'; import type { OfflineAlbumMeta } from '@/features/offline/store/offlineStore';
import { useOfflineStore } from '../../store/offlineStore'; import { useOfflineStore } from '@/features/offline/store/offlineStore';
import { trackToSong } from '../library/advancedSearchLocal'; import { trackToSong } from '@/utils/library/advancedSearchLocal';
import { isActiveServerReachable, onActiveServerBecameReachable } from '../network/activeServerReachability'; import { isActiveServerReachable, onActiveServerBecameReachable } from '@/utils/network/activeServerReachability';
import { shouldAttemptSubsonicForServer } from '../network/subsonicNetworkGuard'; import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
import { resolveServerIdForIndexKey } from '../server/serverLookup'; import { resolveServerIdForIndexKey } from '@/utils/server/serverLookup';
import { isOfflinePinComplete } from './offlineLibraryHelpers'; import { isOfflinePinComplete } from '@/features/offline/utils/offlineLibraryHelpers';
import { resolveAlbumForServer } from './offlineMediaResolve'; import { resolveAlbumForServer } from '@/features/offline/utils/offlineMediaResolve';
const DEBOUNCE_MS = 800; const DEBOUNCE_MS = 800;
+5 -5
View File
@@ -1,8 +1,8 @@
import { useState, useRef, useEffect, useMemo } from 'react'; import { useState, useRef, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { usePlayerStore } from '@/store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { useOfflineJobStore } from '@/store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { clearOfflinePinTasks } from '@/utils/offline/offlinePinQueue'; import { clearOfflinePinTasks } from '@/features/offline';
import { useDeviceSyncJobStore } from '@/features/deviceSync'; import { useDeviceSyncJobStore } from '@/features/deviceSync';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useSidebarStore } from '@/features/sidebar/store/sidebarStore'; 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 { useSidebarNavDnd } from '@/features/sidebar/hooks/useSidebarNavDnd';
import { useSidebarLibraryDropdown } from '@/features/sidebar/hooks/useSidebarLibraryDropdown'; import { useSidebarLibraryDropdown } from '@/features/sidebar/hooks/useSidebarLibraryDropdown';
import { useSidebarScrollVisible } from '@/features/sidebar/hooks/useSidebarScrollVisible'; import { useSidebarScrollVisible } from '@/features/sidebar/hooks/useSidebarScrollVisible';
import { isOfflineSidebarNavAllowed } from '@/utils/offline/offlineNavPolicy'; import { isOfflineSidebarNavAllowed } from '@/features/offline';
import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineBrowseNavFlags } from '@/utils/offline/offlineBrowseContext'; import { offlineBrowseNavFlags } from '@/features/offline';
import { useSidebarPerfProbe } from '@/features/sidebar/hooks/useSidebarPerfProbe'; import { useSidebarPerfProbe } from '@/features/sidebar/hooks/useSidebarPerfProbe';
import SidebarPerfProbeModal from '@/features/sidebar/components/SidebarPerfProbeModal'; import SidebarPerfProbeModal from '@/features/sidebar/components/SidebarPerfProbeModal';
import SidebarNavBody from '@/features/sidebar/components/SidebarNavBody'; import SidebarNavBody from '@/features/sidebar/components/SidebarNavBody';
@@ -1,6 +1,6 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useOfflineBrowseContext } from '@/hooks/useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled'; import { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled';
export default function StatisticsTabBar() { export default function StatisticsTabBar() {
+1 -1
View File
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { useAuthStore } from '@/store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { getMusicNetworkRuntime, type RecentTrack, type StatsPeriod, type TopItem } from '@/music-network'; 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 { usePlayerStatsRecordingEnabled } from '@/features/stats/hooks/usePlayerStatsRecordingEnabled';
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel'; import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
+3 -3
View File
@@ -30,13 +30,13 @@ import {
ALBUM_YEAR_FILTER_DEBOUNCE_MS, ALBUM_YEAR_FILTER_DEBOUNCE_MS,
resolveAlbumYearBounds, resolveAlbumYearBounds,
} from '../utils/library/albumYearFilter'; } from '../utils/library/albumYearFilter';
import { loadOfflineAlbumBrowseInitial } from '../utils/offline/offlineAlbumBrowseCatalog'; import { loadOfflineAlbumBrowseInitial } from '@/features/offline';
import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; import { useOfflineBrowseReloadToken } from '@/features/offline';
import { import {
fetchAlbumBrowseCatalogChunk, fetchAlbumBrowseCatalogChunk,
mergeAlbumCatalogChunk, mergeAlbumCatalogChunk,
} from '../utils/library/albumBrowseCatalogChunk'; } from '../utils/library/albumBrowseCatalogChunk';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { useClientSliceInfiniteScroll } from './useClientSliceInfiniteScroll'; import { useClientSliceInfiniteScroll } from './useClientSliceInfiniteScroll';
import { useDebouncedValue } from './useDebouncedValue'; import { useDebouncedValue } from './useDebouncedValue';
import { useInpageScrollSentinel } from './useInpageScrollSentinel'; import { useInpageScrollSentinel } from './useInpageScrollSentinel';
+4 -4
View File
@@ -5,17 +5,17 @@ import { useAuthStore } from '../store/authStore';
import { import {
loadAlbumFromLibraryIndex, loadAlbumFromLibraryIndex,
loadArtistFromLibraryIndex, loadArtistFromLibraryIndex,
} from '../utils/offline/offlineLibraryIndexLoad'; } from '@/features/offline';
import { import {
resolveAlbum, resolveAlbum,
resolveArtist, resolveArtist,
type ResolvedAlbum, type ResolvedAlbum,
} from '../utils/offline/offlineMediaResolve'; } from '@/features/offline';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { import {
loadArtistFromLocalPlayback, loadArtistFromLocalPlayback,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from '../utils/offline/offlineLocalBrowse'; } from '@/features/offline';
import { readDetailServerId } from '../utils/navigation/detailServerScope'; import { readDetailServerId } from '../utils/navigation/detailServerScope';
import { libraryIsReady } from '../utils/library/libraryReady'; import { libraryIsReady } from '../utils/library/libraryReady';
import { import {
+1 -1
View File
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { useOfflineJobStore } from '../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { useAlbumOfflineState } from './useAlbumOfflineState'; import { useAlbumOfflineState } from './useAlbumOfflineState';
describe('useAlbumOfflineState', () => { describe('useAlbumOfflineState', () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { useOfflineJobStore } from '../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; import { isOfflinePinComplete } from '@/features/offline';
export type AlbumOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached'; export type AlbumOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached';
+3 -3
View File
@@ -7,9 +7,9 @@ import type {
} from '../api/subsonicTypes'; } from '../api/subsonicTypes';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { useConnectionStatus } from './useConnectionStatus'; import { useConnectionStatus } from './useConnectionStatus';
import { loadArtistFromLibraryIndex } from '../utils/offline/offlineLibraryIndexLoad'; import { loadArtistFromLibraryIndex } from '@/features/offline';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '../utils/offline/offlineLocalBrowse'; import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '@/features/offline';
import { readDetailServerId } from '../utils/navigation/detailServerScope'; import { readDetailServerId } from '../utils/navigation/detailServerScope';
import { runLocalArtistLosslessBrowse } from '../utils/library/browseTextSearch'; import { runLocalArtistLosslessBrowse } from '../utils/library/browseTextSearch';
import { isLosslessSuffix } from '../utils/library/losslessFormats'; import { isLosslessSuffix } from '../utils/library/losslessFormats';
+1 -1
View File
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it } from 'vitest'; import { beforeEach, describe, expect, it } from 'vitest';
import { renderHook } from '@testing-library/react'; import { renderHook } from '@testing-library/react';
import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { useOfflineJobStore } from '../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { useArtistOfflineState } from './useArtistOfflineState'; import { useArtistOfflineState } from './useArtistOfflineState';
describe('useArtistOfflineState', () => { describe('useArtistOfflineState', () => {
+2 -2
View File
@@ -1,6 +1,6 @@
import { useLocalPlaybackStore } from '../store/localPlaybackStore'; import { useLocalPlaybackStore } from '../store/localPlaybackStore';
import { useOfflineJobStore } from '../store/offlineJobStore'; import { useOfflineJobStore } from '@/features/offline';
import { isOfflinePinComplete } from '../utils/offline/offlineLibraryHelpers'; import { isOfflinePinComplete } from '@/features/offline';
export type ArtistOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached'; export type ArtistOfflineStatus = 'none' | 'queued' | 'downloading' | 'cached';
+3 -3
View File
@@ -6,13 +6,13 @@ import {
fetchLocalArtistCatalogChunk, fetchLocalArtistCatalogChunk,
fetchNetworkStarredArtists, fetchNetworkStarredArtists,
} from '../utils/library/browseTextSearch'; } from '../utils/library/browseTextSearch';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { useOfflineBrowseReloadToken } from './useOfflineBrowseReloadToken'; import { useOfflineBrowseReloadToken } from '@/features/offline';
import { import {
fetchOfflineLocalArtistCatalogChunk, fetchOfflineLocalArtistCatalogChunk,
fetchOfflineLocalStarredArtists, fetchOfflineLocalStarredArtists,
offlineLocalBrowseEnabled, offlineLocalBrowseEnabled,
} from '../utils/offline/offlineLocalBrowse'; } from '@/features/offline';
/** Local-index artist catalog buffer grows by this many rows per background SQL chunk. */ /** Local-index artist catalog buffer grows by this many rows per background SQL chunk. */
export const ARTIST_CATALOG_CHUNK_SIZE = 200; export const ARTIST_CATALOG_CHUNK_SIZE = 200;
+2 -2
View File
@@ -8,8 +8,8 @@ import {
runLocalBrowseAlbums, runLocalBrowseAlbums,
runNetworkBrowseAlbums, runNetworkBrowseAlbums,
} from '../utils/library/browseTextSearch'; } from '../utils/library/browseTextSearch';
import { useOfflineBrowseContext } from './useOfflineBrowseContext'; import { useOfflineBrowseContext } from '@/features/offline';
import { offlineLocalBrowseEnabled, searchOfflineLocalAlbums } from '../utils/offline/offlineLocalBrowse'; import { offlineLocalBrowseEnabled, searchOfflineLocalAlbums } from '@/features/offline';
/** /**
* Debounced album title search with local-vs-network race when the * Debounced album title search with local-vs-network race when the

Some files were not shown because too many files have changed in this diff Show More