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