From f443a60c546dd0c7ca5d4a38700c89e77885899b Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Tue, 30 Jun 2026 18:03:43 +0200 Subject: [PATCH] refactor(composers): co-locate composers feature into features/composers --- src/app/AppRoutes.tsx | 4 +-- .../hooks/useComposersBrowseFilters.ts | 6 ++-- .../hooks/useComposersBrowseScrollRestore.ts | 4 +-- .../composers}/hooks/useNavigateToComposer.ts | 2 +- src/features/composers/index.ts | 8 +++++ .../composers}/pages/ComposerDetail.tsx | 20 ++++++------ .../composers}/pages/Composers.tsx | 32 +++++++++---------- .../store/composerBrowseSessionStore.ts | 0 .../search/components/liveSearchScope.ts | 2 +- .../search/hooks/useLiveSearchRouteScope.ts | 2 +- 10 files changed, 44 insertions(+), 36 deletions(-) rename src/{ => features/composers}/hooks/useComposersBrowseFilters.ts (94%) rename src/{ => features/composers}/hooks/useComposersBrowseScrollRestore.ts (95%) rename src/{ => features/composers}/hooks/useNavigateToComposer.ts (84%) create mode 100644 src/features/composers/index.ts rename src/{ => features/composers}/pages/ComposerDetail.tsx (95%) rename src/{ => features/composers}/pages/Composers.tsx (94%) rename src/{ => features/composers}/store/composerBrowseSessionStore.ts (100%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index 736ec20f..5469e8b6 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -13,8 +13,8 @@ const Home = lazy(() => import('../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')); -const Composers = lazy(() => import('../pages/Composers')); -const ComposerDetail = lazy(() => import('../pages/ComposerDetail')); +const Composers = lazy(() => import('@/features/composers/pages/Composers')); +const ComposerDetail = lazy(() => import('@/features/composers/pages/ComposerDetail')); const NewReleases = lazy(() => import('@/features/album/pages/NewReleases')); const Favorites = lazy(() => import('@/features/favorites/pages/Favorites')); const RandomMix = lazy(() => import('../pages/RandomMix')); diff --git a/src/hooks/useComposersBrowseFilters.ts b/src/features/composers/hooks/useComposersBrowseFilters.ts similarity index 94% rename from src/hooks/useComposersBrowseFilters.ts rename to src/features/composers/hooks/useComposersBrowseFilters.ts index ae63c9e6..be465a20 100644 --- a/src/hooks/useComposersBrowseFilters.ts +++ b/src/features/composers/hooks/useComposersBrowseFilters.ts @@ -7,9 +7,9 @@ import { type ComposerBrowseViewMode, isComposersBrowsePath, useComposerBrowseSessionStore, -} from '../store/composerBrowseSessionStore'; -import { shouldRestoreComposerBrowseSession } from '../utils/navigation/albumDetailNavigation'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; +} from '@/features/composers/store/composerBrowseSessionStore'; +import { shouldRestoreComposerBrowseSession } from '@/utils/navigation/albumDetailNavigation'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; export type ComposerBrowseScrollSnapshot = { scrollTop: number; diff --git a/src/hooks/useComposersBrowseScrollRestore.ts b/src/features/composers/hooks/useComposersBrowseScrollRestore.ts similarity index 95% rename from src/hooks/useComposersBrowseScrollRestore.ts rename to src/features/composers/hooks/useComposersBrowseScrollRestore.ts index 4b7b9420..76281288 100644 --- a/src/hooks/useComposersBrowseScrollRestore.ts +++ b/src/features/composers/hooks/useComposersBrowseScrollRestore.ts @@ -3,8 +3,8 @@ import { useLocation, useNavigationType, type NavigationType } from 'react-route import { peekComposerBrowseScrollRestore, useComposerBrowseSessionStore, -} from '../store/composerBrowseSessionStore'; -import { shouldRestoreComposerBrowseSession } from '../utils/navigation/albumDetailNavigation'; +} from '@/features/composers/store/composerBrowseSessionStore'; +import { shouldRestoreComposerBrowseSession } from '@/utils/navigation/albumDetailNavigation'; type PendingScroll = { scrollTop: number; diff --git a/src/hooks/useNavigateToComposer.ts b/src/features/composers/hooks/useNavigateToComposer.ts similarity index 84% rename from src/hooks/useNavigateToComposer.ts rename to src/features/composers/hooks/useNavigateToComposer.ts index 01cf8e52..94179344 100644 --- a/src/hooks/useNavigateToComposer.ts +++ b/src/features/composers/hooks/useNavigateToComposer.ts @@ -1,6 +1,6 @@ import { useCallback } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { navigateToComposerDetail } from '../utils/navigation/albumDetailNavigation'; +import { navigateToComposerDetail } from '@/utils/navigation/albumDetailNavigation'; /** Navigate to composer detail, remembering the current page for the back button. */ export function useNavigateToComposer() { diff --git a/src/features/composers/index.ts b/src/features/composers/index.ts new file mode 100644 index 00000000..db68dffa --- /dev/null +++ b/src/features/composers/index.ts @@ -0,0 +1,8 @@ +/** + * Composers feature — the composer overview (`Composers`) and per-composer + * works (`ComposerDetail`) browse pages, their browse-filter / scroll-restore / + * navigation hooks, and the composer browse-session store. The pages are + * lazy-loaded by the router via their deep paths, so they are not re-exported + * here. Only the live-search scope predicate is consumed cross-feature. + */ +export { isComposersBrowsePath } from './store/composerBrowseSessionStore'; diff --git a/src/pages/ComposerDetail.tsx b/src/features/composers/pages/ComposerDetail.tsx similarity index 95% rename from src/pages/ComposerDetail.tsx rename to src/features/composers/pages/ComposerDetail.tsx index 78ea18b1..967430ec 100644 --- a/src/pages/ComposerDetail.tsx +++ b/src/features/composers/pages/ComposerDetail.tsx @@ -5,21 +5,21 @@ import { useEffect, useState, useMemo } from 'react'; import { useParams, useNavigate } from 'react-router-dom'; import { ndListAlbumsByArtistRole } from '@/lib/api/navidromeBrowse'; import { AlbumCard } from '@/features/album'; -import { ArtistHeroCover } from '../cover/artistHero'; -import { coverArtRef } from '../cover/ref'; -import { useCoverLightboxSrc } from '../cover/lightbox'; +import { ArtistHeroCover } from '@/cover/artistHero'; +import { coverArtRef } from '@/cover/ref'; +import { useCoverLightboxSrc } from '@/cover/lightbox'; import { ArrowLeft, Users, Heart, Feather, Share2 } from 'lucide-react'; -import WikipediaIcon from '../components/WikipediaIcon'; +import WikipediaIcon from '@/components/WikipediaIcon'; import { open } from '@tauri-apps/plugin-shell'; import { usePlayerStore } from '@/features/playback/store/playerStore'; -import { useAuthStore } from '../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { useTranslation } from 'react-i18next'; -import { copyEntityShareLink } from '../utils/share/copyEntityShareLink'; -import { showToast } from '../utils/ui/toast'; +import { copyEntityShareLink } from '@/utils/share/copyEntityShareLink'; +import { showToast } from '@/utils/ui/toast'; import { sanitizeHtml } from '@/lib/util/sanitizeHtml'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { albumGridWarmCovers } from '../cover/layoutSizes'; -import { VirtualCardGrid } from '../components/VirtualCardGrid'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { albumGridWarmCovers } from '@/cover/layoutSizes'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; export default function ComposerDetail() { const { t } = useTranslation(); diff --git a/src/pages/Composers.tsx b/src/features/composers/pages/Composers.tsx similarity index 94% rename from src/pages/Composers.tsx rename to src/features/composers/pages/Composers.tsx index fec70f32..7d1a86af 100644 --- a/src/pages/Composers.tsx +++ b/src/features/composers/pages/Composers.tsx @@ -3,32 +3,32 @@ import { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { ndListArtistsByRole } from '@/lib/api/navidromeBrowse'; import { LayoutGrid, List } from 'lucide-react'; -import StarFilterButton from '../components/StarFilterButton'; +import StarFilterButton from '@/components/StarFilterButton'; import { usePlayerStore } from '@/features/playback/store/playerStore'; -import { useAuthStore } from '../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { useTranslation } from 'react-i18next'; 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 '@/lib/hooks/useResizeClientHeight'; -import { useMainstageInpageHeaderTight } from '../hooks/useMainstageInpageHeaderTight'; +import { useMainstageInpageHeaderTight } from '@/hooks/useMainstageInpageHeaderTight'; import { useBrowseArtistTextSearch } from '@/features/artist'; -import { useComposersBrowseFilters, type ComposerBrowseScrollSnapshot } from '../hooks/useComposersBrowseFilters'; -import { useComposersBrowseScrollRestore } from '../hooks/useComposersBrowseScrollRestore'; +import { useComposersBrowseFilters, type ComposerBrowseScrollSnapshot } from '@/features/composers/hooks/useComposersBrowseFilters'; +import { useComposersBrowseScrollRestore } from '@/features/composers/hooks/useComposersBrowseScrollRestore'; import { useArtistsBrowseScrollReset } from '@/features/artist'; -import { useNavigateToComposer } from '../hooks/useNavigateToComposer'; -import { peekComposerBrowseScrollRestore } from '../store/composerBrowseSessionStore'; -import { useScopedBrowseSearchQuery } from '../store/liveSearchScopeStore'; -import { readComposerBrowseRestore } from '../utils/navigation/albumDetailNavigation'; +import { useNavigateToComposer } from '@/features/composers/hooks/useNavigateToComposer'; +import { peekComposerBrowseScrollRestore } from '@/features/composers/store/composerBrowseSessionStore'; +import { useScopedBrowseSearchQuery } from '@/store/liveSearchScopeStore'; +import { readComposerBrowseRestore } from '@/utils/navigation/albumDetailNavigation'; import { filterArtistsWithRoleAlbumCredits } from '@/lib/library/composerBrowse'; import { ALL_SENTINEL, artistLetterBucket } from '@/features/artist'; -import { useLibraryIgnoredArticles } from '../hooks/useLibraryIgnoredArticles'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { VirtualCardGrid } from '../components/VirtualCardGrid'; +import { useLibraryIgnoredArticles } from '@/hooks/useLibraryIgnoredArticles'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { VirtualCardGrid } from '@/components/VirtualCardGrid'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; import { useVirtualizerScrollMargin } from '@/lib/hooks/useVirtualizerScrollMargin'; -import { useClientSliceInfiniteScroll } from '../hooks/useClientSliceInfiniteScroll'; -import { useInpageScrollViewport } from '../hooks/useInpageScrollViewport'; -import InpageScrollSentinel from '../components/InpageScrollSentinel'; +import { useClientSliceInfiniteScroll } from '@/hooks/useClientSliceInfiniteScroll'; +import { useInpageScrollViewport } from '@/hooks/useInpageScrollViewport'; +import InpageScrollSentinel from '@/components/InpageScrollSentinel'; const ALPHABET = [ALL_SENTINEL, '#', ...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('')]; diff --git a/src/store/composerBrowseSessionStore.ts b/src/features/composers/store/composerBrowseSessionStore.ts similarity index 100% rename from src/store/composerBrowseSessionStore.ts rename to src/features/composers/store/composerBrowseSessionStore.ts diff --git a/src/features/search/components/liveSearchScope.ts b/src/features/search/components/liveSearchScope.ts index efebaeb6..63841eb0 100644 --- a/src/features/search/components/liveSearchScope.ts +++ b/src/features/search/components/liveSearchScope.ts @@ -4,7 +4,7 @@ import type { LiveSearchScope } from '@/store/liveSearchScopeStore'; import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/features/album'; import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore'; import { isArtistsBrowsePath } from '@/features/artist'; -import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore'; +import { isComposersBrowsePath } from '@/features/composers'; export const SCOPE_NAV_ITEM: Record = { artists: 'artists', diff --git a/src/features/search/hooks/useLiveSearchRouteScope.ts b/src/features/search/hooks/useLiveSearchRouteScope.ts index 2daf00e6..69f34645 100644 --- a/src/features/search/hooks/useLiveSearchRouteScope.ts +++ b/src/features/search/hooks/useLiveSearchRouteScope.ts @@ -3,7 +3,7 @@ import { useLocation } from 'react-router-dom'; import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/features/album'; import { isArtistsBrowsePath } from '@/features/artist'; import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore'; -import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore'; +import { isComposersBrowsePath } from '@/features/composers'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; /** Keep scope badge in sync with browse routes; clear field text when leaving browse. */