From 5fdec6cd2a06cc13f17f2277637061047392cec6 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 17:59:06 +0200 Subject: [PATCH] refactor(genre): co-locate genre browse pages into features/genre --- src/app/AppRoutes.tsx | 4 ++-- .../genre}/hooks/useGenreDetailBrowse.ts | 4 ++-- src/features/genre/index.ts | 12 ++++++++++ .../genre}/pages/GenreDetail.tsx | 24 +++++++++---------- src/{ => features/genre}/pages/Genres.tsx | 8 +++---- 5 files changed, 32 insertions(+), 20 deletions(-) rename src/{ => features/genre}/hooks/useGenreDetailBrowse.ts (96%) create mode 100644 src/features/genre/index.ts rename src/{ => features/genre}/pages/GenreDetail.tsx (92%) rename src/{ => features/genre}/pages/Genres.tsx (95%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 10c7d2a4..736ec20f 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -37,8 +37,8 @@ const LabelAlbums = lazy(() => import('@/features/album/pages/LabelAlbums')); const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage')); const FolderBrowser = lazy(() => import('@/features/folderBrowser/pages/FolderBrowser')); const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio')); -const Genres = lazy(() => import('../pages/Genres')); -const GenreDetail = lazy(() => import('../pages/GenreDetail')); +const Genres = lazy(() => import('@/features/genre/pages/Genres')); +const GenreDetail = lazy(() => import('@/features/genre/pages/GenreDetail')); /** * Index route ("/") = Mainstage. When the user has hidden Mainstage from the diff --git a/src/hooks/useGenreDetailBrowse.ts b/src/features/genre/hooks/useGenreDetailBrowse.ts similarity index 96% rename from src/hooks/useGenreDetailBrowse.ts rename to src/features/genre/hooks/useGenreDetailBrowse.ts index a46893dc..cfe35fd8 100644 --- a/src/hooks/useGenreDetailBrowse.ts +++ b/src/features/genre/hooks/useGenreDetailBrowse.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, type RefObject } from 'react'; import { useLocation, useNavigationType } from 'react-router-dom'; -import { GENRE_DETAIL_INPAGE_SCROLL_VIEWPORT_ID, readInpageScrollTop } from '../constants/appScroll'; +import { GENRE_DETAIL_INPAGE_SCROLL_VIEWPORT_ID, readInpageScrollTop } from '@/constants/appScroll'; import { DEFAULT_ALBUM_BROWSE_RETURN_FILTERS, albumBrowseSortForServer, @@ -12,7 +12,7 @@ import { stashGenreDetailReturnFilters, useAlbumBrowseSessionStore, } from '@/features/album'; -import { shouldRestoreAlbumBrowseSession } from '../utils/navigation/albumDetailNavigation'; +import { shouldRestoreAlbumBrowseSession } from '@/utils/navigation/albumDetailNavigation'; import type { AlbumBrowseScrollSnapshot } from '@/features/album'; /** Genre detail: locked genre filter + leave/restore session (same contract as All Albums). */ diff --git a/src/features/genre/index.ts b/src/features/genre/index.ts new file mode 100644 index 00000000..a51c0bcb --- /dev/null +++ b/src/features/genre/index.ts @@ -0,0 +1,12 @@ +/** + * Genre feature — the genre overview (`Genres`) and per-genre album browse + * (`GenreDetail`) pages plus the genre-detail browse hook. The pages are + * lazy-loaded by the router via their deep paths, so they are not re-exported + * here; nothing else outside the feature consumes its modules. + * + * Stays OUT (library-core / shared, consumed by this feature, not owned): the + * `lib/library/genre*` catalog/query helpers + `genreColor`, the album-side + * `useGenreAlbumBrowse` browse hook (`features/album`), and the + * `genreBrowsePlayback` queue builder (`features/playback`). + */ +export {}; diff --git a/src/pages/GenreDetail.tsx b/src/features/genre/pages/GenreDetail.tsx similarity index 92% rename from src/pages/GenreDetail.tsx rename to src/features/genre/pages/GenreDetail.tsx index 9d03282c..36513878 100644 --- a/src/pages/GenreDetail.tsx +++ b/src/features/genre/pages/GenreDetail.tsx @@ -3,21 +3,21 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { ArrowLeft, Disc3, Play, ListPlus, Loader2 } from 'lucide-react'; import { AlbumCard } from '@/features/album'; -import { LongPressWaveOverlay } from '../components/LongPressWaveOverlay'; -import InpageScrollSentinel from '../components/InpageScrollSentinel'; +import { LongPressWaveOverlay } from '@/components/LongPressWaveOverlay'; +import InpageScrollSentinel from '@/components/InpageScrollSentinel'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; -import { VirtualCardGrid } from '../components/VirtualCardGrid'; -import { GENRE_DETAIL_INPAGE_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; -import { albumGridWarmCovers } from '../cover/layoutSizes'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; +import { GENRE_DETAIL_INPAGE_SCROLL_VIEWPORT_ID } from '@/constants/appScroll'; +import { albumGridWarmCovers } from '@/cover/layoutSizes'; import { useAlbumBrowseScrollSnapshotSync, type AlbumBrowseScrollSnapshot } from '@/features/album'; import { useGenreAlbumBrowse } from '@/features/album'; import { useAlbumBrowseScrollRestore } from '@/features/album'; -import { useGenreDetailBrowse } from '../hooks/useGenreDetailBrowse'; -import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport'; +import { useGenreDetailBrowse } from '@/features/genre/hooks/useGenreDetailBrowse'; +import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport'; import { useLongPressAction } from '@/lib/hooks/useLongPressAction'; -import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight'; -import { useAuthStore } from '../store/authStore'; -import { useLibraryIndexStore } from '../store/libraryIndexStore'; +import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; import { usePlayerStore } from '@/features/playback/store/playerStore'; import { fetchGenreAlbumCount, @@ -28,8 +28,8 @@ import { libraryScopeForServer } from '@/lib/api/subsonicClient'; import { readAlbumBrowseRestore, readAlbumDetailReturnTo, -} from '../utils/navigation/albumDetailNavigation'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; +} from '@/utils/navigation/albumDetailNavigation'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; import { runBulkEnqueue, runBulkPlayAll, runBulkShuffle } from '@/features/playback/utils/playback/runBulkPlay'; export default function GenreDetail() { diff --git a/src/pages/Genres.tsx b/src/features/genre/pages/Genres.tsx similarity index 95% rename from src/pages/Genres.tsx rename to src/features/genre/pages/Genres.tsx index a884cfe6..4dc13b17 100644 --- a/src/pages/Genres.tsx +++ b/src/features/genre/pages/Genres.tsx @@ -3,14 +3,14 @@ import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Tags } from 'lucide-react'; -import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; +import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll'; import { subscribeLibrarySyncIdle } from '@/lib/api/library'; -import { useAuthStore } from '../store/authStore'; -import { useLibraryIndexStore } from '../store/libraryIndexStore'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; import { fetchGenreCatalog, filterGenresWithContent } from '@/features/playback/utils/playback/genreBrowsePlayback'; import { libraryScopeForServer } from '@/lib/api/subsonicClient'; import { peekGenreCatalogCache } from '@/lib/library/genreCatalogCountsCache'; -import { resolveIndexKey } from '../utils/server/serverIndexKey'; +import { resolveIndexKey } from '@/utils/server/serverIndexKey'; import { genreColor } from '@/lib/library/genreColor'; const SCROLL_KEY = 'genres-scroll';