mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(artist): co-locate artist feature into features/artist
This commit is contained in:
@@ -31,7 +31,7 @@ import { getAlbumInfo2 } from './subsonicAlbumInfo';
|
|||||||
import { getStarred } from './subsonicStarRating';
|
import { getStarred } from './subsonicStarRating';
|
||||||
import { search } from './subsonicSearch';
|
import { search } from './subsonicSearch';
|
||||||
import { getAlbum, getMusicDirectory, getMusicFolders, getMusicIndexes, getRandomSongs, getSong } from './subsonicLibrary';
|
import { getAlbum, getMusicDirectory, getMusicFolders, getMusicIndexes, getRandomSongs, getSong } from './subsonicLibrary';
|
||||||
import { getArtists, getTopSongs } from './subsonicArtists';
|
import { getArtists, getTopSongs } from '@/features/artist';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { resetAuthStore } from '@/test/helpers/storeReset';
|
import { resetAuthStore } from '@/test/helpers/storeReset';
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('./subsonicArtists', () => ({ getArtist: vi.fn() }));
|
vi.mock('@/features/artist', () => ({ getArtist: vi.fn() }));
|
||||||
vi.mock('./subsonicLibrary', () => ({ getAlbum: vi.fn() }));
|
vi.mock('./subsonicLibrary', () => ({ getAlbum: vi.fn() }));
|
||||||
vi.mock('../utils/network/subsonicNetworkGuard', () => ({
|
vi.mock('../utils/network/subsonicNetworkGuard', () => ({
|
||||||
shouldAttemptSubsonicForActiveServer: vi.fn(() => true),
|
shouldAttemptSubsonicForActiveServer: vi.fn(() => true),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getArtist } from './subsonicArtists';
|
import { getArtist } from '@/features/artist';
|
||||||
import { invalidateEntityUserRatingCaches, prefetchArtistUserRatings } from './subsonicRatings';
|
import { invalidateEntityUserRatingCaches, prefetchArtistUserRatings } from './subsonicRatings';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getArtist } from './subsonicArtists';
|
import { getArtist } from '@/features/artist';
|
||||||
import { getAlbum } from './subsonicLibrary';
|
import { getAlbum } from './subsonicLibrary';
|
||||||
import { shouldAttemptSubsonicForActiveServer } from '../utils/network/subsonicNetworkGuard';
|
import { shouldAttemptSubsonicForActiveServer } from '../utils/network/subsonicNetworkGuard';
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { resolveStartRoute } from '@/features/sidebar';
|
|||||||
// the entry chunk; each page is fetched when its route is first visited.
|
// the entry chunk; each page is fetched when its route is first visited.
|
||||||
const Home = lazy(() => import('../pages/Home'));
|
const Home = lazy(() => import('../pages/Home'));
|
||||||
const Albums = lazy(() => import('../pages/Albums'));
|
const Albums = lazy(() => import('../pages/Albums'));
|
||||||
const Artists = lazy(() => import('../pages/Artists'));
|
const Artists = lazy(() => import('@/features/artist/pages/Artists'));
|
||||||
const ArtistDetail = lazy(() => import('../pages/ArtistDetail'));
|
const ArtistDetail = lazy(() => import('@/features/artist/pages/ArtistDetail'));
|
||||||
const Composers = lazy(() => import('../pages/Composers'));
|
const Composers = lazy(() => import('../pages/Composers'));
|
||||||
const ComposerDetail = lazy(() => import('../pages/ComposerDetail'));
|
const ComposerDetail = lazy(() => import('../pages/ComposerDetail'));
|
||||||
const NewReleases = lazy(() => import('../pages/NewReleases'));
|
const NewReleases = lazy(() => import('../pages/NewReleases'));
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { CoverPrefetchPriority } from '../cover/types';
|
|||||||
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
|
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
|
||||||
import { resolveCoverDisplayTier } from '../cover/tiers';
|
import { resolveCoverDisplayTier } from '../cover/tiers';
|
||||||
import { acquireUrl } from '../utils/imageCache/urlPool';
|
import { acquireUrl } from '../utils/imageCache/urlPool';
|
||||||
import { OpenArtistRefInline } from './OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
|
import { fetchAlbumTracks, playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
|
||||||
import { useLongPressAction } from '../hooks/useLongPressAction';
|
import { useLongPressAction } from '../hooks/useLongPressAction';
|
||||||
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
|
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ vi.mock('../hooks/useAlbumDetailBack', () => ({ useAlbumDetailBack: () => vi.fn(
|
|||||||
vi.mock('../hooks/useIsMobile', () => ({ useIsMobile: () => false }));
|
vi.mock('../hooks/useIsMobile', () => ({ useIsMobile: () => false }));
|
||||||
vi.mock('../store/themeStore', () => ({ useThemeStore: () => false }));
|
vi.mock('../store/themeStore', () => ({ useThemeStore: () => false }));
|
||||||
vi.mock('./StarRating', () => ({ default: () => null }));
|
vi.mock('./StarRating', () => ({ default: () => null }));
|
||||||
vi.mock('./OpenArtistRefInline', () => ({ OpenArtistRefInline: () => null }));
|
vi.mock('@/features/artist', () => ({ OpenArtistRefInline: () => null }));
|
||||||
vi.mock('../cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
|
vi.mock('../cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
|
||||||
|
|
||||||
function baseProps() {
|
function baseProps() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { isAlbumRecentlyAdded } from '../utils/albumRecency';
|
|||||||
import { formatLongDuration } from '../utils/format/formatDuration';
|
import { formatLongDuration } from '../utils/format/formatDuration';
|
||||||
import { formatMb } from '../utils/format/formatBytes';
|
import { formatMb } from '../utils/format/formatBytes';
|
||||||
import { sanitizeHtml } from '../utils/sanitizeHtml';
|
import { sanitizeHtml } from '../utils/sanitizeHtml';
|
||||||
import { OpenArtistRefInline } from './OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { tooltipAttrs } from '@/ui/tooltipAttrs';
|
import { tooltipAttrs } from '@/ui/tooltipAttrs';
|
||||||
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
||||||
import { deriveAlbumGenreTags } from '../utils/library/genreTags';
|
import { deriveAlbumGenreTags } from '../utils/library/genreTags';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
|
import { getArtist, getArtistInfo } from '@/features/artist';
|
||||||
import { filterAlbumsToActiveLibrary } from '../api/subsonicLibrary';
|
import { filterAlbumsToActiveLibrary } from '../api/subsonicLibrary';
|
||||||
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
|
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
|
||||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
import { useCachedUrl } from '@/ui/CachedImage';
|
import { useCachedUrl } from '@/ui/CachedImage';
|
||||||
import { OpenArtistRefInline } from './OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { formatTrackTime } from '../utils/format/formatDuration';
|
import { formatTrackTime } from '../utils/format/formatDuration';
|
||||||
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
import { resolveQueueTrack } from '../utils/library/queueTrackView';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import PlaybackDelayModal from './PlaybackDelayModal';
|
|||||||
import { usePlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat';
|
import { usePlaybackScheduleRemaining } from '../utils/format/playbackScheduleFormat';
|
||||||
import { usePreviewStore } from '../store/previewStore';
|
import { usePreviewStore } from '../store/previewStore';
|
||||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||||
import { coerceOpenArtistRefs } from '../utils/openArtistRefs';
|
import { coerceOpenArtistRefs } from '@/features/artist';
|
||||||
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
|
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
|
||||||
import { PlayerTrackInfo } from './playerBar/PlayerTrackInfo';
|
import { PlayerTrackInfo } from './playerBar/PlayerTrackInfo';
|
||||||
import { PlayerTransportControls } from './playerBar/PlayerTransportControls';
|
import { PlayerTransportControls } from './playerBar/PlayerTransportControls';
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import type { SubsonicSong } from '../api/subsonicTypes';
|
|||||||
|
|
||||||
const navigateToArtist = vi.fn();
|
const navigateToArtist = vi.fn();
|
||||||
|
|
||||||
vi.mock('../hooks/useNavigateToArtist', () => ({
|
// Only navigate-to-artist is stubbed; mock that submodule directly so the
|
||||||
|
// barrel re-exports the stub while coerceOpenArtistRefs (used by
|
||||||
|
// trackArtistRefs, a different submodule) stays real.
|
||||||
|
vi.mock('@/features/artist/hooks/useNavigateToArtist', () => ({
|
||||||
useNavigateToArtist: () => navigateToArtist,
|
useNavigateToArtist: () => navigateToArtist,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { enqueueAndPlay } from '../utils/playback/playSong';
|
|||||||
import { useDragDrop } from '../contexts/DragDropContext';
|
import { useDragDrop } from '../contexts/DragDropContext';
|
||||||
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
||||||
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
|
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
|
||||||
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist';
|
||||||
import { OpenArtistRefInline } from './OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
|
import { resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs';
|
||||||
|
|
||||||
interface SongCardProps {
|
interface SongCardProps {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { SubsonicSong } from '../api/subsonicTypes';
|
|||||||
import { songToTrack } from '../utils/playback/songToTrack';
|
import { songToTrack } from '../utils/playback/songToTrack';
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
|
import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum';
|
||||||
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist';
|
||||||
import { Play, ListPlus } from 'lucide-react';
|
import { Play, ListPlus } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '../store/playerStore';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Play, ListPlus, Radio, Heart, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react';
|
import { Play, ListPlus, Radio, Heart, ChevronRight, ChevronsRight, User, Disc3, ListMusic, Info, Sparkles, Star, Trash2, HeartCrack, Share2, Orbit as OrbitIcon } from 'lucide-react';
|
||||||
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
|
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
|
||||||
import { useNavigateToArtist } from '../../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist';
|
||||||
import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
|
import { resolveAlbum, resolveMediaServerId, resolvePlaylist } from '@/features/offline';
|
||||||
import { queueSongStar } from '../../store/pendingStarSync';
|
import { queueSongStar } from '../../store/pendingStarSync';
|
||||||
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network';
|
import { getMusicNetworkRuntime, useEnrichmentPrimary } from '../../music-network';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { albumCoverRef } from '../../cover/ref';
|
|||||||
import { useAlbumCoverRef } from '../../cover/useLibraryCoverRef';
|
import { useAlbumCoverRef } from '../../cover/useLibraryCoverRef';
|
||||||
import { usePlaybackTrackCoverRef } from '../../cover/useLibraryCoverRef';
|
import { usePlaybackTrackCoverRef } from '../../cover/useLibraryCoverRef';
|
||||||
import MarqueeText from '../MarqueeText';
|
import MarqueeText from '../MarqueeText';
|
||||||
import { OpenArtistRefInline } from '../OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import StarRating from '../StarRating';
|
import StarRating from '../StarRating';
|
||||||
import { PlaybackBufferingOverlay } from '../playback/PlaybackBufferingOverlay';
|
import { PlaybackBufferingOverlay } from '../playback/PlaybackBufferingOverlay';
|
||||||
import { usePlayerStore } from '../../store/playerStore';
|
import { usePlayerStore } from '../../store/playerStore';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { useQueueTrackEnrichment } from '../../hooks/useQueueTrackEnrichment';
|
|||||||
import { QueueLufsTargetMenu } from './QueueLufsTargetMenu';
|
import { QueueLufsTargetMenu } from './QueueLufsTargetMenu';
|
||||||
import { PlaybackBufferingOverlay } from '../playback/PlaybackBufferingOverlay';
|
import { PlaybackBufferingOverlay } from '../playback/PlaybackBufferingOverlay';
|
||||||
import { CoverArtImage } from '../../cover/CoverArtImage';
|
import { CoverArtImage } from '../../cover/CoverArtImage';
|
||||||
import { OpenArtistRefInline } from '../OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { usePlaybackTrackCoverRef } from '../../cover/useLibraryCoverRef';
|
import { usePlaybackTrackCoverRef } from '../../cover/useLibraryCoverRef';
|
||||||
import { usePlayerStore } from '../../store/playerStore';
|
import { usePlayerStore } from '../../store/playerStore';
|
||||||
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
|
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import { playSongNow } from '../../utils/playback/playSong';
|
|||||||
import { ndListSongs, ndInvalidateSongsCache } from '../../api/navidromeBrowse';
|
import { ndListSongs, ndInvalidateSongsCache } from '../../api/navidromeBrowse';
|
||||||
import { usePerfProbeFlags } from '../../utils/perf/perfFlags';
|
import { usePerfProbeFlags } from '../../utils/perf/perfFlags';
|
||||||
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
|
import { useNavigateToAlbum } from '../../hooks/useNavigateToAlbum';
|
||||||
import { useNavigateToArtist } from '../../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist';
|
||||||
import { OpenArtistRefInline } from '../OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
|
import { resolveTrackArtistRefs } from '../../utils/playback/trackArtistRefs';
|
||||||
|
|
||||||
const RANDOM_RAIL_SIZE = 18;
|
const RANDOM_RAIL_SIZE = 18;
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('./subsonicClient', () => ({
|
vi.mock('@/api/subsonicClient', () => ({
|
||||||
api: vi.fn(),
|
api: vi.fn(),
|
||||||
apiForServer: vi.fn(),
|
apiForServer: vi.fn(),
|
||||||
libraryFilterParams: () => ({}),
|
libraryFilterParams: () => ({}),
|
||||||
libraryFilterParamsForServer: () => ({}),
|
libraryFilterParamsForServer: () => ({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('./subsonicLibrary', () => ({
|
vi.mock('@/api/subsonicLibrary', () => ({
|
||||||
filterSongsToActiveLibrary: async (songs: unknown[]) => songs,
|
filterSongsToActiveLibrary: async (songs: unknown[]) => songs,
|
||||||
filterSongsToServerLibrary: async (songs: unknown[]) => songs,
|
filterSongsToServerLibrary: async (songs: unknown[]) => songs,
|
||||||
similarSongsRequestCount: (count: number) => count,
|
similarSongsRequestCount: (count: number) => count,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { api } from './subsonicClient';
|
import { api } from '@/api/subsonicClient';
|
||||||
import { fetchSimilarTracksRouted } from './subsonicArtists';
|
import { fetchSimilarTracksRouted } from '@/features/artist/api/subsonicArtists';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
|
||||||
const SID = 'srv-router';
|
const SID = 'srv-router';
|
||||||
const apiMock = vi.mocked(api);
|
const apiMock = vi.mocked(api);
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { api, apiForServer, libraryFilterParams, libraryFilterParamsForServer } from './subsonicClient';
|
import { api, apiForServer, libraryFilterParams, libraryFilterParamsForServer } from '@/api/subsonicClient';
|
||||||
import { filterSongsToServerLibrary } from './subsonicLibrary';
|
import { filterSongsToServerLibrary } from '@/api/subsonicLibrary';
|
||||||
import { filterSongsToActiveLibrary, similarSongsRequestCount } from './subsonicLibrary';
|
import { filterSongsToActiveLibrary, similarSongsRequestCount } from '@/api/subsonicLibrary';
|
||||||
import {
|
import {
|
||||||
FEATURE_AUDIOMUSE_SIMILAR_TRACKS,
|
FEATURE_AUDIOMUSE_SIMILAR_TRACKS,
|
||||||
OP_SIMILAR_TRACKS,
|
OP_SIMILAR_TRACKS,
|
||||||
} from '../serverCapabilities/catalog';
|
} from '@/serverCapabilities/catalog';
|
||||||
import { resolveCallRoutesForServer } from '../serverCapabilities/storeView';
|
import { resolveCallRoutesForServer } from '@/serverCapabilities/storeView';
|
||||||
import type {
|
import type {
|
||||||
SubsonicAlbum,
|
SubsonicAlbum,
|
||||||
SubsonicArtist,
|
SubsonicArtist,
|
||||||
SubsonicArtistInfo,
|
SubsonicArtistInfo,
|
||||||
SubsonicSong,
|
SubsonicSong,
|
||||||
} from './subsonicTypes';
|
} from '@/api/subsonicTypes';
|
||||||
|
|
||||||
export async function getArtists(): Promise<SubsonicArtist[]> {
|
export async function getArtists(): Promise<SubsonicArtist[]> {
|
||||||
type ArtistIndexEntry = { artist?: SubsonicArtist | SubsonicArtist[] };
|
type ArtistIndexEntry = { artist?: SubsonicArtist | SubsonicArtist[] };
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { ArtistCoverArtImage } from '../../cover/ArtistCoverArtImage';
|
import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
|
||||||
import {
|
import {
|
||||||
COVER_DENSE_ARTIST_LIST_CSS_PX,
|
COVER_DENSE_ARTIST_LIST_CSS_PX,
|
||||||
COVER_DENSE_GRID_MIN_CELL_CSS_PX,
|
COVER_DENSE_GRID_MIN_CELL_CSS_PX,
|
||||||
} from '../../cover/layoutSizes';
|
} from '@/cover/layoutSizes';
|
||||||
import { ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
|
import { ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
|
||||||
import { nameColor, nameInitial } from '../../utils/componentHelpers/artistsHelpers';
|
import { nameColor, nameInitial } from '@/features/artist/utils/artistsHelpers';
|
||||||
|
|
||||||
interface AvatarProps {
|
interface AvatarProps {
|
||||||
artist: SubsonicArtist;
|
artist: SubsonicArtist;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { Users } from 'lucide-react';
|
import { Users } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CoverArtImage } from '../cover/CoverArtImage';
|
import { CoverArtImage } from '@/cover/CoverArtImage';
|
||||||
import { useArtistCoverRef } from '../cover/useLibraryCoverRef';
|
import { useArtistCoverRef } from '@/cover/useLibraryCoverRef';
|
||||||
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
|
import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '@/cover/layoutSizes';
|
||||||
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist/hooks/useNavigateToArtist';
|
||||||
import { coverServerScopeForServerId } from '../cover/serverScope';
|
import { coverServerScopeForServerId } from '@/cover/serverScope';
|
||||||
import { appendServerQuery } from '../utils/navigation/detailServerScope';
|
import { appendServerQuery } from '@/utils/navigation/detailServerScope';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
artist: SubsonicArtist;
|
artist: SubsonicArtist;
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import React, { useMemo, useRef, useState } from 'react';
|
import React, { useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useAlbumDetailBack } from '../../hooks/useAlbumDetailBack';
|
import { useAlbumDetailBack } from '@/hooks/useAlbumDetailBack';
|
||||||
import {
|
import {
|
||||||
ArrowLeft, Camera, Check, HardDriveDownload, Heart,
|
ArrowLeft, Camera, Check, HardDriveDownload, Heart,
|
||||||
Loader2, Play, Radio, Share2, Shuffle, Users,
|
Loader2, Play, Radio, Share2, Shuffle, Users,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '@/api/subsonicTypes';
|
||||||
import { useOfflineStore } from '@/features/offline';
|
import { useOfflineStore } from '@/features/offline';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { useThemeStore } from '../../store/themeStore';
|
import { useThemeStore } from '@/store/themeStore';
|
||||||
import { useArtistOfflineState } from '../../hooks/useArtistOfflineState';
|
import { useArtistOfflineState } from '@/features/artist/hooks/useArtistOfflineState';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||||
import { ArtistHeroCover } from '../../cover/artistHero';
|
import { ArtistHeroCover } from '@/cover/artistHero';
|
||||||
import { useArtistBanner, useArtistFanart } from '../../cover/useArtistFanart';
|
import { useArtistBanner, useArtistFanart } from '@/cover/useArtistFanart';
|
||||||
import { backdropFromConfig } from '../../cover/artistBackdrop';
|
import { backdropFromConfig } from '@/cover/artistBackdrop';
|
||||||
import { usePlaybackCoverArt } from '../../cover/usePlaybackCoverArt';
|
import { usePlaybackCoverArt } from '@/cover/usePlaybackCoverArt';
|
||||||
import { useCachedUrl } from '@/ui/CachedImage';
|
import { useCachedUrl } from '@/ui/CachedImage';
|
||||||
import { useCoverLightboxSrc } from '../../cover/lightbox';
|
import { useCoverLightboxSrc } from '@/cover/lightbox';
|
||||||
import type { CoverArtRef } from '../../cover/types';
|
import type { CoverArtRef } from '@/cover/types';
|
||||||
import LastfmIcon from '../LastfmIcon';
|
import LastfmIcon from '@/components/LastfmIcon';
|
||||||
import WikipediaIcon from '../WikipediaIcon';
|
import WikipediaIcon from '@/components/WikipediaIcon';
|
||||||
import StarRating from '../StarRating';
|
import StarRating from '@/components/StarRating';
|
||||||
import { tooltipAttrs } from '@/ui/tooltipAttrs';
|
import { tooltipAttrs } from '@/ui/tooltipAttrs';
|
||||||
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
import { offlineActionPolicy, type OfflineActionPolicy } from '@/features/offline';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { Fragment } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { useIsMobile } from '../../hooks/useIsMobile';
|
import { useIsMobile } from '@/hooks/useIsMobile';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
marginTop: string;
|
marginTop: string;
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AudioLines, ChevronRight, Play, Square } from 'lucide-react';
|
import { AudioLines, ChevronRight, Play, Square } from 'lucide-react';
|
||||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import { usePlayerStore } from '../../store/playerStore';
|
import { usePlayerStore } from '@/store/playerStore';
|
||||||
import { previewInputFromSong, usePreviewStore } from '../../store/previewStore';
|
import { previewInputFromSong, usePreviewStore } from '@/store/previewStore';
|
||||||
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
import { useOrbitSongRowBehavior } from '@/features/orbit';
|
||||||
import { songToTrack } from '../../utils/playback/songToTrack';
|
import { songToTrack } from '@/utils/playback/songToTrack';
|
||||||
import { formatTrackTime } from '../../utils/format/formatDuration';
|
import { formatTrackTime } from '@/utils/format/formatDuration';
|
||||||
import ArtistTopTrackCover from './ArtistTopTrackCover';
|
import ArtistTopTrackCover from '@/features/artist/components/ArtistTopTrackCover';
|
||||||
import { topSongAlbumForCover } from './topSongAlbumForCover';
|
import { topSongAlbumForCover } from '@/features/artist/components/topSongAlbumForCover';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
topSongs: SubsonicSong[];
|
topSongs: SubsonicSong[];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import React, { useRef, useState, useEffect, useLayoutEffect } from 'react';
|
import React, { useRef, useState, useEffect, useLayoutEffect } from 'react';
|
||||||
import ArtistCardLocal from './ArtistCardLocal';
|
import ArtistCardLocal from '@/features/artist/components/ArtistCardLocal';
|
||||||
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, ArrowRight } from 'lucide-react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { CoverArtImage } from '../../cover/CoverArtImage';
|
import { CoverArtImage } from '@/cover/CoverArtImage';
|
||||||
import { useAlbumCoverRef } from '../../cover/useLibraryCoverRef';
|
import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef';
|
||||||
import { COVER_ARTIST_TOP_TRACK_CSS_PX } from '../../cover/layoutSizes';
|
import { COVER_ARTIST_TOP_TRACK_CSS_PX } from '@/cover/layoutSizes';
|
||||||
import type { TopSongAlbumCoverSource } from './topSongAlbumForCover';
|
import type { TopSongAlbumCoverSource } from '@/features/artist/components/topSongAlbumForCover';
|
||||||
|
|
||||||
/** 32px album thumb — same cover ref path as {@link AlbumCard} on artist pages. */
|
/** 32px album thumb — same cover ref path as {@link AlbumCard} on artist pages. */
|
||||||
export default function ArtistTopTrackCover({ album }: { album: TopSongAlbumCoverSource }) {
|
export default function ArtistTopTrackCover({ album }: { album: TopSongAlbumCoverSource }) {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import type { PlayerState } from '../../store/playerStoreTypes';
|
import type { PlayerState } from '@/store/playerStoreTypes';
|
||||||
import { ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
|
import { ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
|
||||||
import { VirtualCardGrid } from '../VirtualCardGrid';
|
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
|
||||||
import { ArtistCardAvatar } from './ArtistAvatars';
|
import { ArtistCardAvatar } from '@/features/artist/components/ArtistAvatars';
|
||||||
|
|
||||||
interface TileProps {
|
interface TileProps {
|
||||||
artist: SubsonicArtist;
|
artist: SubsonicArtist;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Virtualizer } from '@tanstack/react-virtual';
|
import type { Virtualizer } from '@tanstack/react-virtual';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import type { PlayerState } from '../../store/playerStoreTypes';
|
import type { PlayerState } from '@/store/playerStoreTypes';
|
||||||
import { OTHER_BUCKET, type ArtistListFlatRow } from '../../utils/componentHelpers/artistsHelpers';
|
import { OTHER_BUCKET, type ArtistListFlatRow } from '@/features/artist/utils/artistsHelpers';
|
||||||
import { ArtistRowAvatar } from './ArtistAvatars';
|
import { ArtistRowAvatar } from '@/features/artist/components/ArtistAvatars';
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
artist: SubsonicArtist;
|
artist: SubsonicArtist;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import type { SubsonicOpenArtistRef } from '../api/subsonicTypes';
|
import type { SubsonicOpenArtistRef } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
refs: SubsonicOpenArtistRef[];
|
refs: SubsonicOpenArtistRef[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { topSongAlbumForCover, topSongAlbumsForCoverWarm, artistDetailCoverWarmAlbums } from './topSongAlbumForCover';
|
import { topSongAlbumForCover, topSongAlbumsForCoverWarm, artistDetailCoverWarmAlbums } from '@/features/artist/components/topSongAlbumForCover';
|
||||||
|
|
||||||
describe('topSongAlbumForCover', () => {
|
describe('topSongAlbumForCover', () => {
|
||||||
it('uses the artist album row when albumId matches', () => {
|
it('uses the artist album row when albumId matches', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
export type TopSongAlbumCoverSource = Pick<SubsonicAlbum, 'id' | 'coverArt' | 'name'>;
|
export type TopSongAlbumCoverSource = Pick<SubsonicAlbum, 'id' | 'coverArt' | 'name'>;
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ import { renderHook, act, waitFor } from '@testing-library/react';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { SubsonicArtistInfo } from '../api/subsonicTypes';
|
import type { SubsonicArtistInfo } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
vi.mock('../api/subsonicArtists');
|
vi.mock('@/features/artist/api/subsonicArtists');
|
||||||
vi.mock('../api/subsonicSearch');
|
vi.mock('@/api/subsonicSearch');
|
||||||
|
|
||||||
import { getArtist, getArtistInfo, getTopSongs } from '../api/subsonicArtists';
|
import { getArtist, getArtistInfo, getTopSongs } from '@/features/artist/api/subsonicArtists';
|
||||||
import { search } from '../api/subsonicSearch';
|
import { search } from '@/api/subsonicSearch';
|
||||||
import { useArtistDetailData } from './useArtistDetailData';
|
import { useArtistDetailData } from '@/features/artist/hooks/useArtistDetailData';
|
||||||
|
|
||||||
const mockArtistInfo = vi.mocked(getArtistInfo) as unknown as {
|
const mockArtistInfo = vi.mocked(getArtistInfo) as unknown as {
|
||||||
mockImplementation: (impl: (id: string) => Promise<SubsonicArtistInfo | null>) => void;
|
mockImplementation: (impl: (id: string) => Promise<SubsonicArtistInfo | null>) => void;
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { search } from '../api/subsonicSearch';
|
import { search } from '@/api/subsonicSearch';
|
||||||
import { getArtist, getArtistForServer, getArtistInfo, getTopSongs } from '../api/subsonicArtists';
|
import { getArtist, getArtistForServer, getArtistInfo, getTopSongs } from '@/features/artist/api/subsonicArtists';
|
||||||
import type {
|
import type {
|
||||||
SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo, SubsonicSong,
|
SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo, SubsonicSong,
|
||||||
} from '../api/subsonicTypes';
|
} from '@/api/subsonicTypes';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { useConnectionStatus } from './useConnectionStatus';
|
import { useConnectionStatus } from '@/hooks/useConnectionStatus';
|
||||||
import { loadArtistFromLibraryIndex } from '@/features/offline';
|
import { loadArtistFromLibraryIndex } from '@/features/offline';
|
||||||
import { useOfflineBrowseContext } from '@/features/offline';
|
import { useOfflineBrowseContext } from '@/features/offline';
|
||||||
import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '@/features/offline';
|
import { loadArtistFromLocalPlayback, offlineLocalBrowseEnabled } from '@/features/offline';
|
||||||
import { readDetailServerId } from '../utils/navigation/detailServerScope';
|
import { readDetailServerId } from '@/utils/navigation/detailServerScope';
|
||||||
import { runLocalArtistLosslessBrowse } from '../utils/library/browseTextSearch';
|
import { runLocalArtistLosslessBrowse } from '@/utils/library/browseTextSearch';
|
||||||
import { isLosslessSuffix } from '../utils/library/losslessFormats';
|
import { isLosslessSuffix } from '@/utils/library/losslessFormats';
|
||||||
|
|
||||||
export interface UseArtistDetailDataOptions {
|
export interface UseArtistDetailDataOptions {
|
||||||
/** When true, albums and top tracks are limited to lossless containers (local index preferred). */
|
/** When true, albums and top tracks are limited to lossless containers (local index preferred). */
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { getArtistInfoForServer } from '../api/subsonicArtists';
|
import { getArtistInfoForServer } from '@/features/artist/api/subsonicArtists';
|
||||||
import type { SubsonicArtistInfo, SubsonicOpenArtistRef } from '../api/subsonicTypes';
|
import type { SubsonicArtistInfo, SubsonicOpenArtistRef } from '@/api/subsonicTypes';
|
||||||
import { makeCache } from '../utils/cache/nowPlayingCache';
|
import { makeCache } from '@/utils/cache/nowPlayingCache';
|
||||||
|
|
||||||
const artistInfoCache = makeCache<SubsonicArtistInfo | null>();
|
const artistInfoCache = makeCache<SubsonicArtistInfo | null>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { beforeEach, describe, expect, it } from 'vitest';
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { useLocalPlaybackStore } from '../store/localPlaybackStore';
|
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||||
import { useOfflineJobStore } from '@/features/offline';
|
import { useOfflineJobStore } from '@/features/offline';
|
||||||
import { useArtistOfflineState } from './useArtistOfflineState';
|
import { useArtistOfflineState } from '@/features/artist/hooks/useArtistOfflineState';
|
||||||
|
|
||||||
describe('useArtistOfflineState', () => {
|
describe('useArtistOfflineState', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useLocalPlaybackStore } from '../store/localPlaybackStore';
|
import { useLocalPlaybackStore } from '@/store/localPlaybackStore';
|
||||||
import { useOfflineJobStore } from '@/features/offline';
|
import { useOfflineJobStore } from '@/features/offline';
|
||||||
import { isOfflinePinComplete } from '@/features/offline';
|
import { isOfflinePinComplete } from '@/features/offline';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getMusicNetworkRuntime } from '../music-network';
|
import { getMusicNetworkRuntime } from '@/music-network';
|
||||||
import { search } from '../api/subsonicSearch';
|
import { search } from '@/api/subsonicSearch';
|
||||||
import type { SubsonicArtist, SubsonicArtistInfo } from '../api/subsonicTypes';
|
import type { SubsonicArtist, SubsonicArtistInfo } from '@/api/subsonicTypes';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
|
||||||
export interface ArtistSimilarArtistsResult {
|
export interface ArtistSimilarArtistsResult {
|
||||||
similarArtists: SubsonicArtist[];
|
similarArtists: SubsonicArtist[];
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { getArtists } from '../api/subsonicArtists';
|
import { getArtists } from '@/features/artist/api/subsonicArtists';
|
||||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { dedupeById } from '../utils/dedupeById';
|
import { dedupeById } from '@/utils/dedupeById';
|
||||||
import {
|
import {
|
||||||
fetchLocalArtistCatalogChunk,
|
fetchLocalArtistCatalogChunk,
|
||||||
fetchNetworkStarredArtists,
|
fetchNetworkStarredArtists,
|
||||||
} from '../utils/library/browseTextSearch';
|
} from '@/utils/library/browseTextSearch';
|
||||||
import { useOfflineBrowseContext } from '@/features/offline';
|
import { useOfflineBrowseContext } from '@/features/offline';
|
||||||
import { useOfflineBrowseReloadToken } from '@/features/offline';
|
import { useOfflineBrowseReloadToken } from '@/features/offline';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { useEffect, useRef, useState, type RefObject } from 'react';
|
import { useEffect, useRef, useState, type RefObject } from 'react';
|
||||||
import { useLocation, useNavigationType, type NavigationType } from 'react-router-dom';
|
import { useLocation, useNavigationType, type NavigationType } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import {
|
import {
|
||||||
DEFAULT_ARTIST_BROWSE_RETURN_STATE,
|
DEFAULT_ARTIST_BROWSE_RETURN_STATE,
|
||||||
type ArtistBrowseReturnState,
|
type ArtistBrowseReturnState,
|
||||||
type ArtistBrowseViewMode,
|
type ArtistBrowseViewMode,
|
||||||
isArtistsBrowsePath,
|
isArtistsBrowsePath,
|
||||||
useArtistBrowseSessionStore,
|
useArtistBrowseSessionStore,
|
||||||
} from '../store/artistBrowseSessionStore';
|
} from '@/features/artist/store/artistBrowseSessionStore';
|
||||||
import { isArtistDetailPath } from '../store/albumBrowseSessionStore';
|
import { isArtistDetailPath } from '@/store/albumBrowseSessionStore';
|
||||||
import { shouldRestoreArtistBrowseSession } from '../utils/navigation/albumDetailNavigation';
|
import { shouldRestoreArtistBrowseSession } from '@/utils/navigation/albumDetailNavigation';
|
||||||
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore';
|
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
|
||||||
|
|
||||||
export type ArtistBrowseScrollSnapshot = {
|
export type ArtistBrowseScrollSnapshot = {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useLocation, useNavigationType, type NavigationType } from 'react-route
|
|||||||
import {
|
import {
|
||||||
peekArtistBrowseScrollRestore,
|
peekArtistBrowseScrollRestore,
|
||||||
useArtistBrowseSessionStore,
|
useArtistBrowseSessionStore,
|
||||||
} from '../store/artistBrowseSessionStore';
|
} from '@/features/artist/store/artistBrowseSessionStore';
|
||||||
import { shouldRestoreArtistBrowseSession } from '../utils/navigation/albumDetailNavigation';
|
import { shouldRestoreArtistBrowseSession } from '@/utils/navigation/albumDetailNavigation';
|
||||||
|
|
||||||
type PendingScroll = {
|
type PendingScroll = {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '@/store/playerStore';
|
||||||
import { ALL_SENTINEL, artistLetterBucket, compareBuckets, type ArtistListFlatRow } from '../utils/componentHelpers/artistsHelpers';
|
import { ALL_SENTINEL, artistLetterBucket, compareBuckets, type ArtistListFlatRow } from '@/features/artist/utils/artistsHelpers';
|
||||||
|
|
||||||
interface UseArtistsFilteringArgs {
|
interface UseArtistsFilteringArgs {
|
||||||
artists: SubsonicArtist[];
|
artists: SubsonicArtist[];
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export {
|
|||||||
useClientSliceInfiniteScroll as useArtistsInfiniteScroll,
|
useClientSliceInfiniteScroll as useArtistsInfiniteScroll,
|
||||||
type UseClientSliceInfiniteScrollArgs as UseArtistsInfiniteScrollArgs,
|
type UseClientSliceInfiniteScrollArgs as UseArtistsInfiniteScrollArgs,
|
||||||
type UseClientSliceInfiniteScrollResult as UseArtistsInfiniteScrollResult,
|
type UseClientSliceInfiniteScrollResult as UseArtistsInfiniteScrollResult,
|
||||||
} from './useClientSliceInfiniteScroll';
|
} from '@/hooks/useClientSliceInfiniteScroll';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BROWSE_TEXT_DEBOUNCE_NETWORK_MS,
|
BROWSE_TEXT_DEBOUNCE_NETWORK_MS,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
runLocalBrowseArtists,
|
runLocalBrowseArtists,
|
||||||
runNetworkBrowseArtists,
|
runNetworkBrowseArtists,
|
||||||
type LibrarySearchSurface,
|
type LibrarySearchSurface,
|
||||||
} from '../utils/library/browseTextSearch';
|
} from '@/utils/library/browseTextSearch';
|
||||||
import { useOfflineBrowseContext } from '@/features/offline';
|
import { useOfflineBrowseContext } from '@/features/offline';
|
||||||
import { offlineLocalBrowseEnabled, searchOfflineLocalArtists } from '@/features/offline';
|
import { offlineLocalBrowseEnabled, searchOfflineLocalArtists } from '@/features/offline';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { navigateToArtistDetail } from '../utils/navigation/albumDetailNavigation';
|
import { navigateToArtistDetail } from '@/utils/navigation/albumDetailNavigation';
|
||||||
|
|
||||||
/** Navigate to artist detail, remembering the current page for the back button. */
|
/** Navigate to artist detail, remembering the current page for the back button. */
|
||||||
export function useNavigateToArtist() {
|
export function useNavigateToArtist() {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Artist feature — the Artists overview + ArtistDetail (both lazy via their deep
|
||||||
|
* `pages/*` paths, not re-exported), artist cards/rows/avatars, the detail hero +
|
||||||
|
* similar-artists + top-tracks UI, the artist Subsonic API, browse catalog/
|
||||||
|
* filter/scroll hooks, per-artist offline status, and the open-artist-ref nav
|
||||||
|
* cluster. `sortArtistAlbums` moved in from `utils/library/` (artist-only sort,
|
||||||
|
* not the shared index query engine).
|
||||||
|
*
|
||||||
|
* Stays OUT (other owners): `deriveAlbumHeaderArtistRefs` (album header),
|
||||||
|
* `playArtistShuffled` + `trackArtistRefs` (playback/track helpers), the
|
||||||
|
* `*ContextItems`/`*ToPlaylistSubmenu` items (the cross-cutting context-menu
|
||||||
|
* subsystem, shared with album), and `PlaylistArtistCell` (playlist).
|
||||||
|
*/
|
||||||
|
export * from './api/subsonicArtists';
|
||||||
|
export * from './hooks/useArtistDetailData';
|
||||||
|
export * from './hooks/useArtistInfoBatch';
|
||||||
|
export * from './hooks/useArtistOfflineState';
|
||||||
|
export * from './hooks/useArtistsBrowseCatalog';
|
||||||
|
export * from './hooks/useArtistsBrowseFilters';
|
||||||
|
export * from './hooks/useArtistsBrowseScrollReset';
|
||||||
|
export * from './hooks/useArtistsBrowseScrollRestore';
|
||||||
|
export * from './hooks/useArtistsFiltering';
|
||||||
|
export * from './hooks/useArtistSimilarArtists';
|
||||||
|
export * from './hooks/useArtistsInfiniteScroll';
|
||||||
|
export * from './hooks/useBrowseArtistTextSearch';
|
||||||
|
export * from './hooks/useNavigateToArtist';
|
||||||
|
export * from './store/artistAlbumYearSortStore';
|
||||||
|
export * from './store/artistBrowseSessionStore';
|
||||||
|
export * from './store/artistLayoutStore';
|
||||||
|
export * from './utils/artistsHelpers';
|
||||||
|
export * from './utils/openArtistRefs';
|
||||||
|
export * from './utils/runArtistDetailActions';
|
||||||
|
export * from './utils/runArtistDetailPlay';
|
||||||
|
export * from './utils/sortArtistAlbums';
|
||||||
|
export { OpenArtistRefInline } from './components/OpenArtistRefInline';
|
||||||
|
export { default as ArtistCardLocal } from './components/ArtistCardLocal';
|
||||||
|
export { default as ArtistDetailHero } from './components/ArtistDetailHero';
|
||||||
|
export { default as ArtistDetailSimilarArtists } from './components/ArtistDetailSimilarArtists';
|
||||||
|
export { default as ArtistDetailTopTracks } from './components/ArtistDetailTopTracks';
|
||||||
|
export { default as ArtistRow } from './components/ArtistRow';
|
||||||
|
export { default as ArtistTopTrackCover } from './components/ArtistTopTrackCover';
|
||||||
@@ -1,44 +1,44 @@
|
|||||||
import { useCoverArt } from '../cover/useCoverArt';
|
import { useCoverArt } from '@/cover/useCoverArt';
|
||||||
import { useArtistCoverRef } from '../cover/useLibraryCoverRef';
|
import { useArtistCoverRef } from '@/cover/useLibraryCoverRef';
|
||||||
import type { SubsonicArtist, SubsonicAlbum } from '../api/subsonicTypes';
|
import type { SubsonicArtist, SubsonicAlbum } from '@/api/subsonicTypes';
|
||||||
import { useEffect, useState, Fragment, useMemo } from 'react';
|
import { useEffect, useState, Fragment, useMemo } from 'react';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import AlbumCard from '../components/AlbumCard';
|
import AlbumCard from '@/components/AlbumCard';
|
||||||
import { ArrowDownUp } from 'lucide-react';
|
import { ArrowDownUp } from 'lucide-react';
|
||||||
import { open } from '@tauri-apps/plugin-shell';
|
import { open } from '@tauri-apps/plugin-shell';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '@/store/playerStore';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useArtistLayoutStore, type ArtistSectionId } from '../store/artistLayoutStore';
|
import { useArtistLayoutStore, type ArtistSectionId } from '@/features/artist/store/artistLayoutStore';
|
||||||
import {
|
import {
|
||||||
DEFAULT_ARTIST_ALBUM_YEAR_ORDER,
|
DEFAULT_ARTIST_ALBUM_YEAR_ORDER,
|
||||||
useArtistAlbumYearSortStore,
|
useArtistAlbumYearSortStore,
|
||||||
} from '../store/artistAlbumYearSortStore';
|
} from '@/features/artist/store/artistAlbumYearSortStore';
|
||||||
|
|
||||||
import { useArtistDetailData } from '../hooks/useArtistDetailData';
|
import { useArtistDetailData } from '@/features/artist/hooks/useArtistDetailData';
|
||||||
import { useArtistSimilarArtists } from '../hooks/useArtistSimilarArtists';
|
import { useArtistSimilarArtists } from '@/features/artist/hooks/useArtistSimilarArtists';
|
||||||
import {
|
import {
|
||||||
runArtistDetailPlayAll, runArtistDetailPlayTopSong, runArtistDetailShuffle, runArtistDetailStartRadio,
|
runArtistDetailPlayAll, runArtistDetailPlayTopSong, runArtistDetailShuffle, runArtistDetailStartRadio,
|
||||||
} from '../utils/componentHelpers/runArtistDetailPlay';
|
} from '@/features/artist/utils/runArtistDetailPlay';
|
||||||
import { useOfflineBrowseContext } from '@/features/offline';
|
import { useOfflineBrowseContext } from '@/features/offline';
|
||||||
import { offlineActionPolicy } from '@/features/offline';
|
import { offlineActionPolicy } from '@/features/offline';
|
||||||
import {
|
import {
|
||||||
runArtistEntityRating, runArtistToggleStar, runArtistShare, runArtistImageUpload,
|
runArtistEntityRating, runArtistToggleStar, runArtistShare, runArtistImageUpload,
|
||||||
} from '../utils/componentHelpers/runArtistDetailActions';
|
} from '@/features/artist/utils/runArtistDetailActions';
|
||||||
import ArtistDetailHero from '../components/artistDetail/ArtistDetailHero';
|
import ArtistDetailHero from '@/features/artist/components/ArtistDetailHero';
|
||||||
import ArtistDetailTopTracks from '../components/artistDetail/ArtistDetailTopTracks';
|
import ArtistDetailTopTracks from '@/features/artist/components/ArtistDetailTopTracks';
|
||||||
import ArtistDetailSimilarArtists from '../components/artistDetail/ArtistDetailSimilarArtists';
|
import ArtistDetailSimilarArtists from '@/features/artist/components/ArtistDetailSimilarArtists';
|
||||||
import { ArtistCard } from '@/features/nowPlaying';
|
import { ArtistCard } from '@/features/nowPlaying';
|
||||||
import LosslessModeBanner from '../components/LosslessModeBanner';
|
import LosslessModeBanner from '@/components/LosslessModeBanner';
|
||||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
|
||||||
import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX, GRID_COVER_WARM_LIMIT } from '../cover/layoutSizes';
|
import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX, GRID_COVER_WARM_LIMIT } from '@/cover/layoutSizes';
|
||||||
import { artistDetailCoverWarmAlbums } from '../components/artistDetail/topSongAlbumForCover';
|
import { artistDetailCoverWarmAlbums } from '@/features/artist/components/topSongAlbumForCover';
|
||||||
import { useLibraryCoverPrefetch } from '../cover/useLibraryCoverPrefetch';
|
import { useLibraryCoverPrefetch } from '@/cover/useLibraryCoverPrefetch';
|
||||||
import { useWarmGridCovers } from '../hooks/useWarmGridCovers';
|
import { useWarmGridCovers } from '@/hooks/useWarmGridCovers';
|
||||||
import { VirtualCardGrid } from '../components/VirtualCardGrid';
|
import { VirtualCardGrid } from '@/components/VirtualCardGrid';
|
||||||
import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode';
|
import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode';
|
||||||
import { sortArtistAlbumsByYear } from '../utils/library/sortArtistAlbums';
|
import { sortArtistAlbumsByYear } from '@/features/artist/utils/sortArtistAlbums';
|
||||||
import { readDetailServerId } from '../utils/navigation/detailServerScope';
|
import { readDetailServerId } from '@/utils/navigation/detailServerScope';
|
||||||
|
|
||||||
|
|
||||||
export default function ArtistDetail() {
|
export default function ArtistDetail() {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||||
import { useLocation, useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { LayoutGrid, List, Images } from 'lucide-react';
|
import { LayoutGrid, List, Images } from 'lucide-react';
|
||||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
import SelectionToggleButton from '@/components/SelectionToggleButton';
|
||||||
import StarFilterButton from '../components/StarFilterButton';
|
import StarFilterButton from '@/components/StarFilterButton';
|
||||||
import OverlayScrollArea from '@/ui/OverlayScrollArea';
|
import OverlayScrollArea from '@/ui/OverlayScrollArea';
|
||||||
import { usePlayerStore } from '../store/playerStore';
|
import { usePlayerStore } from '@/store/playerStore';
|
||||||
import { useAuthStore } from '../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||||
import { APP_MAIN_SCROLL_VIEWPORT_ID, ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
import { APP_MAIN_SCROLL_VIEWPORT_ID, ARTISTS_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
|
||||||
import { useElementClientHeightById, useElementClientHeightForElement } from '../hooks/useResizeClientHeight';
|
import { useElementClientHeightById, useElementClientHeightForElement } from '@/hooks/useResizeClientHeight';
|
||||||
import { useVirtualizerScrollMargin } from '../hooks/useVirtualizerScrollMargin';
|
import { useVirtualizerScrollMargin } from '@/hooks/useVirtualizerScrollMargin';
|
||||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
|
||||||
import {
|
import {
|
||||||
ALL_SENTINEL,
|
ALL_SENTINEL,
|
||||||
ALPHABET,
|
ALPHABET,
|
||||||
@@ -19,27 +19,27 @@ import {
|
|||||||
ARTIST_LIST_LAST_IN_LETTER_EST,
|
ARTIST_LIST_LAST_IN_LETTER_EST,
|
||||||
ARTIST_LIST_LETTER_ROW_EST,
|
ARTIST_LIST_LETTER_ROW_EST,
|
||||||
ARTIST_LIST_ROW_EST,
|
ARTIST_LIST_ROW_EST,
|
||||||
} from '../utils/componentHelpers/artistsHelpers';
|
} from '@/features/artist/utils/artistsHelpers';
|
||||||
import { useArtistsFiltering } from '../hooks/useArtistsFiltering';
|
import { useArtistsFiltering } from '@/features/artist/hooks/useArtistsFiltering';
|
||||||
import { useLibraryIgnoredArticles } from '../hooks/useLibraryIgnoredArticles';
|
import { useLibraryIgnoredArticles } from '@/hooks/useLibraryIgnoredArticles';
|
||||||
import { useArtistsBrowseCatalog } from '../hooks/useArtistsBrowseCatalog';
|
import { useArtistsBrowseCatalog } from '@/features/artist/hooks/useArtistsBrowseCatalog';
|
||||||
import { useBrowseArtistTextSearch } from '../hooks/useBrowseArtistTextSearch';
|
import { useBrowseArtistTextSearch } from '@/features/artist/hooks/useBrowseArtistTextSearch';
|
||||||
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
|
import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight';
|
||||||
import { useClientSliceInfiniteScroll } from '../hooks/useClientSliceInfiniteScroll';
|
import { useClientSliceInfiniteScroll } from '@/hooks/useClientSliceInfiniteScroll';
|
||||||
import { useInpageScrollSentinel } from '../hooks/useInpageScrollSentinel';
|
import { useInpageScrollSentinel } from '@/hooks/useInpageScrollSentinel';
|
||||||
import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport';
|
import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport';
|
||||||
import { ArtistsGridView } from '../components/artists/ArtistsGridView';
|
import { ArtistsGridView } from '@/features/artist/components/ArtistsGridView';
|
||||||
import { ArtistsListView } from '../components/artists/ArtistsListView';
|
import { ArtistsListView } from '@/features/artist/components/ArtistsListView';
|
||||||
import InpageScrollSentinel from '../components/InpageScrollSentinel';
|
import InpageScrollSentinel from '@/components/InpageScrollSentinel';
|
||||||
import { useArtistsBrowseFilters, type ArtistBrowseScrollSnapshot } from '../hooks/useArtistsBrowseFilters';
|
import { useArtistsBrowseFilters, type ArtistBrowseScrollSnapshot } from '@/features/artist/hooks/useArtistsBrowseFilters';
|
||||||
import { useArtistsBrowseScrollRestore } from '../hooks/useArtistsBrowseScrollRestore';
|
import { useArtistsBrowseScrollRestore } from '@/features/artist/hooks/useArtistsBrowseScrollRestore';
|
||||||
import { useArtistsBrowseScrollReset } from '../hooks/useArtistsBrowseScrollReset';
|
import { useArtistsBrowseScrollReset } from '@/features/artist/hooks/useArtistsBrowseScrollReset';
|
||||||
import { useNavigateToArtist } from '../hooks/useNavigateToArtist';
|
import { useNavigateToArtist } from '@/features/artist/hooks/useNavigateToArtist';
|
||||||
import { peekArtistBrowseScrollRestore } from '../store/artistBrowseSessionStore';
|
import { peekArtistBrowseScrollRestore } from '@/features/artist/store/artistBrowseSessionStore';
|
||||||
import { readArtistBrowseRestore } from '../utils/navigation/albumDetailNavigation';
|
import { readArtistBrowseRestore } from '@/utils/navigation/albumDetailNavigation';
|
||||||
|
|
||||||
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
|
import { useScopedBrowseSearchQuery } from '@/store/liveSearchScopeStore';
|
||||||
import { useLibraryIndexStore } from '../store/libraryIndexStore';
|
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||||
|
|
||||||
export default function Artists() {
|
export default function Artists() {
|
||||||
const perfFlags = usePerfProbeFlags();
|
const perfFlags = usePerfProbeFlags();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, expect, it, beforeEach } from 'vitest';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_ARTIST_ALBUM_YEAR_ORDER,
|
DEFAULT_ARTIST_ALBUM_YEAR_ORDER,
|
||||||
useArtistAlbumYearSortStore,
|
useArtistAlbumYearSortStore,
|
||||||
} from './artistAlbumYearSortStore';
|
} from '@/features/artist/store/artistAlbumYearSortStore';
|
||||||
|
|
||||||
describe('artistAlbumYearSortStore', () => {
|
describe('artistAlbumYearSortStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import type { ArtistAlbumYearOrder } from '../utils/library/sortArtistAlbums';
|
import type { ArtistAlbumYearOrder } from '@/features/artist/utils/sortArtistAlbums';
|
||||||
|
|
||||||
export const DEFAULT_ARTIST_ALBUM_YEAR_ORDER: ArtistAlbumYearOrder = 'yearDesc';
|
export const DEFAULT_ARTIST_ALBUM_YEAR_ORDER: ArtistAlbumYearOrder = 'yearDesc';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
DEFAULT_ARTIST_BROWSE_RETURN_STATE,
|
DEFAULT_ARTIST_BROWSE_RETURN_STATE,
|
||||||
peekArtistBrowseScrollRestore,
|
peekArtistBrowseScrollRestore,
|
||||||
useArtistBrowseSessionStore,
|
useArtistBrowseSessionStore,
|
||||||
} from './artistBrowseSessionStore';
|
} from '@/features/artist/store/artistBrowseSessionStore';
|
||||||
|
|
||||||
describe('artistBrowseSessionStore', () => {
|
describe('artistBrowseSessionStore', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { ALL_SENTINEL } from '../utils/componentHelpers/artistsHelpers';
|
import { ALL_SENTINEL } from '@/features/artist/utils/artistsHelpers';
|
||||||
|
|
||||||
export type ArtistBrowseViewMode = 'grid' | 'list';
|
export type ArtistBrowseViewMode = 'grid' | 'list';
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ALPHABET,
|
ALPHABET,
|
||||||
sortKeyFromDisplayName,
|
sortKeyFromDisplayName,
|
||||||
stripLeadingArticles,
|
stripLeadingArticles,
|
||||||
} from './artistsHelpers';
|
} from '@/features/artist/utils/artistsHelpers';
|
||||||
|
|
||||||
describe('stripLeadingArticles', () => {
|
describe('stripLeadingArticles', () => {
|
||||||
it('strips The from Beatles', () => {
|
it('strips The from Beatles', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
export const ALL_SENTINEL = 'ALL';
|
export const ALL_SENTINEL = 'ALL';
|
||||||
/** Catch-all bucket for names that start with neither an A–Z letter nor a digit
|
/** Catch-all bucket for names that start with neither an A–Z letter nor a digit
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { coerceOpenArtistRefs } from './openArtistRefs';
|
import { coerceOpenArtistRefs } from '@/features/artist/utils/openArtistRefs';
|
||||||
|
|
||||||
describe('coerceOpenArtistRefs', () => {
|
describe('coerceOpenArtistRefs', () => {
|
||||||
it('returns an empty array for nullish input', () => {
|
it('returns an empty array for nullish input', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubsonicOpenArtistRef } from '../api/subsonicTypes';
|
import type { SubsonicOpenArtistRef } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
/** Subsonic JSON may return one ref object instead of a one-element array. */
|
/** Subsonic JSON may return one ref object instead of a one-element array. */
|
||||||
export function coerceOpenArtistRefs(
|
export function coerceOpenArtistRefs(
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { uploadArtistImage } from '../../api/subsonicPlaylists';
|
import { uploadArtistImage } from '@/api/subsonicPlaylists';
|
||||||
import { setRating, star, unstar } from '../../api/subsonicStarRating';
|
import { setRating, star, unstar } from '@/api/subsonicStarRating';
|
||||||
import type { SubsonicArtist } from '../../api/subsonicTypes';
|
import type { SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { copyEntityShareLink } from '../share/copyEntityShareLink';
|
import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink';
|
||||||
import { invalidateCoverArt } from '../imageCache';
|
import { invalidateCoverArt } from '@/utils/imageCache';
|
||||||
import { showToast } from '../ui/toast';
|
import { showToast } from '@/utils/ui/toast';
|
||||||
|
|
||||||
export interface RunArtistEntityRatingDeps {
|
export interface RunArtistEntityRatingDeps {
|
||||||
artist: SubsonicArtist | null;
|
artist: SubsonicArtist | null;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum } from '@/api/subsonicTypes';
|
||||||
import * as offlineMediaResolve from '@/features/offline';
|
import * as offlineMediaResolve from '@/features/offline';
|
||||||
import { fetchArtistDetailTracks, runArtistDetailPlayTopSong } from './runArtistDetailPlay';
|
import { fetchArtistDetailTracks, runArtistDetailPlayTopSong } from '@/features/artist/utils/runArtistDetailPlay';
|
||||||
|
|
||||||
vi.mock('@/features/offline', () => ({
|
vi.mock('@/features/offline', () => ({
|
||||||
resolveAlbum: vi.fn(),
|
resolveAlbum: vi.fn(),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
import { getSimilarSongs2, getTopSongs } from '@/features/artist/api/subsonicArtists';
|
||||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import type { Track } from '../../store/playerStoreTypes';
|
import type { Track } from '@/store/playerStoreTypes';
|
||||||
import { songToTrack } from '../playback/songToTrack';
|
import { songToTrack } from '@/utils/playback/songToTrack';
|
||||||
import { runBulkPlayAll, runBulkShuffle } from '../playback/runBulkPlay';
|
import { runBulkPlayAll, runBulkShuffle } from '@/utils/playback/runBulkPlay';
|
||||||
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
|
import { resolveAlbum, resolveMediaServerId } from '@/features/offline';
|
||||||
|
|
||||||
/** Ordered artist discography tracks for play-all / shuffle (network or local bytes). */
|
/** Ordered artist discography tracks for play-all / shuffle (network or local bytes). */
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum } from '@/api/subsonicTypes';
|
||||||
import { sortArtistAlbumsByYear } from './sortArtistAlbums';
|
import { sortArtistAlbumsByYear } from '@/features/artist/utils/sortArtistAlbums';
|
||||||
|
|
||||||
const album = (id: string, name: string, year?: number): SubsonicAlbum => ({
|
const album = (id: string, name: string, year?: number): SubsonicAlbum => ({
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SubsonicAlbum } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
export type ArtistAlbumYearOrder = 'yearDesc' | 'yearAsc';
|
export type ArtistAlbumYearOrder = 'yearDesc' | 'yearAsc';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { getPlaylists } from '@/api/subsonicPlaylists';
|
import { getPlaylists } from '@/api/subsonicPlaylists';
|
||||||
import { getArtists, getArtist } from '@/api/subsonicArtists';
|
import { getArtists, getArtist } from '@/features/artist';
|
||||||
import { getAlbumList } from '@/api/subsonicLibrary';
|
import { getAlbumList } from '@/api/subsonicLibrary';
|
||||||
import { search as searchSubsonic } from '@/api/subsonicSearch';
|
import { search as searchSubsonic } from '@/api/subsonicSearch';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { formatLastSeen } from '@/utils/componentHelpers/userMgmtHelpers';
|
|||||||
import i18n from '@/i18n';
|
import i18n from '@/i18n';
|
||||||
import { formatTrackTime } from '@/utils/format/formatDuration';
|
import { formatTrackTime } from '@/utils/format/formatDuration';
|
||||||
import StarRating from '@/components/StarRating';
|
import StarRating from '@/components/StarRating';
|
||||||
import { OpenArtistRefInline } from '@/components/OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
|
import { resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
|
||||||
|
|
||||||
export interface FavoriteSongRowCallbacks {
|
export interface FavoriteSongRowCallbacks {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useFavoritesSongFiltering } from '@/features/favorites/hooks/useFavorit
|
|||||||
import { useFavoritesSelection } from '@/features/favorites/hooks/useFavoritesSelection';
|
import { useFavoritesSelection } from '@/features/favorites/hooks/useFavoritesSelection';
|
||||||
import { useBulkPlPickerOutsideClick } from '@/hooks/useBulkPlPickerOutsideClick';
|
import { useBulkPlPickerOutsideClick } from '@/hooks/useBulkPlPickerOutsideClick';
|
||||||
import AlbumRow from '@/components/AlbumRow';
|
import AlbumRow from '@/components/AlbumRow';
|
||||||
import ArtistRow from '@/components/ArtistRow';
|
import { ArtistRow } from '@/features/artist';
|
||||||
import { usePlayerStore } from '@/store/playerStore';
|
import { usePlayerStore } from '@/store/playerStore';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelectionStore } from '@/store/selectionStore';
|
import { useSelectionStore } from '@/store/selectionStore';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import CachedImage from '@/ui/CachedImage';
|
import CachedImage from '@/ui/CachedImage';
|
||||||
import { OpenArtistRefInline } from '@/components/OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import type { MiniTrackInfo } from '@/features/miniPlayer/utils/miniPlayerBridge';
|
import type { MiniTrackInfo } from '@/features/miniPlayer/utils/miniPlayerBridge';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { CoverArtImage } from '@/cover/CoverArtImage';
|
|||||||
import type { CoverArtRef } from '@/cover/types';
|
import type { CoverArtRef } from '@/cover/types';
|
||||||
import type { ArtistStats, TrackStats } from '@/music-network';
|
import type { ArtistStats, TrackStats } from '@/music-network';
|
||||||
import type { SubsonicOpenArtistRef } from '@/api/subsonicTypes';
|
import type { SubsonicOpenArtistRef } from '@/api/subsonicTypes';
|
||||||
import { OpenArtistRefInline } from '@/components/OpenArtistRefInline';
|
import { OpenArtistRefInline } from '@/features/artist';
|
||||||
import { formatTrackTime } from '@/utils/format/formatDuration';
|
import { formatTrackTime } from '@/utils/format/formatDuration';
|
||||||
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
|
import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel';
|
||||||
import { useEnrichmentPrimaryIcon } from '@/hooks/useEnrichmentPrimaryIcon';
|
import { useEnrichmentPrimaryIcon } from '@/hooks/useEnrichmentPrimaryIcon';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getSongForServer } from '@/api/subsonicLibrary';
|
import { getSongForServer } from '@/api/subsonicLibrary';
|
||||||
import { getArtistInfoForServer } from '@/api/subsonicArtists';
|
import { getArtistInfoForServer } from '@/features/artist';
|
||||||
import type { SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { renderHook, act, waitFor } from '@testing-library/react';
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import type { SubsonicArtistInfo, SubsonicSong, SubsonicAlbum, SubsonicArtist } from '@/api/subsonicTypes';
|
import type { SubsonicArtistInfo, SubsonicSong, SubsonicAlbum, SubsonicArtist } from '@/api/subsonicTypes';
|
||||||
|
|
||||||
vi.mock('@/api/subsonicArtists');
|
vi.mock('@/features/artist');
|
||||||
vi.mock('@/api/subsonicLibrary');
|
vi.mock('@/api/subsonicLibrary');
|
||||||
vi.mock('@/api/bandsintown');
|
vi.mock('@/api/bandsintown');
|
||||||
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
||||||
@@ -21,7 +21,7 @@ vi.mock('@/utils/network/subsonicNetworkGuard', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||||
import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/api/subsonicArtists';
|
import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/features/artist';
|
||||||
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
|
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
|
||||||
import { fetchBandsintownEvents } from '@/api/bandsintown';
|
import { fetchBandsintownEvents } from '@/api/bandsintown';
|
||||||
import { useNowPlayingFetchers, type NowPlayingFetchersDeps } from '@/features/nowPlaying/hooks/useNowPlayingFetchers';
|
import { useNowPlayingFetchers, type NowPlayingFetchersDeps } from '@/features/nowPlaying/hooks/useNowPlayingFetchers';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { getArtistInfoForServer } from '@/api/subsonicArtists';
|
import { getArtistInfoForServer } from '@/features/artist';
|
||||||
import type { SubsonicAlbum, SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSongs } from '@/features/nowPlaying/utils/nowPlayingMetadataResolve';
|
import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSongs } from '@/features/nowPlaying/utils/nowPlayingMetadataResolve';
|
||||||
import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown';
|
import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown';
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import TourCard from '@/features/nowPlaying/components/TourCard';
|
|||||||
import DiscographyCard from '@/features/nowPlaying/components/DiscographyCard';
|
import DiscographyCard from '@/features/nowPlaying/components/DiscographyCard';
|
||||||
import { useNowPlayingFetchers } from '@/features/nowPlaying/hooks/useNowPlayingFetchers';
|
import { useNowPlayingFetchers } from '@/features/nowPlaying/hooks/useNowPlayingFetchers';
|
||||||
import { useNowPlayingStarLove } from '@/features/nowPlaying/hooks/useNowPlayingStarLove';
|
import { useNowPlayingStarLove } from '@/features/nowPlaying/hooks/useNowPlayingStarLove';
|
||||||
import { useArtistInfoBatch } from '@/hooks/useArtistInfoBatch';
|
import { useArtistInfoBatch } from '@/features/artist';
|
||||||
import { primaryTrackArtistRef, resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
|
import { primaryTrackArtistRef, resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs';
|
||||||
import type { ArtistCardTab } from '@/features/nowPlaying/components/ArtistCard';
|
import type { ArtistCardTab } from '@/features/nowPlaying/components/ArtistCard';
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|||||||
import { onInvoke } from '@/test/mocks/tauri';
|
import { onInvoke } from '@/test/mocks/tauri';
|
||||||
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||||
import type { LibraryAdvancedSearchResponse } from '@/api/library';
|
import type { LibraryAdvancedSearchResponse } from '@/api/library';
|
||||||
import * as subsonicArtists from '@/api/subsonicArtists';
|
import * as subsonicArtists from '@/features/artist';
|
||||||
import * as subsonicLibrary from '@/api/subsonicLibrary';
|
import * as subsonicLibrary from '@/api/subsonicLibrary';
|
||||||
|
|
||||||
// Network reachability is decided by the guard; mock it so we can test both arms.
|
// Network reachability is decided by the guard; mock it so we can test both arms.
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* is intentionally absent here.
|
* is intentionally absent here.
|
||||||
*/
|
*/
|
||||||
import { libraryGetTrack, libraryGetTracksByAlbum } from '@/api/library';
|
import { libraryGetTrack, libraryGetTracksByAlbum } from '@/api/library';
|
||||||
import { getArtistForServer, getTopSongsForServer } from '@/api/subsonicArtists';
|
import { getArtistForServer, getTopSongsForServer } from '@/features/artist';
|
||||||
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
|
import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary';
|
||||||
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { libraryUpsertSongsFromApi } from '@/api/library';
|
import { libraryUpsertSongsFromApi } from '@/api/library';
|
||||||
import { buildStreamUrl } from '@/api/subsonicStreamUrl';
|
import { buildStreamUrl } from '@/api/subsonicStreamUrl';
|
||||||
import { getAlbum } from '@/api/subsonicLibrary';
|
import { getAlbum } from '@/api/subsonicLibrary';
|
||||||
import { getArtist } from '@/api/subsonicArtists';
|
import { getArtist } from '@/features/artist';
|
||||||
import type { SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ vi.mock('@/api/subsonicLibrary', () => ({
|
|||||||
getAlbumForServer: vi.fn(async () => ({ songs: [] })),
|
getAlbumForServer: vi.fn(async () => ({ songs: [] })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/api/subsonicArtists', () => ({
|
vi.mock('@/features/artist', () => ({
|
||||||
getArtistForServer: vi.fn(async () => ({ albums: [] })),
|
getArtistForServer: vi.fn(async () => ({ albums: [] })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { libraryUpsertSongsFromApi } from '@/api/library';
|
import { libraryUpsertSongsFromApi } from '@/api/library';
|
||||||
import { librarySqlServerId } from '@/api/coverCache';
|
import { librarySqlServerId } from '@/api/coverCache';
|
||||||
import { getAlbumForServer } from '@/api/subsonicLibrary';
|
import { getAlbumForServer } from '@/api/subsonicLibrary';
|
||||||
import { getArtistForServer } from '@/api/subsonicArtists';
|
import { getArtistForServer } from '@/features/artist';
|
||||||
import { getStarredForServer } from '@/api/subsonicStarRating';
|
import { getStarredForServer } from '@/api/subsonicStarRating';
|
||||||
import { buildStreamUrlForServer } from '@/api/subsonicStreamUrl';
|
import { buildStreamUrlForServer } from '@/api/subsonicStreamUrl';
|
||||||
import type { SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicSong } from '@/api/subsonicTypes';
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ vi.mock('@/api/subsonicLibrary', () => ({
|
|||||||
getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId),
|
getAlbumForServer: (serverId: string, albumId: string) => getAlbumForServerMock(serverId, albumId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/api/subsonicArtists', () => ({
|
vi.mock('@/features/artist', () => ({
|
||||||
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
|
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getAlbumForServer } from '@/api/subsonicLibrary';
|
import { getAlbumForServer } from '@/api/subsonicLibrary';
|
||||||
import { getArtistForServer } from '@/api/subsonicArtists';
|
import { getArtistForServer } from '@/features/artist';
|
||||||
import { getPlaylistForServer } from '@/api/subsonicPlaylists';
|
import { getPlaylistForServer } from '@/api/subsonicPlaylists';
|
||||||
import type {
|
import type {
|
||||||
SubsonicAlbum,
|
SubsonicAlbum,
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ vi.mock('@/api/subsonicLibrary', () => ({
|
|||||||
filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs),
|
filterSongsToServerLibrary: (songs: SubsonicSong[]) => filterSongsMock(songs),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('@/api/subsonicArtists', () => ({
|
vi.mock('@/features/artist', () => ({
|
||||||
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
|
getArtistForServer: (serverId: string, artistId: string) => getArtistForServerMock(serverId, artistId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/api/library';
|
import { libraryGetTracksByAlbum, subscribeLibrarySyncIdle } from '@/api/library';
|
||||||
import { getAlbumForServer, filterSongsToServerLibrary } from '@/api/subsonicLibrary';
|
import { getAlbumForServer, filterSongsToServerLibrary } from '@/api/subsonicLibrary';
|
||||||
import { getPlaylistForServer } from '@/api/subsonicPlaylists';
|
import { getPlaylistForServer } from '@/api/subsonicPlaylists';
|
||||||
import { getArtistForServer } from '@/api/subsonicArtists';
|
import { getArtistForServer } from '@/features/artist';
|
||||||
import type { SubsonicSong } from '@/api/subsonicTypes';
|
import type { SubsonicSong } from '@/api/subsonicTypes';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ALL_NAV_ITEMS } from '@/config/navItems';
|
|||||||
import type { LiveSearchScope } from '@/store/liveSearchScopeStore';
|
import type { LiveSearchScope } from '@/store/liveSearchScopeStore';
|
||||||
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
|
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
|
||||||
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
|
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
|
||||||
import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore';
|
import { isArtistsBrowsePath } from '@/features/artist';
|
||||||
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
|
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
|
||||||
|
|
||||||
export const SCOPE_NAV_ITEM: Record<LiveSearchScope, keyof typeof ALL_NAV_ITEMS> = {
|
export const SCOPE_NAV_ITEM: Record<LiveSearchScope, keyof typeof ALL_NAV_ITEMS> = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
|
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
|
||||||
import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore';
|
import { isArtistsBrowsePath } from '@/features/artist';
|
||||||
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
|
import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
|
||||||
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
|
import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
|
||||||
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
|
import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useLocation, useNavigate, useNavigationType, useSearchParams } from 're
|
|||||||
import { SlidersVertical, Search, X } from 'lucide-react';
|
import { SlidersVertical, Search, X } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import AlbumRow from '@/components/AlbumRow';
|
import AlbumRow from '@/components/AlbumRow';
|
||||||
import ArtistRow from '@/components/ArtistRow';
|
import { ArtistRow } from '@/features/artist';
|
||||||
import PagedSongList from '@/components/PagedSongList';
|
import PagedSongList from '@/components/PagedSongList';
|
||||||
import CustomSelect from '@/ui/CustomSelect';
|
import CustomSelect from '@/ui/CustomSelect';
|
||||||
import StarFilterButton from '@/components/StarFilterButton';
|
import StarFilterButton from '@/components/StarFilterButton';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useRef } from 'react';
|
import { useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useArtistLayoutStore, type ArtistSectionConfig, type ArtistSectionId } from '@/store/artistLayoutStore';
|
import { useArtistLayoutStore, type ArtistSectionConfig, type ArtistSectionId } from '@/features/artist';
|
||||||
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
|
import { useListReorderDnd } from '@/hooks/useListReorderDnd';
|
||||||
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
|
import { applyListReorderById, type ListReorderDropTarget } from '@/utils/componentHelpers/listReorder';
|
||||||
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
|
import { ReorderGripHandle } from '@/features/settings/components/ReorderGripHandle';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
|
import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } from 'lucide-react';
|
||||||
import { useArtistLayoutStore } from '@/store/artistLayoutStore';
|
import { useArtistLayoutStore } from '@/features/artist';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import type { QueueDisplayMode } from '@/store/authStoreTypes';
|
import type { QueueDisplayMode } from '@/store/authStoreTypes';
|
||||||
import { useHomeStore } from '@/store/homeStore';
|
import { useHomeStore } from '@/store/homeStore';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { genreTagsFor } from '@/utils/library/genreTags';
|
import { genreTagsFor } from '@/utils/library/genreTags';
|
||||||
import { getArtists } from '@/api/subsonicArtists';
|
import { getArtists } from '@/features/artist';
|
||||||
import { getAlbumList, getRandomSongs } from '@/api/subsonicLibrary';
|
import { getAlbumList, getRandomSongs } from '@/api/subsonicLibrary';
|
||||||
import type {
|
import type {
|
||||||
StatisticsFormatSample,
|
StatisticsFormatSample,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import type { NavigateFunction } from 'react-router-dom';
|
import type { NavigateFunction } from 'react-router-dom';
|
||||||
import { getSimilarSongs } from '../../api/subsonicArtists';
|
import { getSimilarSongs } from '@/features/artist';
|
||||||
import { getMusicFolders } from '../../api/subsonicLibrary';
|
import { getMusicFolders } from '../../api/subsonicLibrary';
|
||||||
import { search as subsonicSearch } from '../../api/subsonicSearch';
|
import { search as subsonicSearch } from '../../api/subsonicSearch';
|
||||||
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../../utils/mix/mixRatingFilter';
|
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../../utils/mix/mixRatingFilter';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { buildDownloadUrl } from '../api/subsonicStreamUrl';
|
|||||||
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
||||||
import { queueSongStar, queueSongRating } from '../store/pendingStarSync';
|
import { queueSongStar, queueSongRating } from '../store/pendingStarSync';
|
||||||
import { getAlbumForServer } from '../api/subsonicLibrary';
|
import { getAlbumForServer } from '../api/subsonicLibrary';
|
||||||
import { getArtistInfo } from '../api/subsonicArtists';
|
import { getArtistInfo } from '@/features/artist';
|
||||||
import type { SubsonicSong } from '../api/subsonicTypes';
|
import type { SubsonicSong } from '../api/subsonicTypes';
|
||||||
import { songToTrack } from '../utils/playback/songToTrack';
|
import { songToTrack } from '../utils/playback/songToTrack';
|
||||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { star, unstar } from '../api/subsonicStarRating';
|
import { star, unstar } from '../api/subsonicStarRating';
|
||||||
import { getArtist, getArtistInfo } from '../api/subsonicArtists';
|
import { getArtist, getArtistInfo } from '@/features/artist';
|
||||||
import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '../api/subsonicTypes';
|
import type { SubsonicArtist, SubsonicAlbum, SubsonicArtistInfo } from '../api/subsonicTypes';
|
||||||
import { useEffect, useState, useMemo } from 'react';
|
import { useEffect, useState, useMemo } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
|||||||
@@ -11,16 +11,16 @@ import { useVirtualizer } from '@tanstack/react-virtual';
|
|||||||
import { APP_MAIN_SCROLL_VIEWPORT_ID, COMPOSERS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
import { APP_MAIN_SCROLL_VIEWPORT_ID, COMPOSERS_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
|
||||||
import { useElementClientHeightById, useElementClientHeightForElement } from '../hooks/useResizeClientHeight';
|
import { useElementClientHeightById, useElementClientHeightForElement } from '../hooks/useResizeClientHeight';
|
||||||
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
|
import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight';
|
||||||
import { useBrowseArtistTextSearch } from '../hooks/useBrowseArtistTextSearch';
|
import { useBrowseArtistTextSearch } from '@/features/artist';
|
||||||
import { useComposersBrowseFilters, type ComposerBrowseScrollSnapshot } from '../hooks/useComposersBrowseFilters';
|
import { useComposersBrowseFilters, type ComposerBrowseScrollSnapshot } from '../hooks/useComposersBrowseFilters';
|
||||||
import { useComposersBrowseScrollRestore } from '../hooks/useComposersBrowseScrollRestore';
|
import { useComposersBrowseScrollRestore } from '../hooks/useComposersBrowseScrollRestore';
|
||||||
import { useArtistsBrowseScrollReset } from '../hooks/useArtistsBrowseScrollReset';
|
import { useArtistsBrowseScrollReset } from '@/features/artist';
|
||||||
import { useNavigateToComposer } from '../hooks/useNavigateToComposer';
|
import { useNavigateToComposer } from '../hooks/useNavigateToComposer';
|
||||||
import { peekComposerBrowseScrollRestore } from '../store/composerBrowseSessionStore';
|
import { peekComposerBrowseScrollRestore } from '../store/composerBrowseSessionStore';
|
||||||
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
|
import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore';
|
||||||
import { readComposerBrowseRestore } from '../utils/navigation/albumDetailNavigation';
|
import { readComposerBrowseRestore } from '../utils/navigation/albumDetailNavigation';
|
||||||
import { filterArtistsWithRoleAlbumCredits } from '../utils/library/composerBrowse';
|
import { filterArtistsWithRoleAlbumCredits } from '../utils/library/composerBrowse';
|
||||||
import { ALL_SENTINEL, artistLetterBucket } from '../utils/componentHelpers/artistsHelpers';
|
import { ALL_SENTINEL, artistLetterBucket } from '@/features/artist';
|
||||||
import { useLibraryIgnoredArticles } from '../hooks/useLibraryIgnoredArticles';
|
import { useLibraryIgnoredArticles } from '../hooks/useLibraryIgnoredArticles';
|
||||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||||
import { VirtualCardGrid } from '../components/VirtualCardGrid';
|
import { VirtualCardGrid } from '../components/VirtualCardGrid';
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { getArtists } from '../api/subsonicArtists';
|
import { getArtists } from '@/features/artist';
|
||||||
import { getAlbumList, getRandomSongs } from '../api/subsonicLibrary';
|
import { getAlbumList, getRandomSongs } from '../api/subsonicLibrary';
|
||||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonicTypes';
|
||||||
import { runLocalRandomSongs } from '../utils/library/browseTextSearch';
|
import { runLocalRandomSongs } from '../utils/library/browseTextSearch';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { ALL_SENTINEL } from '../utils/componentHelpers/artistsHelpers';
|
import { ALL_SENTINEL } from '@/features/artist';
|
||||||
|
|
||||||
export type ComposerBrowseViewMode = 'grid' | 'list';
|
export type ComposerBrowseViewMode = 'grid' | 'list';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getSimilarSongs2, getTopSongs } from '../api/subsonicArtists';
|
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { buildInfiniteQueueCandidates } from '../utils/playback/buildInfiniteQueueCandidates';
|
import { buildInfiniteQueueCandidates } from '../utils/playback/buildInfiniteQueueCandidates';
|
||||||
import { songToTrack } from '../utils/playback/songToTrack';
|
import { songToTrack } from '../utils/playback/songToTrack';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const { inOrbit, getSimilarSongs2, getTopSongs } = vi.hoisted(() => ({
|
|||||||
getTopSongs: vi.fn(() => Promise.resolve([])),
|
getTopSongs: vi.fn(() => Promise.resolve([])),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../api/subsonicArtists', () => ({ getSimilarSongs2, getTopSongs }));
|
vi.mock('@/features/artist', () => ({ getSimilarSongs2, getTopSongs }));
|
||||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(() => Promise.resolve()) }));
|
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(() => Promise.resolve()) }));
|
||||||
vi.mock('@/features/orbit', () => ({ isInOrbitSession: () => inOrbit.value }));
|
vi.mock('@/features/orbit', () => ({ isInOrbitSession: () => inOrbit.value }));
|
||||||
vi.mock('./authStore', () => ({
|
vi.mock('./authStore', () => ({
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ vi.mock('@/api/subsonicLibrary', () => ({
|
|||||||
getSong: vi.fn(async () => null),
|
getSong: vi.fn(async () => null),
|
||||||
getRandomSongs: vi.fn(async () => []),
|
getRandomSongs: vi.fn(async () => []),
|
||||||
}));
|
}));
|
||||||
vi.mock('@/api/subsonicArtists', () => ({
|
vi.mock('@/features/artist', () => ({
|
||||||
getSimilarSongs2: vi.fn(async () => []),
|
getSimilarSongs2: vi.fn(async () => []),
|
||||||
getTopSongs: vi.fn(async () => []),
|
getTopSongs: vi.fn(async () => []),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { SubsonicAlbum, SubsonicOpenArtistRef, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicOpenArtistRef, SubsonicSong } from '../../api/subsonicTypes';
|
||||||
import { coerceOpenArtistRefs } from '../openArtistRefs';
|
import { coerceOpenArtistRefs } from '@/features/artist';
|
||||||
|
|
||||||
function nonEmpty(refs: SubsonicOpenArtistRef[]): refs is SubsonicOpenArtistRef[] {
|
function nonEmpty(refs: SubsonicOpenArtistRef[]): refs is SubsonicOpenArtistRef[] {
|
||||||
return refs.length > 0;
|
return refs.length > 0;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { join } from '@tauri-apps/api/path';
|
import { join } from '@tauri-apps/api/path';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { getSimilarSongs2, fetchSimilarTracksRouted, getTopSongs } from '../../api/subsonicArtists';
|
import { getSimilarSongs2, fetchSimilarTracksRouted, getTopSongs } from '@/features/artist';
|
||||||
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../mix/mixRatingFilter';
|
import { filterSongsForLuckyMixRatings, getMixMinRatingsConfigFromAuth } from '../mix/mixRatingFilter';
|
||||||
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
|
import { buildDownloadUrl } from '../../api/subsonicStreamUrl';
|
||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAuthStore } from '../../store/authStore';
|
import { useAuthStore } from '../../store/authStore';
|
||||||
import { DEFAULT_ARTIST_SECTIONS, useArtistLayoutStore } from '../../store/artistLayoutStore';
|
import { DEFAULT_ARTIST_SECTIONS, useArtistLayoutStore } from '@/features/artist';
|
||||||
import { DEFAULT_QUEUE_TOOLBAR_BUTTONS, useQueueToolbarStore } from '../../store/queueToolbarStore';
|
import { DEFAULT_QUEUE_TOOLBAR_BUTTONS, useQueueToolbarStore } from '../../store/queueToolbarStore';
|
||||||
import { DEFAULT_PLAYLIST_LAYOUT_ITEMS, usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
|
import { DEFAULT_PLAYLIST_LAYOUT_ITEMS, usePlaylistLayoutStore } from '../../store/playlistLayoutStore';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { fetchSimilarTracksRouted } from '../../api/subsonicArtists';
|
import { fetchSimilarTracksRouted } from '@/features/artist';
|
||||||
import { filterSongsToActiveLibrary, getRandomSongs } from '../../api/subsonicLibrary';
|
import { filterSongsToActiveLibrary, getRandomSongs } from '../../api/subsonicLibrary';
|
||||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
||||||
import type { QueueItemRef } from '../../store/playerStoreTypes';
|
import type { QueueItemRef } from '../../store/playerStoreTypes';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getTopSongs } from '../../api/subsonicArtists';
|
import { getTopSongs } from '@/features/artist';
|
||||||
import { filterSongsToActiveLibrary, getAlbumList, getRandomSongs } from '../../api/subsonicLibrary';
|
import { filterSongsToActiveLibrary, getAlbumList, getRandomSongs } from '../../api/subsonicLibrary';
|
||||||
import { resolveAlbumForActiveServer } from '@/features/offline';
|
import { resolveAlbumForActiveServer } from '@/features/offline';
|
||||||
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes';
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
* refactor (2026-05-12). This test pins the artist-first / random-fallback
|
* refactor (2026-05-12). This test pins the artist-first / random-fallback
|
||||||
* order, the dedup contract against existingIds, and the autoAdded flag.
|
* order, the dedup contract against existingIds, and the autoAdded flag.
|
||||||
*/
|
*/
|
||||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
|
||||||
import { getRandomSongs } from '../../api/subsonicLibrary';
|
import { getRandomSongs } from '../../api/subsonicLibrary';
|
||||||
import type { Track } from '../../store/playerStoreTypes';
|
import type { Track } from '../../store/playerStoreTypes';
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
vi.mock('../../api/subsonicArtists', () => ({
|
// Mock only the artist Subsonic API submodule (the pre-move target was
|
||||||
|
// `api/subsonicArtists`); the barrel re-exports it, so consumers still get the
|
||||||
|
// stubs while `coerceOpenArtistRefs` (used by songToTrack) stays real.
|
||||||
|
vi.mock('@/features/artist/api/subsonicArtists', () => ({
|
||||||
getSimilarSongs2: vi.fn(),
|
getSimilarSongs2: vi.fn(),
|
||||||
getTopSongs: vi.fn(),
|
getTopSongs: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getSimilarSongs2, getTopSongs } from '../../api/subsonicArtists';
|
import { getSimilarSongs2, getTopSongs } from '@/features/artist';
|
||||||
import { getRandomSongs } from '../../api/subsonicLibrary';
|
import { getRandomSongs } from '../../api/subsonicLibrary';
|
||||||
import type { Track } from '../../store/playerStoreTypes';
|
import type { Track } from '../../store/playerStoreTypes';
|
||||||
import {
|
import {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user