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

This commit is contained in:
Psychotoxical
2026-06-30 00:56:08 +02:00
parent 70c145db06
commit d6dbb615fd
149 changed files with 521 additions and 466 deletions
+1 -1
View File
@@ -57,7 +57,7 @@ export async function updatePlaylist(id: string, songIds: string[], prevCount =
songIndexToRemove: Array.from({ length: prevCount }, (_, i) => i),
});
}
void import('../utils/offline/pinnedOfflineSync')
void import('@/features/offline')
.then(m => m.schedulePinnedPlaylistSync(id))
.catch(() => {});
}
+2 -2
View File
@@ -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(() => {});
}
+1 -1
View File
@@ -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'));
+6 -6
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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',
+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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';
+3 -3
View File
@@ -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 -1
View File
@@ -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;
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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;
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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';
@@ -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();
@@ -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;
@@ -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';
@@ -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 {
@@ -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,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() {
+55
View File
@@ -0,0 +1,55 @@
/**
* Offline feature offline mode + the downloaded/pinned-library machinery:
* the offline/job/zip-download/download-modal stores, the offline-browse and
* media-resolution utils, pin-queue + sync engines, the offline overlays/banner,
* and the OfflineLibrary page (lazy via the deep path `pages/OfflineLibrary`, so
* it is not re-exported here). The favorites-offline integration layer
* (`favoritesOffline*`, `favoritesOfflineSyncStore`) lives here too the
* offline core owns it; the favorites feature consumes it through this barrel.
*
* Stays OUT (consumed by, not owned by, offline): the per-entity offline-status
* hooks `useAlbumOfflineState` / `useArtistOfflineState` (album/artist),
* `runPlaylistZipDownload` (playlist), `ensureQueueServerPinned` (playback), and
* the `localPlayback*` byte-store substrate (decided with playback, which moves
* last). Playback-core (`playTrackAction`) and the server key-remigration infra
* consume this barrel the correct featurefeature edge, realized early.
*/
export * from './hooks/useOfflineAutoNav';
export * from './hooks/useOfflineBrowseContext';
export * from './hooks/useOfflineBrowseReloadToken';
export * from './hooks/useOfflineLibraryFilterSuspend';
export * from './hooks/useZipDownloadBridge';
export * from './store/devOfflineBrowseStore';
export * from './store/downloadModalStore';
export * from './store/favoritesOfflineSyncStore';
export * from './store/offlineJobStore';
export * from './store/offlineStore';
export * from './store/zipDownloadStore';
export * from './utils/favoritesOfflineBrowse';
export * from './utils/favoritesOfflineConstants';
export * from './utils/favoritesOfflineSync';
export * from './utils/legacyOfflineFileMigration';
export * from './utils/libraryTierReconcile';
export * from './utils/offlineActionPolicy';
export * from './utils/offlineAlbumBrowseCatalog';
export * from './utils/offlineBrowseContext';
export * from './utils/offlineBrowseMode';
export * from './utils/offlineBrowseRouting';
export * from './utils/offlineLibraryFilterSuspend';
export * from './utils/offlineLibraryHelpers';
export * from './utils/offlineLibraryIndexLoad';
export * from './utils/offlineLocalBrowse';
export * from './utils/offlineMediaResolve';
export * from './utils/offlineNavPolicy';
export * from './utils/offlinePinQueue';
export * from './utils/offlinePlaylistBrowse';
export * from './utils/offlineStarredLoad';
export * from './utils/pinnedOfflineSync';
export * from './utils/resumeIncompleteOfflinePins';
// `OfflinePinKind` (= PinSource['kind']) is declared in both pin modules; the
// explicit re-export resolves the `export *` ambiguity (TS2308).
export type { OfflinePinKind } from './utils/offlinePinQueue';
export { OfflineLibraryDiskStat } from './components/OfflineLibraryDiskStat';
export { default as DownloadFolderModal } from './components/DownloadFolderModal';
export { default as OfflineBanner } from './components/OfflineBanner';
export { default as ZipDownloadOverlay } from './components/ZipDownloadOverlay';
@@ -3,16 +3,16 @@ import { useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { 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,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);
@@ -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),
@@ -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,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', () => {
@@ -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: [] })),
}));
@@ -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;
@@ -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 {
@@ -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;
@@ -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,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', () => {
@@ -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[];
@@ -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(() => {
@@ -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;
@@ -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(() => {
@@ -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 {
@@ -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,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(
@@ -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,4 +1,4 @@
import { useAuthStore } from '../../store/authStore';
import { useAuthStore } from '@/store/authStore';
let savedFilterByServer: Record<string, 'all' | string> | null = null;
@@ -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,
@@ -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,
@@ -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,
@@ -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: [] })),
@@ -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));
@@ -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),
}));
@@ -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[] };
@@ -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(() => {
@@ -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'];
@@ -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(),
}));
@@ -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()
@@ -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 })[] =>
@@ -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),
@@ -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'];
@@ -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,
}));
@@ -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;
+5 -5
View File
@@ -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() {
+1 -1
View File
@@ -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';
+3 -3
View File
@@ -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';
+4 -4
View File
@@ -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 -1
View File
@@ -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', () => {
+2 -2
View File
@@ -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';
+3 -3
View File
@@ -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 -1
View File
@@ -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', () => {
+2 -2
View File
@@ -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';
+3 -3
View File
@@ -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;
+2 -2
View File
@@ -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