From 2f4303ecc8c13e89f17eb83501dd0512153ab6b4 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:12:27 +0200 Subject: [PATCH] refactor(home): co-locate Mainstage/home feature into features/home --- src/app/AppRoutes.tsx | 2 +- src/components/tracks/TracksPageChrome.tsx | 2 +- .../home}/components/BecauseYouLikeRail.tsx | 16 ++++++------ src/{ => features/home}/components/Hero.tsx | 20 +++++++------- .../home}/components/SongRail.tsx | 4 +-- src/features/home/index.ts | 11 ++++++++ src/{ => features/home}/pages/Home.tsx | 26 +++++++++---------- .../home}/store/becauseYouLikeCache.test.ts | 2 +- .../home}/store/becauseYouLikeCache.ts | 0 .../home}/store/homeFeedCache.ts | 0 src/{ => features/home}/store/homeStore.ts | 0 .../settings/components/HomeCustomizer.tsx | 2 +- .../components/PersonalisationTab.tsx | 2 +- 13 files changed, 49 insertions(+), 38 deletions(-) rename src/{ => features/home}/components/BecauseYouLikeRail.tsx (98%) rename src/{ => features/home}/components/Hero.tsx (97%) rename src/{ => features/home}/components/SongRail.tsx (98%) create mode 100644 src/features/home/index.ts rename src/{ => features/home}/pages/Home.tsx (96%) rename src/{ => features/home}/store/becauseYouLikeCache.test.ts (92%) rename src/{ => features/home}/store/becauseYouLikeCache.ts (100%) rename src/{ => features/home}/store/homeFeedCache.ts (100%) rename src/{ => features/home}/store/homeStore.ts (100%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 5e62235e..f74c6932 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -9,7 +9,7 @@ import { resolveStartRoute } from '@/features/sidebar'; // Route-level lazy loading: keeps the non-page graph (shell, player, stores) in // the entry chunk; each page is fetched when its route is first visited. -const Home = lazy(() => import('../pages/Home')); +const Home = lazy(() => import('@/features/home/pages/Home')); const Albums = lazy(() => import('@/features/album/pages/Albums')); const Artists = lazy(() => import('@/features/artist/pages/Artists')); const ArtistDetail = lazy(() => import('@/features/artist/pages/ArtistDetail')); diff --git a/src/components/tracks/TracksPageChrome.tsx b/src/components/tracks/TracksPageChrome.tsx index a44b79fe..bb82e98b 100644 --- a/src/components/tracks/TracksPageChrome.tsx +++ b/src/components/tracks/TracksPageChrome.tsx @@ -7,7 +7,7 @@ import { Play, ListPlus, RefreshCw, Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useAuthStore } from '../../store/authStore'; import { usePlayerStore } from '@/features/playback/store/playerStore'; -import SongRail from '../SongRail'; +import { SongRail } from '@/features/home'; import { playSongNow } from '@/features/playback/utils/playback/playSong'; import { ndListSongs, ndInvalidateSongsCache } from '@/lib/api/navidromeBrowse'; import { usePerfProbeFlags } from '../../utils/perf/perfFlags'; diff --git a/src/components/BecauseYouLikeRail.tsx b/src/features/home/components/BecauseYouLikeRail.tsx similarity index 98% rename from src/components/BecauseYouLikeRail.tsx rename to src/features/home/components/BecauseYouLikeRail.tsx index f2578ece..2988ec45 100644 --- a/src/components/BecauseYouLikeRail.tsx +++ b/src/features/home/components/BecauseYouLikeRail.tsx @@ -8,21 +8,21 @@ import React, { memo, useEffect, useLayoutEffect, useMemo, useRef, useState } fr import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Play, ListPlus, Music } from 'lucide-react'; -import { useAlbumCoverRef } from '../cover/useLibraryCoverRef'; -import { useLibraryCoverPrefetch } from '../cover/useLibraryCoverPrefetch'; -import { coverImgSrc } from '../cover/imgSrc'; -import { useCoverArt } from '../cover/useCoverArt'; -import { primeAlbumCoversForDisplay } from '../cover/warmDiskPeek'; +import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef'; +import { useLibraryCoverPrefetch } from '@/cover/useLibraryCoverPrefetch'; +import { coverImgSrc } from '@/cover/imgSrc'; +import { useCoverArt } from '@/cover/useCoverArt'; +import { primeAlbumCoversForDisplay } from '@/cover/warmDiskPeek'; import { readBecauseYouLikeCache, writeBecauseYouLikeCache, type BecauseYouLikeAnchor, -} from '../store/becauseYouLikeCache'; +} from '@/features/home/store/becauseYouLikeCache'; import { usePlayerStore } from '@/features/playback/store/playerStore'; -import { useAuthStore } from '../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { playAlbum, playAlbumShuffled } from '@/features/playback/utils/playback/playAlbum'; import { useLongPressAction } from '@/lib/hooks/useLongPressAction'; -import { LongPressWaveOverlay } from './LongPressWaveOverlay'; +import { LongPressWaveOverlay } from '@/components/LongPressWaveOverlay'; import { formatHumanHoursMinutes } from '@/lib/format/formatHumanDuration'; import { AlbumRow } from '@/features/album'; import { albumArtistDisplayName } from '@/features/album'; diff --git a/src/components/Hero.tsx b/src/features/home/components/Hero.tsx similarity index 97% rename from src/components/Hero.tsx rename to src/features/home/components/Hero.tsx index da5498c5..157b6ed0 100644 --- a/src/components/Hero.tsx +++ b/src/features/home/components/Hero.tsx @@ -5,23 +5,23 @@ import { songToTrack } from '@/lib/media/songToTrack'; import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { useNavigateToAlbum } from '@/features/album'; import { Play, ListPlus, ChevronLeft, ChevronRight } from 'lucide-react'; -import { CoverArtImage } from '../cover/CoverArtImage'; -import { useAlbumCoverRef } from '../cover/useLibraryCoverRef'; -import { useArtistBanner, useArtistFanart } from '../cover/useArtistFanart'; -import { useCoverArt } from '../cover/useCoverArt'; -import { useHeroBackdrop } from '../cover/useHeroBackdrop'; +import { CoverArtImage } from '@/cover/CoverArtImage'; +import { useAlbumCoverRef } from '@/cover/useLibraryCoverRef'; +import { useArtistBanner, useArtistFanart } from '@/cover/useArtistFanart'; +import { useCoverArt } from '@/cover/useCoverArt'; +import { useHeroBackdrop } from '@/cover/useHeroBackdrop'; import { useCachedUrl } from '@/ui/CachedImage'; import { usePlayerStore } from '@/features/playback/store/playerStore'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '@/lib/hooks/useIsMobile'; import { useWindowVisibility } from '@/lib/hooks/useWindowVisibility'; -import { useAuthStore } from '../store/authStore'; -import { useThemeStore } from '../store/themeStore'; -import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; +import { useAuthStore } from '@/store/authStore'; +import { useThemeStore } from '@/store/themeStore'; +import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '@/utils/mix/mixRatingFilter'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; import { playAlbum, playAlbumShuffled } from '@/features/playback/utils/playback/playAlbum'; import { useLongPressAction } from '@/lib/hooks/useLongPressAction'; -import { LongPressWaveOverlay } from './LongPressWaveOverlay'; +import { LongPressWaveOverlay } from '@/components/LongPressWaveOverlay'; import { albumArtistDisplayName, deriveAlbumArtistRefs } from '@/features/album'; const INTERVAL_MS = 10000; diff --git a/src/components/SongRail.tsx b/src/features/home/components/SongRail.tsx similarity index 98% rename from src/components/SongRail.tsx rename to src/features/home/components/SongRail.tsx index 21f991a1..ec7904d8 100644 --- a/src/components/SongRail.tsx +++ b/src/features/home/components/SongRail.tsx @@ -1,8 +1,8 @@ import type { SubsonicSong } from '@/lib/api/subsonicTypes'; import React, { useRef, useState, useEffect, useMemo } from 'react'; import { ChevronLeft, ChevronRight, RefreshCw } from 'lucide-react'; -import SongCard from './SongCard'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; +import SongCard from '@/components/SongCard'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; import { dedupeById } from '@/lib/util/dedupeById'; interface Props { diff --git a/src/features/home/index.ts b/src/features/home/index.ts new file mode 100644 index 00000000..76a6da08 --- /dev/null +++ b/src/features/home/index.ts @@ -0,0 +1,11 @@ +/** + * Home (Mainstage) feature — the Home landing page, its Hero banner, the song + * rail + "because you like" recommendation rail, the mainstage section config + * store, and the home-feed / because-you-like client caches. The page is + * lazy-loaded by the router via its deep path, so it is not re-exported here. + * + * Cross-feature consumers pull only the section config store (settings' + * mainstage customiser, the tracks-page chrome). + */ +export { useHomeStore, type HomeSectionId, type HomeSectionConfig, DEFAULT_HOME_SECTIONS } from './store/homeStore'; +export { default as SongRail } from './components/SongRail'; diff --git a/src/pages/Home.tsx b/src/features/home/pages/Home.tsx similarity index 96% rename from src/pages/Home.tsx rename to src/features/home/pages/Home.tsx index da6a8062..72ec8f82 100644 --- a/src/pages/Home.tsx +++ b/src/features/home/pages/Home.tsx @@ -3,32 +3,32 @@ import { getAlbumList, getRandomSongs } from '@/lib/api/subsonicLibrary'; import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/lib/api/subsonicTypes'; import { runLocalRandomSongs } from '@/lib/library/browseTextSearch'; import React, { useEffect, useState } from 'react'; -import Hero from '../components/Hero'; +import Hero from '@/features/home/components/Hero'; import { AlbumRow } from '@/features/album'; -import SongRail from '../components/SongRail'; -import BecauseYouLikeRail from '../components/BecauseYouLikeRail'; +import SongRail from '@/features/home/components/SongRail'; +import BecauseYouLikeRail from '@/features/home/components/BecauseYouLikeRail'; import { LosslessAlbumsRail } from '@/features/album'; import { useTranslation } from 'react-i18next'; import { NavLink, useNavigate } from 'react-router-dom'; import { ChevronRight } from 'lucide-react'; -import { useHomeStore } from '../store/homeStore'; -import { useAuthStore } from '../store/authStore'; -import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { bumpPerfCounter } from '../utils/perf/perfTelemetry'; +import { useHomeStore } from '@/features/home/store/homeStore'; +import { useAuthStore } from '@/store/authStore'; +import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '@/utils/mix/mixRatingFilter'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { bumpPerfCounter } from '@/utils/perf/perfTelemetry'; import { dedupeById } from '@/lib/util/dedupeById'; import { shuffleArray } from '@/lib/util/shuffleArray'; -import { useLibraryCoverPrefetch } from '../cover/useLibraryCoverPrefetch'; -import { primeAlbumCoversForDisplay, warmHomeMainstageCovers } from '../cover/warmDiskPeek'; -import { readBecauseYouLikeCache } from '../store/becauseYouLikeCache'; +import { useLibraryCoverPrefetch } from '@/cover/useLibraryCoverPrefetch'; +import { primeAlbumCoversForDisplay, warmHomeMainstageCovers } from '@/cover/warmDiskPeek'; +import { readBecauseYouLikeCache } from '@/features/home/store/becauseYouLikeCache'; import { isHomeFeedSnapshotEmpty, readHomeFeedCache, readHomeFeedCacheStale, writeHomeFeedCache, type HomeFeedSnapshot, -} from '../store/homeFeedCache'; -import { useConnectionStatus } from '../hooks/useConnectionStatus'; +} from '@/features/home/store/homeFeedCache'; +import { useConnectionStatus } from '@/hooks/useConnectionStatus'; import { useOfflineBrowseContext } from '@/features/offline'; import { useOfflineBrowseReloadToken } from '@/features/offline'; import { useDevOfflineBrowseStore } from '@/features/offline'; diff --git a/src/store/becauseYouLikeCache.test.ts b/src/features/home/store/becauseYouLikeCache.test.ts similarity index 92% rename from src/store/becauseYouLikeCache.test.ts rename to src/features/home/store/becauseYouLikeCache.test.ts index 7121f40f..241f26fc 100644 --- a/src/store/becauseYouLikeCache.test.ts +++ b/src/features/home/store/becauseYouLikeCache.test.ts @@ -3,7 +3,7 @@ import { clearBecauseYouLikeCache, readBecauseYouLikeCache, writeBecauseYouLikeCache, -} from './becauseYouLikeCache'; +} from '@/features/home/store/becauseYouLikeCache'; describe('becauseYouLikeCache', () => { it('invalidates when music library filter version changes', () => { diff --git a/src/store/becauseYouLikeCache.ts b/src/features/home/store/becauseYouLikeCache.ts similarity index 100% rename from src/store/becauseYouLikeCache.ts rename to src/features/home/store/becauseYouLikeCache.ts diff --git a/src/store/homeFeedCache.ts b/src/features/home/store/homeFeedCache.ts similarity index 100% rename from src/store/homeFeedCache.ts rename to src/features/home/store/homeFeedCache.ts diff --git a/src/store/homeStore.ts b/src/features/home/store/homeStore.ts similarity index 100% rename from src/store/homeStore.ts rename to src/features/home/store/homeStore.ts diff --git a/src/features/settings/components/HomeCustomizer.tsx b/src/features/settings/components/HomeCustomizer.tsx index 80208ac5..9d2ac695 100644 --- a/src/features/settings/components/HomeCustomizer.tsx +++ b/src/features/settings/components/HomeCustomizer.tsx @@ -1,5 +1,5 @@ import { useTranslation } from 'react-i18next'; -import { useHomeStore, HomeSectionId } from '@/store/homeStore'; +import { useHomeStore, HomeSectionId } from '@/features/home'; export function HomeCustomizer() { const { t } = useTranslation(); diff --git a/src/features/settings/components/PersonalisationTab.tsx b/src/features/settings/components/PersonalisationTab.tsx index 952d08f1..3ce630d1 100644 --- a/src/features/settings/components/PersonalisationTab.tsx +++ b/src/features/settings/components/PersonalisationTab.tsx @@ -3,7 +3,7 @@ import { Disc3, LayoutGrid, ListOrdered, ListTodo, PanelLeft, RotateCcw, Users } import { useArtistLayoutStore } from '@/features/artist'; import { useAuthStore } from '@/store/authStore'; import type { QueueDisplayMode } from '@/store/authStoreTypes'; -import { useHomeStore } from '@/store/homeStore'; +import { useHomeStore } from '@/features/home'; import { usePlayerBarLayoutStore } from '@/store/playerBarLayoutStore'; import { usePlaylistLayoutStore } from '@/features/playlist'; import { useQueueToolbarStore } from '@/store/queueToolbarStore';