From 931c47e19e8972bb3b453d1796a5717a6cfe8c8b Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:38:09 +0200 Subject: [PATCH] refactor(nowPlaying): co-locate now playing feature into features/nowPlaying --- src/app/AppRoutes.tsx | 2 +- src/app/AppShell.tsx | 4 +- src/components/QueuePanel.tsx | 2 +- .../api/nowPlayingPresence.test.ts | 4 +- .../nowPlaying}/api/nowPlayingPresence.ts | 2 +- .../nowPlaying/components}/AlbumCard.tsx | 6 +- .../components}/ArtistCard.test.tsx | 4 +- .../nowPlaying/components}/ArtistCard.tsx | 4 +- .../nowPlaying/components}/CreditsCard.tsx | 2 +- .../components}/DiscographyCard.tsx | 6 +- .../nowPlaying/components}/Hero.tsx | 18 +++--- .../components/NowPlayingDropdown.tsx | 12 ++-- .../nowPlaying}/components/NowPlayingInfo.tsx | 16 ++--- .../nowPlaying/components}/NpCardWrap.tsx | 4 +- .../nowPlaying/components}/NpColumnEl.tsx | 2 +- .../nowPlaying/components}/RadioView.tsx | 4 +- .../nowPlaying/components}/TopSongsCard.tsx | 4 +- .../nowPlaying/components}/TourCard.tsx | 4 +- .../hooks/useNowPlayingFetchers.test.ts | 20 +++---- .../hooks/useNowPlayingFetchers.ts | 18 +++--- .../hooks/useNowPlayingPrewarm.test.ts | 24 ++++---- .../nowPlaying}/hooks/useNowPlayingPrewarm.ts | 28 ++++----- .../hooks/useNowPlayingStarLove.ts | 10 ++-- src/features/nowPlaying/index.ts | 12 ++++ .../nowPlaying}/pages/NowPlaying.tsx | 58 +++++++++---------- .../store/nowPlayingLayoutStore.ts | 0 .../utils}/nowPlayingMetadataResolve.test.ts | 2 +- .../utils}/nowPlayingMetadataResolve.ts | 16 ++--- src/pages/ArtistDetail.tsx | 2 +- 29 files changed, 151 insertions(+), 139 deletions(-) rename src/{ => features/nowPlaying}/api/nowPlayingPresence.test.ts (87%) rename src/{ => features/nowPlaying}/api/nowPlayingPresence.ts (95%) rename src/{components/nowPlaying => features/nowPlaying/components}/AlbumCard.tsx (94%) rename src/{components/nowPlaying => features/nowPlaying/components}/ArtistCard.test.tsx (96%) rename src/{components/nowPlaying => features/nowPlaying/components}/ArtistCard.tsx (97%) rename src/{components/nowPlaying => features/nowPlaying/components}/CreditsCard.tsx (91%) rename src/{components/nowPlaying => features/nowPlaying/components}/DiscographyCard.tsx (93%) rename src/{components/nowPlaying => features/nowPlaying/components}/Hero.tsx (93%) rename src/{ => features/nowPlaying}/components/NowPlayingDropdown.tsx (97%) rename src/{ => features/nowPlaying}/components/NowPlayingInfo.tsx (96%) rename src/{components/nowPlaying => features/nowPlaying/components}/NpCardWrap.tsx (79%) rename src/{components/nowPlaying => features/nowPlaying/components}/NpColumnEl.tsx (95%) rename src/{components/nowPlaying => features/nowPlaying/components}/RadioView.tsx (97%) rename src/{components/nowPlaying => features/nowPlaying/components}/TopSongsCard.tsx (94%) rename src/{components/nowPlaying => features/nowPlaying/components}/TourCard.tsx (97%) rename src/{ => features/nowPlaying}/hooks/useNowPlayingFetchers.test.ts (95%) rename src/{ => features/nowPlaying}/hooks/useNowPlayingFetchers.ts (96%) rename src/{ => features/nowPlaying}/hooks/useNowPlayingPrewarm.test.ts (79%) rename src/{ => features/nowPlaying}/hooks/useNowPlayingPrewarm.ts (78%) rename src/{ => features/nowPlaying}/hooks/useNowPlayingStarLove.ts (88%) create mode 100644 src/features/nowPlaying/index.ts rename src/{ => features/nowPlaying}/pages/NowPlaying.tsx (90%) rename src/{ => features/nowPlaying}/store/nowPlayingLayoutStore.ts (100%) rename src/{utils/library => features/nowPlaying/utils}/nowPlayingMetadataResolve.test.ts (99%) rename src/{utils/library => features/nowPlaying/utils}/nowPlayingMetadataResolve.ts (87%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index e36a5539..c0dc9f1a 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -26,7 +26,7 @@ const RandomAlbums = lazy(() => import('../pages/RandomAlbums')); const LuckyMixPage = lazy(() => import('../pages/LuckyMix')); const Playlists = lazy(() => import('../pages/Playlists')); const PlaylistDetail = lazy(() => import('../pages/PlaylistDetail')); -const NowPlayingPage = lazy(() => import('../pages/NowPlaying')); +const NowPlayingPage = lazy(() => import('@/features/nowPlaying/pages/NowPlaying')); const Settings = lazy(() => import('../pages/Settings')); const Statistics = lazy(() => import('@/features/stats/pages/Statistics')); const Help = lazy(() => import('../pages/Help')); diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index 6ccd55c7..8b444787 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -11,7 +11,7 @@ import BottomNav from '../components/BottomNav'; import { useIsMobile } from '../hooks/useIsMobile'; import LiveSearch from '../components/LiveSearch'; import DevNetworkModeToggle from '../components/DevNetworkModeToggle'; -import NowPlayingDropdown from '../components/NowPlayingDropdown'; +import { NowPlayingDropdown } from '@/features/nowPlaying'; import QueuePanel from '../components/QueuePanel'; import AppRoutes from './AppRoutes'; import FullscreenPlayer from '@/features/fullscreenPlayer'; @@ -52,7 +52,7 @@ import { useAppActivityTracking } from '../hooks/useAppActivityTracking'; import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator'; import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority'; import { useLiveSearchRouteScope } from '../hooks/useLiveSearchRouteScope'; -import { useNowPlayingPrewarm } from '../hooks/useNowPlayingPrewarm'; +import { useNowPlayingPrewarm } from '@/features/nowPlaying'; import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; import { AppShellQueueResizerSeam } from '../components/AppShellQueueResizerSeam'; diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 727a1a0a..601106de 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -20,7 +20,7 @@ import { showToast } from '../utils/ui/toast'; import { useThemeStore } from '../store/themeStore'; import { useLyricsStore } from '../store/lyricsStore'; import LyricsPane from './LyricsPane'; -import NowPlayingInfo from './NowPlayingInfo'; +import { NowPlayingInfo } from '@/features/nowPlaying'; import { useLuckyMixStore } from '../store/luckyMixStore'; import { useQueueToolbarStore } from '../store/queueToolbarStore'; import { SavePlaylistModal } from './queuePanel/SavePlaylistModal'; diff --git a/src/api/nowPlayingPresence.test.ts b/src/features/nowPlaying/api/nowPlayingPresence.test.ts similarity index 87% rename from src/api/nowPlayingPresence.test.ts rename to src/features/nowPlaying/api/nowPlayingPresence.test.ts index 50efeb96..ec756007 100644 --- a/src/api/nowPlayingPresence.test.ts +++ b/src/features/nowPlaying/api/nowPlayingPresence.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { nowPlayingPresence, NOW_PLAYING_IDLE_MINUTES } from './nowPlayingPresence'; -import type { SubsonicNowPlaying, PlaybackReportState } from './subsonicTypes'; +import { nowPlayingPresence, NOW_PLAYING_IDLE_MINUTES } from '@/features/nowPlaying/api/nowPlayingPresence'; +import type { SubsonicNowPlaying, PlaybackReportState } from '@/api/subsonicTypes'; // The function only reads `state` and `minutesAgo`; cast a minimal fixture. function entry(partial: { state?: PlaybackReportState; minutesAgo?: number }): SubsonicNowPlaying { diff --git a/src/api/nowPlayingPresence.ts b/src/features/nowPlaying/api/nowPlayingPresence.ts similarity index 95% rename from src/api/nowPlayingPresence.ts rename to src/features/nowPlaying/api/nowPlayingPresence.ts index b2ca65fa..d7854acb 100644 --- a/src/api/nowPlayingPresence.ts +++ b/src/features/nowPlaying/api/nowPlayingPresence.ts @@ -1,4 +1,4 @@ -import type { SubsonicNowPlaying } from './subsonicTypes'; +import type { SubsonicNowPlaying } from '@/api/subsonicTypes'; /** * Derived liveness of a now-playing entry, surfaced as the indicator dot in the diff --git a/src/components/nowPlaying/AlbumCard.tsx b/src/features/nowPlaying/components/AlbumCard.tsx similarity index 94% rename from src/components/nowPlaying/AlbumCard.tsx rename to src/features/nowPlaying/components/AlbumCard.tsx index 544397ce..40bf21a0 100644 --- a/src/components/nowPlaying/AlbumCard.tsx +++ b/src/features/nowPlaying/components/AlbumCard.tsx @@ -1,9 +1,9 @@ import React, { memo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Disc3, ExternalLink, Star } from 'lucide-react'; -import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes'; -import { formatTotalDuration } from '../../utils/componentHelpers/nowPlayingHelpers'; -import { formatTrackTime } from '../../utils/format/formatDuration'; +import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes'; +import { formatTotalDuration } from '@/utils/componentHelpers/nowPlayingHelpers'; +import { formatTrackTime } from '@/utils/format/formatDuration'; interface AlbumCardProps { album: SubsonicAlbum | null; diff --git a/src/components/nowPlaying/ArtistCard.test.tsx b/src/features/nowPlaying/components/ArtistCard.test.tsx similarity index 96% rename from src/components/nowPlaying/ArtistCard.test.tsx rename to src/features/nowPlaying/components/ArtistCard.test.tsx index 490c8ce2..4ddd8469 100644 --- a/src/components/nowPlaying/ArtistCard.test.tsx +++ b/src/features/nowPlaying/components/ArtistCard.test.tsx @@ -7,8 +7,8 @@ import { describe, it, expect, vi } from 'vitest'; import { fireEvent } from '@testing-library/react'; import { renderWithProviders } from '@/test/helpers/renderWithProviders'; -import ArtistCard from './ArtistCard'; -import type { SubsonicArtistInfo } from '../../api/subsonicTypes'; +import ArtistCard from '@/features/nowPlaying/components/ArtistCard'; +import type { SubsonicArtistInfo } from '@/api/subsonicTypes'; const infoWithImage: SubsonicArtistInfo = { biography: 'Some bio text here.', diff --git a/src/components/nowPlaying/ArtistCard.tsx b/src/features/nowPlaying/components/ArtistCard.tsx similarity index 97% rename from src/components/nowPlaying/ArtistCard.tsx rename to src/features/nowPlaying/components/ArtistCard.tsx index 84823e9e..52aa9dd5 100644 --- a/src/components/nowPlaying/ArtistCard.tsx +++ b/src/features/nowPlaying/components/ArtistCard.tsx @@ -1,8 +1,8 @@ import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ExternalLink } from 'lucide-react'; -import type { SubsonicArtistInfo } from '../../api/subsonicTypes'; -import { isRealArtistImage, sanitizeHtml } from '../../utils/componentHelpers/nowPlayingHelpers'; +import type { SubsonicArtistInfo } from '@/api/subsonicTypes'; +import { isRealArtistImage, sanitizeHtml } from '@/utils/componentHelpers/nowPlayingHelpers'; import CachedImage from '@/ui/CachedImage'; export interface ArtistCardTab { diff --git a/src/components/nowPlaying/CreditsCard.tsx b/src/features/nowPlaying/components/CreditsCard.tsx similarity index 91% rename from src/components/nowPlaying/CreditsCard.tsx rename to src/features/nowPlaying/components/CreditsCard.tsx index 3fb6b02a..082b4555 100644 --- a/src/components/nowPlaying/CreditsCard.tsx +++ b/src/features/nowPlaying/components/CreditsCard.tsx @@ -1,6 +1,6 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; -import type { ContributorRow } from '../../utils/componentHelpers/nowPlayingHelpers'; +import type { ContributorRow } from '@/utils/componentHelpers/nowPlayingHelpers'; interface CreditsCardProps { rows: ContributorRow[]; } diff --git a/src/components/nowPlaying/DiscographyCard.tsx b/src/features/nowPlaying/components/DiscographyCard.tsx similarity index 93% rename from src/components/nowPlaying/DiscographyCard.tsx rename to src/features/nowPlaying/components/DiscographyCard.tsx index 7082f392..e0dbd3fb 100644 --- a/src/components/nowPlaying/DiscographyCard.tsx +++ b/src/features/nowPlaying/components/DiscographyCard.tsx @@ -1,9 +1,9 @@ import React, { memo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Disc3, ExternalLink, Music } from 'lucide-react'; -import type { SubsonicAlbum } from '../../api/subsonicTypes'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; -import { COVER_DENSE_RAIL_CELL_CSS_PX } from '../../cover/layoutSizes'; +import type { SubsonicAlbum } from '@/api/subsonicTypes'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { COVER_DENSE_RAIL_CELL_CSS_PX } from '@/cover/layoutSizes'; interface DiscographyCardProps { artistId?: string; diff --git a/src/components/nowPlaying/Hero.tsx b/src/features/nowPlaying/components/Hero.tsx similarity index 93% rename from src/components/nowPlaying/Hero.tsx rename to src/features/nowPlaying/components/Hero.tsx index fa40c23c..2639bd93 100644 --- a/src/components/nowPlaying/Hero.tsx +++ b/src/features/nowPlaying/components/Hero.tsx @@ -1,15 +1,15 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Headphones, Heart, MicVocal, Music, Star } from 'lucide-react'; -import { CoverArtImage } from '../../cover/CoverArtImage'; -import type { CoverArtRef } from '../../cover/types'; -import type { ArtistStats, TrackStats } from '../../music-network'; -import type { SubsonicOpenArtistRef } from '../../api/subsonicTypes'; -import { OpenArtistRefInline } from '../OpenArtistRefInline'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import { useEnrichmentPrimaryLabel } from '../../hooks/useEnrichmentPrimaryLabel'; -import { useEnrichmentPrimaryIcon } from '../../hooks/useEnrichmentPrimaryIcon'; -import { renderPresetIcon } from '../settings/musicNetwork/presetIcon'; +import { CoverArtImage } from '@/cover/CoverArtImage'; +import type { CoverArtRef } from '@/cover/types'; +import type { ArtistStats, TrackStats } from '@/music-network'; +import type { SubsonicOpenArtistRef } from '@/api/subsonicTypes'; +import { OpenArtistRefInline } from '@/components/OpenArtistRefInline'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import { useEnrichmentPrimaryLabel } from '@/hooks/useEnrichmentPrimaryLabel'; +import { useEnrichmentPrimaryIcon } from '@/hooks/useEnrichmentPrimaryIcon'; +import { renderPresetIcon } from '@/components/settings/musicNetwork/presetIcon'; interface HeroProps { track: { title: string; artist: string; album: string; year?: number; diff --git a/src/components/NowPlayingDropdown.tsx b/src/features/nowPlaying/components/NowPlayingDropdown.tsx similarity index 97% rename from src/components/NowPlayingDropdown.tsx rename to src/features/nowPlaying/components/NowPlayingDropdown.tsx index 70cccf74..f0971418 100644 --- a/src/components/NowPlayingDropdown.tsx +++ b/src/features/nowPlaying/components/NowPlayingDropdown.tsx @@ -1,12 +1,12 @@ -import { TrackCoverArtImage } from '../cover/TrackCoverArtImage'; -import { getNowPlaying } from '../api/subsonicScrobble'; -import type { SubsonicNowPlaying } from '../api/subsonicTypes'; +import { TrackCoverArtImage } from '@/cover/TrackCoverArtImage'; +import { getNowPlaying } from '@/api/subsonicScrobble'; +import type { SubsonicNowPlaying } from '@/api/subsonicTypes'; import React, { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; import { PlayCircle, Pause, User, Radio, RefreshCw } from 'lucide-react'; -import { nowPlayingPresence } from '../api/nowPlayingPresence'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; +import { nowPlayingPresence } from '@/features/nowPlaying/api/nowPlayingPresence'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; diff --git a/src/components/NowPlayingInfo.tsx b/src/features/nowPlaying/components/NowPlayingInfo.tsx similarity index 96% rename from src/components/NowPlayingInfo.tsx rename to src/features/nowPlaying/components/NowPlayingInfo.tsx index 25e1474f..976614f4 100644 --- a/src/components/NowPlayingInfo.tsx +++ b/src/features/nowPlaying/components/NowPlayingInfo.tsx @@ -1,17 +1,17 @@ -import { getSongForServer } from '../api/subsonicLibrary'; -import { getArtistInfoForServer } from '../api/subsonicArtists'; -import type { SubsonicArtistInfo, SubsonicSong } from '../api/subsonicTypes'; +import { getSongForServer } from '@/api/subsonicLibrary'; +import { getArtistInfoForServer } from '@/api/subsonicArtists'; +import type { SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes'; import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Info } from 'lucide-react'; import { open as shellOpen } from '@tauri-apps/plugin-shell'; -import { usePlayerStore } from '../store/playerStore'; -import { useAuthStore } from '../store/authStore'; -import { usePlaybackServerId } from '../hooks/usePlaybackServerId'; -import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown'; +import { usePlayerStore } from '@/store/playerStore'; +import { useAuthStore } from '@/store/authStore'; +import { usePlaybackServerId } from '@/hooks/usePlaybackServerId'; +import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown'; import CachedImage from '@/ui/CachedImage'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; -import { primaryTrackArtistRef } from '../utils/playback/trackArtistRefs'; +import { primaryTrackArtistRef } from '@/utils/playback/trackArtistRefs'; const TOUR_LIMIT = 5; const BIO_CLAMP_LINES = 4; diff --git a/src/components/nowPlaying/NpCardWrap.tsx b/src/features/nowPlaying/components/NpCardWrap.tsx similarity index 79% rename from src/components/nowPlaying/NpCardWrap.tsx rename to src/features/nowPlaying/components/NpCardWrap.tsx index 451f952d..05900acc 100644 --- a/src/components/nowPlaying/NpCardWrap.tsx +++ b/src/features/nowPlaying/components/NpCardWrap.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { useDragSource } from '../../contexts/DragDropContext'; -import type { NpCardId } from '../../store/nowPlayingLayoutStore'; +import { useDragSource } from '@/contexts/DragDropContext'; +import type { NpCardId } from '@/features/nowPlaying/store/nowPlayingLayoutStore'; interface NpCardWrapProps { id: NpCardId; diff --git a/src/components/nowPlaying/NpColumnEl.tsx b/src/features/nowPlaying/components/NpColumnEl.tsx similarity index 95% rename from src/components/nowPlaying/NpColumnEl.tsx rename to src/features/nowPlaying/components/NpColumnEl.tsx index a3382d4c..715059f9 100644 --- a/src/components/nowPlaying/NpColumnEl.tsx +++ b/src/features/nowPlaying/components/NpColumnEl.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react'; -import type { NpCardId, NpColumn } from '../../store/nowPlayingLayoutStore'; +import type { NpCardId, NpColumn } from '@/features/nowPlaying/store/nowPlayingLayoutStore'; interface NpColumnProps { col: NpColumn; diff --git a/src/components/nowPlaying/RadioView.tsx b/src/features/nowPlaying/components/RadioView.tsx similarity index 97% rename from src/components/nowPlaying/RadioView.tsx rename to src/features/nowPlaying/components/RadioView.tsx index fb7ed9d2..84f06bfd 100644 --- a/src/components/nowPlaying/RadioView.tsx +++ b/src/features/nowPlaying/components/RadioView.tsx @@ -2,8 +2,8 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Cast, Clock, Radio, SkipForward, Users } from 'lucide-react'; import type { useRadioMetadata } from '@/features/radio'; -import { usePlayerStore } from '../../store/playerStore'; -import { formatTrackTime } from '../../utils/format/formatDuration'; +import { usePlayerStore } from '@/store/playerStore'; +import { formatTrackTime } from '@/utils/format/formatDuration'; type NonNullStoreField> = NonNullable[K]>; diff --git a/src/components/nowPlaying/TopSongsCard.tsx b/src/features/nowPlaying/components/TopSongsCard.tsx similarity index 94% rename from src/components/nowPlaying/TopSongsCard.tsx rename to src/features/nowPlaying/components/TopSongsCard.tsx index 6975af3f..aa6344d6 100644 --- a/src/components/nowPlaying/TopSongsCard.tsx +++ b/src/features/nowPlaying/components/TopSongsCard.tsx @@ -1,8 +1,8 @@ import React, { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { ExternalLink, Play, TrendingUp } from 'lucide-react'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import { formatTrackTime } from '../../utils/format/formatDuration'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { formatTrackTime } from '@/utils/format/formatDuration'; interface TopSongsCardProps { artistName: string; diff --git a/src/components/nowPlaying/TourCard.tsx b/src/features/nowPlaying/components/TourCard.tsx similarity index 97% rename from src/components/nowPlaying/TourCard.tsx rename to src/features/nowPlaying/components/TourCard.tsx index abc76f10..bdfa5630 100644 --- a/src/components/nowPlaying/TourCard.tsx +++ b/src/features/nowPlaying/components/TourCard.tsx @@ -2,8 +2,8 @@ import React, { memo, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Calendar, Info } from 'lucide-react'; import { open as shellOpen } from '@tauri-apps/plugin-shell'; -import type { BandsintownEvent } from '../../api/bandsintown'; -import { isoToParts } from '../../utils/componentHelpers/nowPlayingHelpers'; +import type { BandsintownEvent } from '@/api/bandsintown'; +import { isoToParts } from '@/utils/componentHelpers/nowPlayingHelpers'; interface TourCardProps { artistName: string; diff --git a/src/hooks/useNowPlayingFetchers.test.ts b/src/features/nowPlaying/hooks/useNowPlayingFetchers.test.ts similarity index 95% rename from src/hooks/useNowPlayingFetchers.test.ts rename to src/features/nowPlaying/hooks/useNowPlayingFetchers.test.ts index 95ebe17e..917fb02a 100644 --- a/src/hooks/useNowPlayingFetchers.test.ts +++ b/src/features/nowPlaying/hooks/useNowPlayingFetchers.test.ts @@ -11,20 +11,20 @@ */ import { renderHook, act, waitFor } from '@testing-library/react'; 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('../api/subsonicLibrary'); -vi.mock('../api/bandsintown'); -vi.mock('../utils/network/subsonicNetworkGuard', () => ({ +vi.mock('@/api/subsonicArtists'); +vi.mock('@/api/subsonicLibrary'); +vi.mock('@/api/bandsintown'); +vi.mock('@/utils/network/subsonicNetworkGuard', () => ({ shouldAttemptSubsonicForServer: vi.fn(() => true), })); -import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard'; -import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '../api/subsonicArtists'; -import { getAlbumForServer, getSongForServer } from '../api/subsonicLibrary'; -import { fetchBandsintownEvents } from '../api/bandsintown'; -import { useNowPlayingFetchers, type NowPlayingFetchersDeps } from './useNowPlayingFetchers'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { getArtistForServer, getArtistInfoForServer, getTopSongsForServer } from '@/api/subsonicArtists'; +import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary'; +import { fetchBandsintownEvents } from '@/api/bandsintown'; +import { useNowPlayingFetchers, type NowPlayingFetchersDeps } from '@/features/nowPlaying/hooks/useNowPlayingFetchers'; // Resolved return shapes of the mocked API calls — used to cast deliberately // partial test fixtures without `any`. diff --git a/src/hooks/useNowPlayingFetchers.ts b/src/features/nowPlaying/hooks/useNowPlayingFetchers.ts similarity index 96% rename from src/hooks/useNowPlayingFetchers.ts rename to src/features/nowPlaying/hooks/useNowPlayingFetchers.ts index 93cbf5fa..3404d7db 100644 --- a/src/hooks/useNowPlayingFetchers.ts +++ b/src/features/nowPlaying/hooks/useNowPlayingFetchers.ts @@ -1,13 +1,13 @@ import { useEffect, useState } from 'react'; -import { getArtistInfoForServer } from '../api/subsonicArtists'; -import type { SubsonicAlbum, SubsonicArtistInfo, SubsonicSong } from '../api/subsonicTypes'; -import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSongs } from '../utils/library/nowPlayingMetadataResolve'; -import { fetchBandsintownEvents, type BandsintownEvent } from '../api/bandsintown'; -import type { ArtistStats, TrackStats } from '../music-network'; -import { getMusicNetworkRuntimeOrNull } from '../music-network'; -import { makeCache } from '../utils/cache/nowPlayingCache'; -import { shouldAttemptSubsonicForServer } from '../utils/network/subsonicNetworkGuard'; -import { useConnectionStatus } from './useConnectionStatus'; +import { getArtistInfoForServer } from '@/api/subsonicArtists'; +import type { SubsonicAlbum, SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes'; +import { resolveNpAlbum, resolveNpDiscography, resolveNpSongMeta, resolveNpTopSongs } from '@/features/nowPlaying/utils/nowPlayingMetadataResolve'; +import { fetchBandsintownEvents, type BandsintownEvent } from '@/api/bandsintown'; +import type { ArtistStats, TrackStats } from '@/music-network'; +import { getMusicNetworkRuntimeOrNull } from '@/music-network'; +import { makeCache } from '@/utils/cache/nowPlayingCache'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { useConnectionStatus } from '@/hooks/useConnectionStatus'; // Module-level TTL caches (shared across mounts) const songMetaCache = makeCache(); diff --git a/src/hooks/useNowPlayingPrewarm.test.ts b/src/features/nowPlaying/hooks/useNowPlayingPrewarm.test.ts similarity index 79% rename from src/hooks/useNowPlayingPrewarm.test.ts rename to src/features/nowPlaying/hooks/useNowPlayingPrewarm.test.ts index 73e01172..71a2444e 100644 --- a/src/hooks/useNowPlayingPrewarm.test.ts +++ b/src/features/nowPlaying/hooks/useNowPlayingPrewarm.test.ts @@ -1,17 +1,17 @@ import { renderHook, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { coverCacheEnsure, coverCachePeekBatch } from '../api/coverCache'; -import { coverIndexKeyFromRef } from '../cover/storageKeys'; -import { useNowPlayingPrewarm } from './useNowPlayingPrewarm'; -import { prewarmNowPlayingFetchers } from './useNowPlayingFetchers'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; -import { makeTrack } from '../test/helpers/factories'; -import { resetAllStores } from '../test/helpers/storeReset'; -import { toQueueItemRefs } from '../utils/library/queueItemRef'; +import { coverCacheEnsure, coverCachePeekBatch } from '@/api/coverCache'; +import { coverIndexKeyFromRef } from '@/cover/storageKeys'; +import { useNowPlayingPrewarm } from '@/features/nowPlaying/hooks/useNowPlayingPrewarm'; +import { prewarmNowPlayingFetchers } from '@/features/nowPlaying/hooks/useNowPlayingFetchers'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { makeTrack } from '@/test/helpers/factories'; +import { resetAllStores } from '@/test/helpers/storeReset'; +import { toQueueItemRefs } from '@/utils/library/queueItemRef'; -vi.mock('../api/coverCache', async importOriginal => { - const actual = await importOriginal(); +vi.mock('@/api/coverCache', async importOriginal => { + const actual = await importOriginal(); return { ...actual, coverCachePeekBatch: vi.fn(async () => ({})), @@ -19,7 +19,7 @@ vi.mock('../api/coverCache', async importOriginal => { }; }); -vi.mock('./useNowPlayingFetchers', () => ({ +vi.mock('@/features/nowPlaying/hooks/useNowPlayingFetchers', () => ({ prewarmNowPlayingFetchers: vi.fn(async () => undefined), })); diff --git a/src/hooks/useNowPlayingPrewarm.ts b/src/features/nowPlaying/hooks/useNowPlayingPrewarm.ts similarity index 78% rename from src/hooks/useNowPlayingPrewarm.ts rename to src/features/nowPlaying/hooks/useNowPlayingPrewarm.ts index 88bb6240..e698b38f 100644 --- a/src/hooks/useNowPlayingPrewarm.ts +++ b/src/features/nowPlaying/hooks/useNowPlayingPrewarm.ts @@ -1,18 +1,18 @@ import { useEffect } from 'react'; -import { coverCacheEnsure, coverCachePeekBatch } from '../api/coverCache'; -import { albumCoverRef } from '../cover/ref'; -import { resolvePlaybackCoverScope } from '../cover/ref'; -import { resolveTrackCoverRefFromLibrary } from '../cover/resolveEntryLibrary'; -import { getDiskSrc, rememberDiskSrc } from '../cover/diskSrcCache'; -import { coverStorageKeyFromRef } from '../cover/storageKeys'; -import { resolveCoverDisplayTier } from '../cover/tiers'; -import { coverArtIdFromRadio } from '../cover/ids'; -import type { CoverArtRef } from '../cover/types'; -import { prewarmNowPlayingFetchers } from './useNowPlayingFetchers'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; -import { usePlaybackServerId } from './usePlaybackServerId'; -import { primaryTrackArtistRef } from '../utils/playback/trackArtistRefs'; +import { coverCacheEnsure, coverCachePeekBatch } from '@/api/coverCache'; +import { albumCoverRef } from '@/cover/ref'; +import { resolvePlaybackCoverScope } from '@/cover/ref'; +import { resolveTrackCoverRefFromLibrary } from '@/cover/resolveEntryLibrary'; +import { getDiskSrc, rememberDiskSrc } from '@/cover/diskSrcCache'; +import { coverStorageKeyFromRef } from '@/cover/storageKeys'; +import { resolveCoverDisplayTier } from '@/cover/tiers'; +import { coverArtIdFromRadio } from '@/cover/ids'; +import type { CoverArtRef } from '@/cover/types'; +import { prewarmNowPlayingFetchers } from '@/features/nowPlaying/hooks/useNowPlayingFetchers'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { usePlaybackServerId } from '@/hooks/usePlaybackServerId'; +import { primaryTrackArtistRef } from '@/utils/playback/trackArtistRefs'; const NOW_PLAYING_COVER_CSS_PX = 800; diff --git a/src/hooks/useNowPlayingStarLove.ts b/src/features/nowPlaying/hooks/useNowPlayingStarLove.ts similarity index 88% rename from src/hooks/useNowPlayingStarLove.ts rename to src/features/nowPlaying/hooks/useNowPlayingStarLove.ts index 5eef3b76..63578bda 100644 --- a/src/hooks/useNowPlayingStarLove.ts +++ b/src/features/nowPlaying/hooks/useNowPlayingStarLove.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect, useState } from 'react'; -import { queueSongStar } from '../store/pendingStarSync'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import type { Track } from '../store/playerStoreTypes'; -import type { TrackStats } from '../music-network'; -import { getMusicNetworkRuntime } from '../music-network'; +import { queueSongStar } from '@/store/pendingStarSync'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { Track } from '@/store/playerStoreTypes'; +import type { TrackStats } from '@/music-network'; +import { getMusicNetworkRuntime } from '@/music-network'; export interface NowPlayingStarLoveDeps { currentTrack: Pick | null; diff --git a/src/features/nowPlaying/index.ts b/src/features/nowPlaying/index.ts new file mode 100644 index 00000000..16489767 --- /dev/null +++ b/src/features/nowPlaying/index.ts @@ -0,0 +1,12 @@ +/** + * Now Playing feature — the Now Playing page (album/artist/credits/discography/ + * tour cards, hero), its layout store and index-first metadata resolvers, the + * topbar "Who is listening?" dropdown, and the queue-side track info panel. + * + * The page itself is lazy-loaded via the deep path `pages/NowPlaying`. + * `ArtistCard` is also reused by the Artist Detail page. + */ +export { default as NowPlayingDropdown } from './components/NowPlayingDropdown'; +export { default as NowPlayingInfo } from './components/NowPlayingInfo'; +export { default as ArtistCard } from './components/ArtistCard'; +export { useNowPlayingPrewarm } from './hooks/useNowPlayingPrewarm'; diff --git a/src/pages/NowPlaying.tsx b/src/features/nowPlaying/pages/NowPlaying.tsx similarity index 90% rename from src/pages/NowPlaying.tsx rename to src/features/nowPlaying/pages/NowPlaying.tsx index 3179cecc..49fb1d3a 100644 --- a/src/pages/NowPlaying.tsx +++ b/src/features/nowPlaying/pages/NowPlaying.tsx @@ -1,45 +1,45 @@ -import { useCoverArt } from '../cover/useCoverArt'; -import { albumCoverRef } from '../cover/ref'; -import { usePlaybackTrackCoverRef } from '../cover/useLibraryCoverRef'; -import { coverArtIdFromRadio } from '../cover/ids'; -import type { SubsonicArtistInfo, SubsonicSong } from '../api/subsonicTypes'; +import { useCoverArt } from '@/cover/useCoverArt'; +import { albumCoverRef } from '@/cover/ref'; +import { usePlaybackTrackCoverRef } from '@/cover/useLibraryCoverRef'; +import { coverArtIdFromRadio } from '@/cover/ids'; +import type { SubsonicArtistInfo, SubsonicSong } from '@/api/subsonicTypes'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; -import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate'; -import { usePlaybackServerId } from '../hooks/usePlaybackServerId'; +import { usePlaybackLibraryNavigate } from '@/hooks/usePlaybackLibraryNavigate'; +import { usePlaybackServerId } from '@/hooks/usePlaybackServerId'; import { useTranslation } from 'react-i18next'; import { Music, EyeOff, LayoutGrid, RotateCcw, Eye } from 'lucide-react'; -import { usePlayerStore } from '../store/playerStore'; -import { useAuthStore } from '../store/authStore'; -import { useLyricsStore } from '../store/lyricsStore'; -import { songToTrack } from '../utils/playback/songToTrack'; +import { usePlayerStore } from '@/store/playerStore'; +import { useAuthStore } from '@/store/authStore'; +import { useLyricsStore } from '@/store/lyricsStore'; +import { songToTrack } from '@/utils/playback/songToTrack'; import { useRadioMetadata } from '@/features/radio'; -import { useDragDrop } from '../contexts/DragDropContext'; +import { useDragDrop } from '@/contexts/DragDropContext'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; import { useNpLayoutStore, NP_CARD_IDS, type NpCardId, type NpColumn, -} from '../store/nowPlayingLayoutStore'; +} from '@/features/nowPlaying/store/nowPlayingLayoutStore'; // ─── Helpers ────────────────────────────────────────────────────────────────── import { buildContributorRows, -} from '../utils/componentHelpers/nowPlayingHelpers'; -import NpCardWrap from '../components/nowPlaying/NpCardWrap'; -import NpColumnEl from '../components/nowPlaying/NpColumnEl'; -import RadioView from '../components/nowPlaying/RadioView'; -import Hero from '../components/nowPlaying/Hero'; -import ArtistCard from '../components/nowPlaying/ArtistCard'; -import AlbumCard from '../components/nowPlaying/AlbumCard'; -import TopSongsCard from '../components/nowPlaying/TopSongsCard'; -import CreditsCard from '../components/nowPlaying/CreditsCard'; -import TourCard from '../components/nowPlaying/TourCard'; -import DiscographyCard from '../components/nowPlaying/DiscographyCard'; -import { useNowPlayingFetchers } from '../hooks/useNowPlayingFetchers'; -import { useNowPlayingStarLove } from '../hooks/useNowPlayingStarLove'; -import { useArtistInfoBatch } from '../hooks/useArtistInfoBatch'; -import { primaryTrackArtistRef, resolveTrackArtistRefs } from '../utils/playback/trackArtistRefs'; -import type { ArtistCardTab } from '../components/nowPlaying/ArtistCard'; +} from '@/utils/componentHelpers/nowPlayingHelpers'; +import NpCardWrap from '@/features/nowPlaying/components/NpCardWrap'; +import NpColumnEl from '@/features/nowPlaying/components/NpColumnEl'; +import RadioView from '@/features/nowPlaying/components/RadioView'; +import Hero from '@/features/nowPlaying/components/Hero'; +import ArtistCard from '@/features/nowPlaying/components/ArtistCard'; +import AlbumCard from '@/features/nowPlaying/components/AlbumCard'; +import TopSongsCard from '@/features/nowPlaying/components/TopSongsCard'; +import CreditsCard from '@/features/nowPlaying/components/CreditsCard'; +import TourCard from '@/features/nowPlaying/components/TourCard'; +import DiscographyCard from '@/features/nowPlaying/components/DiscographyCard'; +import { useNowPlayingFetchers } from '@/features/nowPlaying/hooks/useNowPlayingFetchers'; +import { useNowPlayingStarLove } from '@/features/nowPlaying/hooks/useNowPlayingStarLove'; +import { useArtistInfoBatch } from '@/hooks/useArtistInfoBatch'; +import { primaryTrackArtistRef, resolveTrackArtistRefs } from '@/utils/playback/trackArtistRefs'; +import type { ArtistCardTab } from '@/features/nowPlaying/components/ArtistCard'; // ─── Main Page ──────────────────────────────────────────────────────────────── diff --git a/src/store/nowPlayingLayoutStore.ts b/src/features/nowPlaying/store/nowPlayingLayoutStore.ts similarity index 100% rename from src/store/nowPlayingLayoutStore.ts rename to src/features/nowPlaying/store/nowPlayingLayoutStore.ts diff --git a/src/utils/library/nowPlayingMetadataResolve.test.ts b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.test.ts similarity index 99% rename from src/utils/library/nowPlayingMetadataResolve.test.ts rename to src/features/nowPlaying/utils/nowPlayingMetadataResolve.test.ts index b9a0df86..7155d935 100644 --- a/src/utils/library/nowPlayingMetadataResolve.test.ts +++ b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.test.ts @@ -22,7 +22,7 @@ import { resolveNpDiscography, resolveNpTopSongs, resolveNpSongMeta, -} from './nowPlayingMetadataResolve'; +} from '@/features/nowPlaying/utils/nowPlayingMetadataResolve'; const guard = vi.mocked(shouldAttemptSubsonicForServer); diff --git a/src/utils/library/nowPlayingMetadataResolve.ts b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts similarity index 87% rename from src/utils/library/nowPlayingMetadataResolve.ts rename to src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts index 488c5a73..6ccd10f8 100644 --- a/src/utils/library/nowPlayingMetadataResolve.ts +++ b/src/features/nowPlaying/utils/nowPlayingMetadataResolve.ts @@ -16,14 +16,14 @@ * `artistInfo` (bio / similar) has no index source and stays network-only — it * is intentionally absent here. */ -import { libraryGetTrack, libraryGetTracksByAlbum } from '../../api/library'; -import { getArtistForServer, getTopSongsForServer } from '../../api/subsonicArtists'; -import { getAlbumForServer, getSongForServer } from '../../api/subsonicLibrary'; -import type { SubsonicAlbum, SubsonicSong } from '../../api/subsonicTypes'; -import { shouldAttemptSubsonicForServer } from '../network/subsonicNetworkGuard'; -import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '../offline/offlineLibraryIndexLoad'; -import { trackToSong } from './advancedSearchLocal'; -import { libraryIsReady } from './libraryReady'; +import { libraryGetTrack, libraryGetTracksByAlbum } from '@/api/library'; +import { getArtistForServer, getTopSongsForServer } from '@/api/subsonicArtists'; +import { getAlbumForServer, getSongForServer } from '@/api/subsonicLibrary'; +import type { SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes'; +import { shouldAttemptSubsonicForServer } from '@/utils/network/subsonicNetworkGuard'; +import { loadAlbumFromLibraryIndex, loadArtistFromLibraryIndex } from '@/utils/offline/offlineLibraryIndexLoad'; +import { trackToSong } from '@/utils/library/advancedSearchLocal'; +import { libraryIsReady } from '@/utils/library/libraryReady'; const TOP_SONGS_LIMIT = 5; diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 92ea7cb2..1ba13495 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -28,7 +28,7 @@ import { import ArtistDetailHero from '../components/artistDetail/ArtistDetailHero'; import ArtistDetailTopTracks from '../components/artistDetail/ArtistDetailTopTracks'; import ArtistDetailSimilarArtists from '../components/artistDetail/ArtistDetailSimilarArtists'; -import ArtistCard from '../components/nowPlaying/ArtistCard'; +import { ArtistCard } from '@/features/nowPlaying'; import LosslessModeBanner from '../components/LosslessModeBanner'; import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { albumGridWarmCovers, COVER_DENSE_GRID_MIN_CELL_CSS_PX, GRID_COVER_WARM_LIMIT } from '../cover/layoutSizes';