From fbc37db64e516f8ab1913d78e10c9637c71df9f2 Mon Sep 17 00:00:00 2001 From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com> Date: Mon, 29 Jun 2026 23:42:22 +0200 Subject: [PATCH] refactor(search): co-locate search feature into features/search --- src/app/AppRoutes.tsx | 2 +- src/app/AppShell.tsx | 4 +- src/components/BottomNav.tsx | 2 +- src/components/PasteClipboardHandler.tsx | 4 +- .../search}/components/LiveSearch.tsx | 52 ++++++++-------- .../components/MobileSearchOverlay.test.tsx | 16 ++--- .../components/MobileSearchOverlay.tsx | 34 +++++------ .../components}/ShareQueuePreviewModal.tsx | 18 +++--- .../search/components}/ShareSearchResults.tsx | 24 ++++---- .../search/components}/liveSearchScope.ts | 12 ++-- .../components}/liveSearchScopeUi.test.ts | 2 +- .../search/components}/liveSearchScopeUi.tsx | 6 +- .../hooks/useLiveSearchRouteScope.test.ts | 4 +- .../search}/hooks/useLiveSearchRouteScope.ts | 10 ++-- .../search}/hooks/useShareQueuePreview.ts | 6 +- .../search}/hooks/useShareSearch.ts | 18 +++--- .../search}/hooks/useShareSearchPreview.ts | 6 +- src/features/search/index.ts | 15 +++++ .../search}/pages/SearchBrowsePage.tsx | 60 +++++++++---------- 19 files changed, 155 insertions(+), 140 deletions(-) rename src/{ => features/search}/components/LiveSearch.tsx (95%) rename src/{ => features/search}/components/MobileSearchOverlay.test.tsx (70%) rename src/{ => features/search}/components/MobileSearchOverlay.tsx (93%) rename src/{components/search => features/search/components}/ShareQueuePreviewModal.tsx (92%) rename src/{components/search => features/search/components}/ShareSearchResults.tsx (94%) rename src/{components/search => features/search/components}/liveSearchScope.ts (92%) rename src/{components/search => features/search/components}/liveSearchScopeUi.test.ts (99%) rename src/{components/search => features/search/components}/liveSearchScopeUi.tsx (92%) rename src/{ => features/search}/hooks/useLiveSearchRouteScope.test.ts (90%) rename src/{ => features/search}/hooks/useLiveSearchRouteScope.ts (73%) rename src/{ => features/search}/hooks/useShareQueuePreview.ts (89%) rename src/{ => features/search}/hooks/useShareSearch.ts (86%) rename src/{ => features/search}/hooks/useShareSearchPreview.ts (96%) create mode 100644 src/features/search/index.ts rename src/{ => features/search}/pages/SearchBrowsePage.tsx (96%) diff --git a/src/app/AppRoutes.tsx b/src/app/AppRoutes.tsx index c0dc9f1a..85f41e0b 100644 --- a/src/app/AppRoutes.tsx +++ b/src/app/AppRoutes.tsx @@ -34,7 +34,7 @@ const WhatsNew = lazy(() => import('../pages/WhatsNew')); const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync')); const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary')); const LabelAlbums = lazy(() => import('../pages/LabelAlbums')); -const SearchBrowsePage = lazy(() => import('../pages/SearchBrowsePage')); +const SearchBrowsePage = lazy(() => import('@/features/search/pages/SearchBrowsePage')); const FolderBrowser = lazy(() => import('../pages/FolderBrowser')); const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio')); const Genres = lazy(() => import('../pages/Genres')); diff --git a/src/app/AppShell.tsx b/src/app/AppShell.tsx index 8b444787..a041b0bb 100644 --- a/src/app/AppShell.tsx +++ b/src/app/AppShell.tsx @@ -9,7 +9,7 @@ import Sidebar from '@/features/sidebar'; import PlayerBar from '../components/PlayerBar'; import BottomNav from '../components/BottomNav'; import { useIsMobile } from '../hooks/useIsMobile'; -import LiveSearch from '../components/LiveSearch'; +import { LiveSearch } from '@/features/search'; import DevNetworkModeToggle from '../components/DevNetworkModeToggle'; import { NowPlayingDropdown } from '@/features/nowPlaying'; import QueuePanel from '../components/QueuePanel'; @@ -51,7 +51,7 @@ import { useGlobalDndAndSelectionBlockers } from '../hooks/useGlobalDndAndSelect import { useAppActivityTracking } from '../hooks/useAppActivityTracking'; import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator'; import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority'; -import { useLiveSearchRouteScope } from '../hooks/useLiveSearchRouteScope'; +import { useLiveSearchRouteScope } from '@/features/search'; import { useNowPlayingPrewarm } from '@/features/nowPlaying'; import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx index ba019207..6cb0f627 100644 --- a/src/components/BottomNav.tsx +++ b/src/components/BottomNav.tsx @@ -3,7 +3,7 @@ import { NavLink } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Disc3, Search, Music4, AudioLines, MoreHorizontal } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; -import MobileSearchOverlay from './MobileSearchOverlay'; +import { MobileSearchOverlay } from '@/features/search'; import MobileMoreOverlay from './MobileMoreOverlay'; const NAV_ITEMS = [ diff --git a/src/components/PasteClipboardHandler.tsx b/src/components/PasteClipboardHandler.tsx index 205ba60b..b4c8fade 100644 --- a/src/components/PasteClipboardHandler.tsx +++ b/src/components/PasteClipboardHandler.tsx @@ -8,8 +8,8 @@ import { decodeServerMagicStringFromText } from '../utils/server/serverMagicStri import { applySharePastePayload, applySharePasteQueue } from '../utils/share/applySharePaste'; import { shareQueueServerContext } from '../utils/share/shareServerOriginLabel'; import { showToast } from '../utils/ui/toast'; -import { useShareQueuePreview } from '../hooks/useShareQueuePreview'; -import ShareQueuePreviewModal from './search/ShareQueuePreviewModal'; +import { useShareQueuePreview } from '@/features/search'; +import { ShareQueuePreviewModal } from '@/features/search'; import { parseOrbitShareLink, joinOrbitSession, diff --git a/src/components/LiveSearch.tsx b/src/features/search/components/LiveSearch.tsx similarity index 95% rename from src/components/LiveSearch.tsx rename to src/features/search/components/LiveSearch.tsx index 77a3953d..68276c0f 100644 --- a/src/components/LiveSearch.tsx +++ b/src/features/search/components/LiveSearch.tsx @@ -1,6 +1,6 @@ -import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '../api/library'; -import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes'; -import { songToTrack } from '../utils/playback/songToTrack'; +import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '@/api/library'; +import type { SearchResults, SubsonicArtist } from '@/api/subsonicTypes'; +import { songToTrack } from '@/utils/playback/songToTrack'; import { LIVE_SEARCH_DEBOUNCE_NETWORK_MS, LIVE_SEARCH_DEBOUNCE_RACE_MS, @@ -9,40 +9,40 @@ import { mergeLiveSearchResults, runLocalLiveSearch, runNetworkLiveSearch, -} from '../utils/library/liveSearchLocal'; -import { raceLiveSearch } from '../utils/library/searchRace'; -import { libraryIsReady } from '../utils/library/libraryReady'; +} from '@/utils/library/liveSearchLocal'; +import { raceLiveSearch } from '@/utils/library/searchRace'; +import { libraryIsReady } from '@/utils/library/libraryReady'; import { emitLiveSearchDebug, searchHitCounts, searchResultSamples, -} from '../utils/library/liveSearchDebug'; +} from '@/utils/library/liveSearchDebug'; import { logLibrarySearch, -} from '../utils/library/libraryDevLog'; +} from '@/utils/library/libraryDevLog'; import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; -import { useNavigateToAlbum } from '../hooks/useNavigateToAlbum'; +import { useNavigateToAlbum } from '@/hooks/useNavigateToAlbum'; import { Search, Disc3, Users, Music, TextSearch, Database, Globe } from 'lucide-react'; -import { usePlayerStore } from '../store/playerStore'; -import { useAuthStore } from '../store/authStore'; -import { useLibraryIndexStore } from '../store/libraryIndexStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { useAuthStore } from '@/store/authStore'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; import { useTranslation } from 'react-i18next'; -import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs'; +import { albumArtistDisplayName } from '@/utils/album/deriveAlbumHeaderArtistRefs'; import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage'; -import type { SubsonicSong } from '../api/subsonicTypes'; -import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; -import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage'; -import { CoverArtImage } from '../cover/CoverArtImage'; -import { COVER_DENSE_SEARCH_CSS_PX } from '../cover/layoutSizes'; -import { albumCoverRef } from '../cover/ref'; -import { showToast } from '../utils/ui/toast'; -import { useShareSearch } from '../hooks/useShareSearch'; -import ShareSearchResults from './search/ShareSearchResults'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage'; +import { CoverArtImage } from '@/cover/CoverArtImage'; +import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes'; +import { albumCoverRef } from '@/cover/ref'; +import { showToast } from '@/utils/ui/toast'; +import { useShareSearch } from '@/features/search/hooks/useShareSearch'; +import ShareSearchResults from '@/features/search/components/ShareSearchResults'; import { LiveSearchScopeBadge, LiveSearchScopeGhostBadge, -} from './search/liveSearchScopeUi'; +} from '@/features/search/components/liveSearchScopeUi'; import { createLiveSearchScopeBackspaceState, handleLiveSearchScopeBackspace, @@ -52,9 +52,9 @@ import { noteLiveSearchScopeQueryInput, resetLiveSearchScopeBackspaceState, resolveLiveSearchScopeGhost, -} from './search/liveSearchScope'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; -import { resolveIndexKey } from '../utils/server/serverIndexKey'; +} from '@/features/search/components/liveSearchScope'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; +import { resolveIndexKey } from '@/utils/server/serverIndexKey'; type LiveSearchSource = 'local' | 'network'; diff --git a/src/components/MobileSearchOverlay.test.tsx b/src/features/search/components/MobileSearchOverlay.test.tsx similarity index 70% rename from src/components/MobileSearchOverlay.test.tsx rename to src/features/search/components/MobileSearchOverlay.test.tsx index dd06e3a9..97855bf0 100644 --- a/src/components/MobileSearchOverlay.test.tsx +++ b/src/features/search/components/MobileSearchOverlay.test.tsx @@ -1,21 +1,21 @@ import { describe, expect, it, vi, beforeEach } from 'vitest'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { renderWithProviders } from '../test/helpers/renderWithProviders'; -import MobileSearchOverlay from './MobileSearchOverlay'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; +import { renderWithProviders } from '@/test/helpers/renderWithProviders'; +import MobileSearchOverlay from '@/features/search/components/MobileSearchOverlay'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; // The overlay's only behaviour-bearing change in PR #1165 was renaming the // recent-search handler `useRecent` → `applyRecentSearch` (it was a plain // function mis-flagged as a hook). Smoke-test that the recent-search path still // applies the term to the live-search store. Heavy collaborators are stubbed. -vi.mock('../hooks/useShareSearch', () => ({ useShareSearch: () => ({ shareMatch: null }) })); -vi.mock('../api/subsonicSearch', () => ({ +vi.mock('@/features/search/hooks/useShareSearch', () => ({ useShareSearch: () => ({ shareMatch: null }) })); +vi.mock('@/api/subsonicSearch', () => ({ search: vi.fn(() => Promise.resolve({ artists: [], albums: [], songs: [] })), })); -vi.mock('../cover/AlbumCoverArtImage', () => ({ AlbumCoverArtImage: () => null })); -vi.mock('../cover/ArtistCoverArtImage', () => ({ ArtistCoverArtImage: () => null })); -vi.mock('../cover/CoverArtImage', () => ({ CoverArtImage: () => null })); +vi.mock('@/cover/AlbumCoverArtImage', () => ({ AlbumCoverArtImage: () => null })); +vi.mock('@/cover/ArtistCoverArtImage', () => ({ ArtistCoverArtImage: () => null })); +vi.mock('@/cover/CoverArtImage', () => ({ CoverArtImage: () => null })); const RECENT_KEY = 'psysonic_recent_searches'; diff --git a/src/components/MobileSearchOverlay.tsx b/src/features/search/components/MobileSearchOverlay.tsx similarity index 93% rename from src/components/MobileSearchOverlay.tsx rename to src/features/search/components/MobileSearchOverlay.tsx index 224eed00..eff6f332 100644 --- a/src/components/MobileSearchOverlay.tsx +++ b/src/features/search/components/MobileSearchOverlay.tsx @@ -1,28 +1,28 @@ -import { search } from '../api/subsonicSearch'; -import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes'; -import { songToTrack } from '../utils/playback/songToTrack'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; +import { search } from '@/api/subsonicSearch'; +import type { SearchResults, SubsonicArtist } from '@/api/subsonicTypes'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { useLocation, useNavigate } from 'react-router-dom'; -import { navigatePathWithAlbumReturnTo } from '../utils/navigation/albumDetailNavigation'; +import { navigatePathWithAlbumReturnTo } from '@/utils/navigation/albumDetailNavigation'; import { X, Search, Disc3, Users, Music, Music2, Clock, ChevronRight } from 'lucide-react'; -import { usePlayerStore } from '../store/playerStore'; -import { useAuthStore } from '../store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { useAuthStore } from '@/store/authStore'; import { useTranslation } from 'react-i18next'; import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage'; -import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; -import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage'; -import { CoverArtImage } from '../cover/CoverArtImage'; -import { albumCoverRefForSong } from '../cover/ref'; -import { showToast } from '../utils/ui/toast'; -import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs'; -import { useShareSearch } from '../hooks/useShareSearch'; -import ShareSearchResults from './search/ShareSearchResults'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage'; +import { CoverArtImage } from '@/cover/CoverArtImage'; +import { albumCoverRefForSong } from '@/cover/ref'; +import { showToast } from '@/utils/ui/toast'; +import { albumArtistDisplayName } from '@/utils/album/deriveAlbumHeaderArtistRefs'; +import { useShareSearch } from '@/features/search/hooks/useShareSearch'; +import ShareSearchResults from '@/features/search/components/ShareSearchResults'; import { LiveSearchScopeBadge, LiveSearchScopeGhostBadge, -} from './search/liveSearchScopeUi'; +} from '@/features/search/components/liveSearchScopeUi'; import { createLiveSearchScopeBackspaceState, handleLiveSearchScopeBackspace, @@ -32,7 +32,7 @@ import { noteLiveSearchScopeQueryInput, resetLiveSearchScopeBackspaceState, resolveLiveSearchScopeGhost, -} from './search/liveSearchScope'; +} from '@/features/search/components/liveSearchScope'; const STORAGE_KEY = 'psysonic_recent_searches'; const MAX_RECENT = 6; diff --git a/src/components/search/ShareQueuePreviewModal.tsx b/src/features/search/components/ShareQueuePreviewModal.tsx similarity index 92% rename from src/components/search/ShareQueuePreviewModal.tsx rename to src/features/search/components/ShareQueuePreviewModal.tsx index 6615db22..176135b9 100644 --- a/src/components/search/ShareQueuePreviewModal.tsx +++ b/src/features/search/components/ShareQueuePreviewModal.tsx @@ -2,16 +2,16 @@ import React, { useEffect } from 'react'; import { createPortal } from 'react-dom'; import { Music, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import type { SubsonicSong } from '../../api/subsonicTypes'; -import type { ServerProfile } from '../../store/authStoreTypes'; -import { formatTrackTime } from '../../utils/format/formatDuration'; -import type { ShareQueuePreviewState } from '../../hooks/useShareQueuePreview'; -import { sharePayloadTotal, type QueueableShareSearchPayload } from '../../utils/share/shareSearch'; +import type { SubsonicSong } from '@/api/subsonicTypes'; +import type { ServerProfile } from '@/store/authStoreTypes'; +import { formatTrackTime } from '@/utils/format/formatDuration'; +import type { ShareQueuePreviewState } from '@/features/search/hooks/useShareQueuePreview'; +import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/utils/share/shareSearch'; import OverlayScrollArea from '@/ui/OverlayScrollArea'; -import { usePlayerStore } from '../../store/playerStore'; -import { COVER_DENSE_SEARCH_CSS_PX } from '../../cover/layoutSizes'; -import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '../../cover/types'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; +import { usePlayerStore } from '@/store/playerStore'; +import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes'; +import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '@/cover/types'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; type ShareQueuePreviewModalProps = { open: boolean; diff --git a/src/components/search/ShareSearchResults.tsx b/src/features/search/components/ShareSearchResults.tsx similarity index 94% rename from src/components/search/ShareSearchResults.tsx rename to src/features/search/components/ShareSearchResults.tsx index 1f9b2e44..87bf11dd 100644 --- a/src/components/search/ShareSearchResults.tsx +++ b/src/features/search/components/ShareSearchResults.tsx @@ -2,19 +2,19 @@ import React, { useEffect, useState } from 'react'; import { Disc3, Eye, Link2, ListPlus, Music, Users } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { TFunction } from 'i18next'; -import type { SubsonicArtist } from '../../api/subsonicTypes'; -import type { ServerProfile } from '../../store/authStoreTypes'; -import { songToTrack } from '../../utils/playback/songToTrack'; -import { activateShareSearchServer } from '../../utils/share/enqueueShareSearchPayload'; -import { sharePayloadTotal, type ShareSearchMatch } from '../../utils/share/shareSearch'; -import type { ShareSearchPreviewState } from '../../hooks/useShareSearchPreview'; +import type { SubsonicArtist } from '@/api/subsonicTypes'; +import type { ServerProfile } from '@/store/authStoreTypes'; +import { songToTrack } from '@/utils/playback/songToTrack'; +import { activateShareSearchServer } from '@/utils/share/enqueueShareSearchPayload'; +import { sharePayloadTotal, type ShareSearchMatch } from '@/utils/share/shareSearch'; +import type { ShareSearchPreviewState } from '@/features/search/hooks/useShareSearchPreview'; import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage'; -import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; -import { ArtistCoverArtImage } from '../../cover/ArtistCoverArtImage'; -import { COVER_DENSE_SEARCH_CSS_PX } from '../../cover/layoutSizes'; -import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '../../cover/types'; -import { useShareQueuePreview } from '../../hooks/useShareQueuePreview'; -import ShareQueuePreviewModal from './ShareQueuePreviewModal'; +import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage'; +import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage'; +import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes'; +import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '@/cover/types'; +import { useShareQueuePreview } from '@/features/search/hooks/useShareQueuePreview'; +import ShareQueuePreviewModal from '@/features/search/components/ShareQueuePreviewModal'; type ShareSearchResultsProps = { variant: 'desktop' | 'mobile'; diff --git a/src/components/search/liveSearchScope.ts b/src/features/search/components/liveSearchScope.ts similarity index 92% rename from src/components/search/liveSearchScope.ts rename to src/features/search/components/liveSearchScope.ts index 113c87f3..315f3504 100644 --- a/src/components/search/liveSearchScope.ts +++ b/src/features/search/components/liveSearchScope.ts @@ -1,10 +1,10 @@ import type { KeyboardEvent, MouseEvent } from 'react'; -import { ALL_NAV_ITEMS } from '../../config/navItems'; -import type { LiveSearchScope } from '../../store/liveSearchScopeStore'; -import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '../../store/albumBrowseSessionStore'; -import { isTracksBrowsePath } from '../../store/advancedSearchSessionStore'; -import { isArtistsBrowsePath } from '../../store/artistBrowseSessionStore'; -import { isComposersBrowsePath } from '../../store/composerBrowseSessionStore'; +import { ALL_NAV_ITEMS } from '@/config/navItems'; +import type { LiveSearchScope } from '@/store/liveSearchScopeStore'; +import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore'; +import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore'; +import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore'; +import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore'; export const SCOPE_NAV_ITEM: Record = { artists: 'artists', diff --git a/src/components/search/liveSearchScopeUi.test.ts b/src/features/search/components/liveSearchScopeUi.test.ts similarity index 99% rename from src/components/search/liveSearchScopeUi.test.ts rename to src/features/search/components/liveSearchScopeUi.test.ts index 528b81c0..33e6297b 100644 --- a/src/components/search/liveSearchScopeUi.test.ts +++ b/src/features/search/components/liveSearchScopeUi.test.ts @@ -11,7 +11,7 @@ import { noteLiveSearchScopeQueryInput, resetLiveSearchScopeBackspaceState, resolveLiveSearchScopeGhost, -} from './liveSearchScope'; +} from '@/features/search/components/liveSearchScope'; function keyEvent( key: string, diff --git a/src/components/search/liveSearchScopeUi.tsx b/src/features/search/components/liveSearchScopeUi.tsx similarity index 92% rename from src/components/search/liveSearchScopeUi.tsx rename to src/features/search/components/liveSearchScopeUi.tsx index a12a148e..154d2013 100644 --- a/src/components/search/liveSearchScopeUi.tsx +++ b/src/features/search/components/liveSearchScopeUi.tsx @@ -1,13 +1,13 @@ import { useTranslation } from 'react-i18next'; -import { ALL_NAV_ITEMS } from '../../config/navItems'; -import type { LiveSearchScope } from '../../store/liveSearchScopeStore'; +import { ALL_NAV_ITEMS } from '@/config/navItems'; +import type { LiveSearchScope } from '@/store/liveSearchScopeStore'; import { SCOPE_NAV_ITEM, handleLiveSearchScopeBadgeClick, handleLiveSearchScopeGhostClick, liveSearchScopeBadgeTooltipKey, liveSearchScopeGhostTooltipKey, -} from './liveSearchScope'; +} from '@/features/search/components/liveSearchScope'; type LiveSearchScopeIconProps = { scope: LiveSearchScope; diff --git a/src/hooks/useLiveSearchRouteScope.test.ts b/src/features/search/hooks/useLiveSearchRouteScope.test.ts similarity index 90% rename from src/hooks/useLiveSearchRouteScope.test.ts rename to src/features/search/hooks/useLiveSearchRouteScope.test.ts index 53fac976..8f5ca195 100644 --- a/src/hooks/useLiveSearchRouteScope.test.ts +++ b/src/features/search/hooks/useLiveSearchRouteScope.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, beforeEach } from 'vitest'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; -import { syncLiveSearchRouteScope } from './useLiveSearchRouteScope'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; +import { syncLiveSearchRouteScope } from '@/features/search/hooks/useLiveSearchRouteScope'; describe('syncLiveSearchRouteScope', () => { beforeEach(() => { diff --git a/src/hooks/useLiveSearchRouteScope.ts b/src/features/search/hooks/useLiveSearchRouteScope.ts similarity index 73% rename from src/hooks/useLiveSearchRouteScope.ts rename to src/features/search/hooks/useLiveSearchRouteScope.ts index 1a5b67e8..32e39ce3 100644 --- a/src/hooks/useLiveSearchRouteScope.ts +++ b/src/features/search/hooks/useLiveSearchRouteScope.ts @@ -1,10 +1,10 @@ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; -import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '../store/albumBrowseSessionStore'; -import { isArtistsBrowsePath } from '../store/artistBrowseSessionStore'; -import { isTracksBrowsePath } from '../store/advancedSearchSessionStore'; -import { isComposersBrowsePath } from '../store/composerBrowseSessionStore'; -import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; +import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore'; +import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore'; +import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore'; +import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; /** Keep scope badge in sync with browse routes; clear field text when leaving browse. */ export function syncLiveSearchRouteScope(pathname: string): void { diff --git a/src/hooks/useShareQueuePreview.ts b/src/features/search/hooks/useShareQueuePreview.ts similarity index 89% rename from src/hooks/useShareQueuePreview.ts rename to src/features/search/hooks/useShareQueuePreview.ts index d6f54b27..f366e5e5 100644 --- a/src/hooks/useShareQueuePreview.ts +++ b/src/features/search/hooks/useShareQueuePreview.ts @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; -import type { SubsonicSong } from '../api/subsonicTypes'; +import type { SubsonicSong } from '@/api/subsonicTypes'; import { resolveShareSearchPayload, type ShareSearchResolveResult, -} from '../utils/share/enqueueShareSearchPayload'; -import type { QueueableShareSearchPayload } from '../utils/share/shareSearch'; +} from '@/utils/share/enqueueShareSearchPayload'; +import type { QueueableShareSearchPayload } from '@/utils/share/shareSearch'; export type ShareQueuePreviewState = | { status: 'idle' } diff --git a/src/hooks/useShareSearch.ts b/src/features/search/hooks/useShareSearch.ts similarity index 86% rename from src/hooks/useShareSearch.ts rename to src/features/search/hooks/useShareSearch.ts index ea1a4409..cf88d0e8 100644 --- a/src/hooks/useShareSearch.ts +++ b/src/features/search/hooks/useShareSearch.ts @@ -1,18 +1,18 @@ import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { useNavigateToAlbum } from './useNavigateToAlbum'; +import { useNavigateToAlbum } from '@/hooks/useNavigateToAlbum'; import { useTranslation } from 'react-i18next'; -import { useAuthStore } from '../store/authStore'; +import { useAuthStore } from '@/store/authStore'; import { activateShareSearchServer, enqueueShareSearchPayload, -} from '../utils/share/enqueueShareSearchPayload'; -import type { ServerProfile } from '../store/authStoreTypes'; -import { findServerIdForShareUrl } from '../utils/share/shareLink'; -import { shareServerOriginLabel } from '../utils/share/shareServerOriginLabel'; -import { parseShareSearchText } from '../utils/share/shareSearch'; -import { serverIndexKeyFromUrl } from '../utils/server/serverIndexKey'; -import { useShareSearchPreview } from './useShareSearchPreview'; +} from '@/utils/share/enqueueShareSearchPayload'; +import type { ServerProfile } from '@/store/authStoreTypes'; +import { findServerIdForShareUrl } from '@/utils/share/shareLink'; +import { shareServerOriginLabel } from '@/utils/share/shareServerOriginLabel'; +import { parseShareSearchText } from '@/utils/share/shareSearch'; +import { serverIndexKeyFromUrl } from '@/utils/server/serverIndexKey'; +import { useShareSearchPreview } from '@/features/search/hooks/useShareSearchPreview'; export function useShareSearch(query: string, onSuccess?: () => void) { const { t } = useTranslation(); diff --git a/src/hooks/useShareSearchPreview.ts b/src/features/search/hooks/useShareSearchPreview.ts similarity index 96% rename from src/hooks/useShareSearchPreview.ts rename to src/features/search/hooks/useShareSearchPreview.ts index 1fa43dbe..da02f660 100644 --- a/src/hooks/useShareSearchPreview.ts +++ b/src/features/search/hooks/useShareSearchPreview.ts @@ -1,11 +1,11 @@ import { useEffect, useState } from 'react'; -import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonicTypes'; +import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/api/subsonicTypes'; import { resolveShareSearchAlbum, resolveShareSearchArtist, resolveShareSearchPayload, -} from '../utils/share/enqueueShareSearchPayload'; -import type { ShareSearchMatch } from '../utils/share/shareSearch'; +} from '@/utils/share/enqueueShareSearchPayload'; +import type { ShareSearchMatch } from '@/utils/share/shareSearch'; export interface ShareSearchPreviewState { shareTrackSong: SubsonicSong | null; diff --git a/src/features/search/index.ts b/src/features/search/index.ts new file mode 100644 index 00000000..80b8ffb4 --- /dev/null +++ b/src/features/search/index.ts @@ -0,0 +1,15 @@ +/** + * Search feature — the live search dropdown, the mobile search overlay, the + * search/browse page (lazy via the deep path `pages/SearchBrowsePage`), live + * search scope UI/state, and the share-link search/queue-preview surfaces. + * + * The local-index query engine (`utils/library/*search*`), the shared Subsonic + * search API, the cross-cutting `liveSearchScopeStore`, and the + * `advancedSearch*` session/scroll state live outside this feature + * (library-core / cross-cutting) — this feature consumes them. + */ +export { default as LiveSearch } from './components/LiveSearch'; +export { default as MobileSearchOverlay } from './components/MobileSearchOverlay'; +export { default as ShareQueuePreviewModal } from './components/ShareQueuePreviewModal'; +export { useLiveSearchRouteScope } from './hooks/useLiveSearchRouteScope'; +export { useShareQueuePreview } from './hooks/useShareQueuePreview'; diff --git a/src/pages/SearchBrowsePage.tsx b/src/features/search/pages/SearchBrowsePage.tsx similarity index 96% rename from src/pages/SearchBrowsePage.tsx rename to src/features/search/pages/SearchBrowsePage.tsx index a904e2ab..cb68cd30 100644 --- a/src/pages/SearchBrowsePage.tsx +++ b/src/features/search/pages/SearchBrowsePage.tsx @@ -1,32 +1,32 @@ -import { getGenres, getAlbumsByGenre } from '../api/subsonicGenres'; -import { search, searchSongsPaged } from '../api/subsonicSearch'; -import { getRandomSongs } from '../api/subsonicLibrary'; -import type { SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes'; +import { getGenres, getAlbumsByGenre } from '@/api/subsonicGenres'; +import { search, searchSongsPaged } from '@/api/subsonicSearch'; +import { getRandomSongs } from '@/api/subsonicLibrary'; +import type { SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useLocation, useNavigate, useNavigationType, useSearchParams } from 'react-router-dom'; import { SlidersVertical, Search, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import AlbumRow from '../components/AlbumRow'; -import ArtistRow from '../components/ArtistRow'; -import PagedSongList from '../components/PagedSongList'; +import AlbumRow from '@/components/AlbumRow'; +import ArtistRow from '@/components/ArtistRow'; +import PagedSongList from '@/components/PagedSongList'; import CustomSelect from '@/ui/CustomSelect'; -import StarFilterButton from '../components/StarFilterButton'; +import StarFilterButton from '@/components/StarFilterButton'; import { tooltipAttrs } from '@/ui/tooltipAttrs'; -import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; -import { useAuthStore } from '../store/authStore'; -import { usePlayerStore } from '../store/playerStore'; -import { isAdvancedSearchLeaveTargetPath } from '../store/albumBrowseSessionStore'; +import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll'; +import { useAuthStore } from '@/store/authStore'; +import { usePlayerStore } from '@/store/playerStore'; +import { isAdvancedSearchLeaveTargetPath } from '@/store/albumBrowseSessionStore'; import { isAdvancedSearchPath, isAdvancedSearchPanelPath, isTracksBrowsePath, useAdvancedSearchSessionStore, type AdvancedSearchSessionStash, -} from '../store/advancedSearchSessionStore'; +} from '@/store/advancedSearchSessionStore'; import { readAdvancedSearchRestore, shouldRestoreAdvancedSearchSession, -} from '../utils/navigation/albumDetailNavigation'; +} from '@/utils/navigation/albumDetailNavigation'; import { clearAdvancedSearchLeaveSnapshots, consumeAdvancedSearchLeavingForDetail, @@ -35,36 +35,36 @@ import { registerAdvancedSearchSessionProvider, resolveAdvancedSearchLeaveSnapshot, type AdvancedSearchLeaveSnapshot, -} from '../utils/navigation/advancedSearchScrollSnapshot'; -import { restoreMainViewportScroll } from '../utils/navigation/restoreMainViewportScroll'; +} from '@/utils/navigation/advancedSearchScrollSnapshot'; +import { restoreMainViewportScroll } from '@/utils/navigation/restoreMainViewportScroll'; import { loadMoreLocalSongs, runNetworkAdvancedTextSearch, runNetworkAdvancedYearAlbums, tryRunLocalAdvancedSearch, -} from '../utils/library/advancedSearchLocal'; -import { isLosslessSuffix } from '../utils/library/losslessFormats'; -import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode'; -import { OXIMEDIA_MOOD_SEARCH_ENABLED } from '../utils/library/trackEnrichment'; -import { raceSearchSources } from '../utils/library/searchRace'; -import { logLibrarySearch } from '../utils/library/libraryDevLog'; +} from '@/utils/library/advancedSearchLocal'; +import { isLosslessSuffix } from '@/utils/library/losslessFormats'; +import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode'; +import { OXIMEDIA_MOOD_SEARCH_ENABLED } from '@/utils/library/trackEnrichment'; +import { raceSearchSources } from '@/utils/library/searchRace'; +import { logLibrarySearch } from '@/utils/library/libraryDevLog'; import { browseRaceCountsFullSearch, loadMoreLocalBrowseSongs, raceBrowseWithLocalFallback, runLocalBrowseFullSearch, runNetworkBrowseFullSearch, -} from '../utils/library/browseTextSearch'; -import { useLibraryIndexStore } from '../store/libraryIndexStore'; -import { MOOD_GROUP_IDS } from '../config/moodGroups'; -import { usePerfProbeFlags } from '../utils/perf/perfFlags'; -import { useSongBrowseList, type SongBrowseListRestore } from '../hooks/useSongBrowseList'; -import TracksPageChrome from '../components/tracks/TracksPageChrome'; -import SongBrowseSection from '../components/tracks/SongBrowseSection'; +} from '@/utils/library/browseTextSearch'; +import { useLibraryIndexStore } from '@/store/libraryIndexStore'; +import { MOOD_GROUP_IDS } from '@/config/moodGroups'; +import { usePerfProbeFlags } from '@/utils/perf/perfFlags'; +import { useSongBrowseList, type SongBrowseListRestore } from '@/hooks/useSongBrowseList'; +import TracksPageChrome from '@/components/tracks/TracksPageChrome'; +import SongBrowseSection from '@/components/tracks/SongBrowseSection'; import { useLiveSearchScopeStore, useScopedBrowseSearchQuery, -} from '../store/liveSearchScopeStore'; +} from '@/store/liveSearchScopeStore'; const MOOD_UI_ENABLED = OXIMEDIA_MOOD_SEARCH_ENABLED;