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

This commit is contained in:
Psychotoxical
2026-06-30 01:27:01 +02:00
parent 17ea96dcdb
commit 9ffc42688c
103 changed files with 301 additions and 254 deletions
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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 -1
View File
@@ -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';
+2 -2
View File
@@ -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'));
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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() {
+1 -1
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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';
+4 -1
View File
@@ -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,
})); }));
+2 -2
View File
@@ -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 {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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';
+2 -2
View File
@@ -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);
+7 -7
View File
@@ -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[];
+2 -2
View File
@@ -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() {
+41
View File
@@ -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';
+25 -25
View File
@@ -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() {
+28 -28
View File
@@ -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 -1
View File
@@ -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 AZ letter nor a digit /** Catch-all bucket for names that start with neither an AZ 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 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,
+1 -1
View File
@@ -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';
+1 -1
View File
@@ -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 -1
View File
@@ -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';
+3 -3
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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';
+1 -1
View File
@@ -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', () => ({
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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