refactor(search): co-locate search feature into features/search

This commit is contained in:
Psychotoxical
2026-06-29 23:42:22 +02:00
parent 931c47e19e
commit fbc37db64e
19 changed files with 155 additions and 140 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ const WhatsNew = lazy(() => import('../pages/WhatsNew'));
const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync')); const DeviceSync = lazy(() => import('@/features/deviceSync/pages/DeviceSync'));
const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary')); const OfflineLibrary = lazy(() => import('../pages/OfflineLibrary'));
const LabelAlbums = lazy(() => import('../pages/LabelAlbums')); 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 FolderBrowser = lazy(() => import('../pages/FolderBrowser'));
const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio')); const InternetRadio = lazy(() => import('@/features/radio/pages/InternetRadio'));
const Genres = lazy(() => import('../pages/Genres')); const Genres = lazy(() => import('../pages/Genres'));
+2 -2
View File
@@ -9,7 +9,7 @@ import Sidebar from '@/features/sidebar';
import PlayerBar from '../components/PlayerBar'; import PlayerBar from '../components/PlayerBar';
import BottomNav from '../components/BottomNav'; import BottomNav from '../components/BottomNav';
import { useIsMobile } from '../hooks/useIsMobile'; import { useIsMobile } from '../hooks/useIsMobile';
import LiveSearch from '../components/LiveSearch'; import { LiveSearch } from '@/features/search';
import DevNetworkModeToggle from '../components/DevNetworkModeToggle'; import DevNetworkModeToggle from '../components/DevNetworkModeToggle';
import { NowPlayingDropdown } from '@/features/nowPlaying'; import { NowPlayingDropdown } from '@/features/nowPlaying';
import QueuePanel from '../components/QueuePanel'; import QueuePanel from '../components/QueuePanel';
@@ -51,7 +51,7 @@ import { useGlobalDndAndSelectionBlockers } from '../hooks/useGlobalDndAndSelect
import { useAppActivityTracking } from '../hooks/useAppActivityTracking'; import { useAppActivityTracking } from '../hooks/useAppActivityTracking';
import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator'; import { useMainScrollingIndicator } from '../hooks/useMainScrollingIndicator';
import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority'; import { useCoverNavigationPriority } from '../hooks/useCoverNavigationPriority';
import { useLiveSearchRouteScope } from '../hooks/useLiveSearchRouteScope'; import { useLiveSearchRouteScope } from '@/features/search';
import { useNowPlayingPrewarm } from '@/features/nowPlaying'; import { useNowPlayingPrewarm } from '@/features/nowPlaying';
import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav'; import { useOfflineAutoNav } from '../hooks/useOfflineAutoNav';
import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend'; import { useOfflineLibraryFilterSuspend } from '../hooks/useOfflineLibraryFilterSuspend';
+1 -1
View File
@@ -3,7 +3,7 @@ import { NavLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Disc3, Search, Music4, AudioLines, MoreHorizontal } from 'lucide-react'; import { Disc3, Search, Music4, AudioLines, MoreHorizontal } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '../store/playerStore';
import MobileSearchOverlay from './MobileSearchOverlay'; import { MobileSearchOverlay } from '@/features/search';
import MobileMoreOverlay from './MobileMoreOverlay'; import MobileMoreOverlay from './MobileMoreOverlay';
const NAV_ITEMS = [ const NAV_ITEMS = [
+2 -2
View File
@@ -8,8 +8,8 @@ import { decodeServerMagicStringFromText } from '../utils/server/serverMagicStri
import { applySharePastePayload, applySharePasteQueue } from '../utils/share/applySharePaste'; import { applySharePastePayload, applySharePasteQueue } from '../utils/share/applySharePaste';
import { shareQueueServerContext } from '../utils/share/shareServerOriginLabel'; import { shareQueueServerContext } from '../utils/share/shareServerOriginLabel';
import { showToast } from '../utils/ui/toast'; import { showToast } from '../utils/ui/toast';
import { useShareQueuePreview } from '../hooks/useShareQueuePreview'; import { useShareQueuePreview } from '@/features/search';
import ShareQueuePreviewModal from './search/ShareQueuePreviewModal'; import { ShareQueuePreviewModal } from '@/features/search';
import { import {
parseOrbitShareLink, parseOrbitShareLink,
joinOrbitSession, joinOrbitSession,
@@ -1,6 +1,6 @@
import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '../api/library'; import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '@/api/library';
import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes'; import type { SearchResults, SubsonicArtist } from '@/api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack'; import { songToTrack } from '@/utils/playback/songToTrack';
import { import {
LIVE_SEARCH_DEBOUNCE_NETWORK_MS, LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
LIVE_SEARCH_DEBOUNCE_RACE_MS, LIVE_SEARCH_DEBOUNCE_RACE_MS,
@@ -9,40 +9,40 @@ import {
mergeLiveSearchResults, mergeLiveSearchResults,
runLocalLiveSearch, runLocalLiveSearch,
runNetworkLiveSearch, runNetworkLiveSearch,
} from '../utils/library/liveSearchLocal'; } from '@/utils/library/liveSearchLocal';
import { raceLiveSearch } from '../utils/library/searchRace'; import { raceLiveSearch } from '@/utils/library/searchRace';
import { libraryIsReady } from '../utils/library/libraryReady'; import { libraryIsReady } from '@/utils/library/libraryReady';
import { import {
emitLiveSearchDebug, emitLiveSearchDebug,
searchHitCounts, searchHitCounts,
searchResultSamples, searchResultSamples,
} from '../utils/library/liveSearchDebug'; } from '@/utils/library/liveSearchDebug';
import { import {
logLibrarySearch, logLibrarySearch,
} from '../utils/library/libraryDevLog'; } from '@/utils/library/libraryDevLog';
import React, { useState, useEffect, useRef, useCallback } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useNavigate, useLocation } from 'react-router-dom'; 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 { Search, Disc3, Users, Music, TextSearch, Database, Globe } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useTranslation } from 'react-i18next'; 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 { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
import type { SubsonicSong } from '../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage'; import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
import { CoverArtImage } from '../cover/CoverArtImage'; import { CoverArtImage } from '@/cover/CoverArtImage';
import { COVER_DENSE_SEARCH_CSS_PX } from '../cover/layoutSizes'; import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
import { albumCoverRef } from '../cover/ref'; import { albumCoverRef } from '@/cover/ref';
import { showToast } from '../utils/ui/toast'; import { showToast } from '@/utils/ui/toast';
import { useShareSearch } from '../hooks/useShareSearch'; import { useShareSearch } from '@/features/search/hooks/useShareSearch';
import ShareSearchResults from './search/ShareSearchResults'; import ShareSearchResults from '@/features/search/components/ShareSearchResults';
import { import {
LiveSearchScopeBadge, LiveSearchScopeBadge,
LiveSearchScopeGhostBadge, LiveSearchScopeGhostBadge,
} from './search/liveSearchScopeUi'; } from '@/features/search/components/liveSearchScopeUi';
import { import {
createLiveSearchScopeBackspaceState, createLiveSearchScopeBackspaceState,
handleLiveSearchScopeBackspace, handleLiveSearchScopeBackspace,
@@ -52,9 +52,9 @@ import {
noteLiveSearchScopeQueryInput, noteLiveSearchScopeQueryInput,
resetLiveSearchScopeBackspaceState, resetLiveSearchScopeBackspaceState,
resolveLiveSearchScopeGhost, resolveLiveSearchScopeGhost,
} from './search/liveSearchScope'; } from '@/features/search/components/liveSearchScope';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
import { resolveIndexKey } from '../utils/server/serverIndexKey'; import { resolveIndexKey } from '@/utils/server/serverIndexKey';
type LiveSearchSource = 'local' | 'network'; type LiveSearchSource = 'local' | 'network';
@@ -1,21 +1,21 @@
import { describe, expect, it, vi, beforeEach } from 'vitest'; import { describe, expect, it, vi, beforeEach } from 'vitest';
import { screen } from '@testing-library/react'; import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { renderWithProviders } from '../test/helpers/renderWithProviders'; import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import MobileSearchOverlay from './MobileSearchOverlay'; import MobileSearchOverlay from '@/features/search/components/MobileSearchOverlay';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
// The overlay's only behaviour-bearing change in PR #1165 was renaming the // The overlay's only behaviour-bearing change in PR #1165 was renaming the
// recent-search handler `useRecent` → `applyRecentSearch` (it was a plain // recent-search handler `useRecent` → `applyRecentSearch` (it was a plain
// function mis-flagged as a hook). Smoke-test that the recent-search path still // 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. // applies the term to the live-search store. Heavy collaborators are stubbed.
vi.mock('../hooks/useShareSearch', () => ({ useShareSearch: () => ({ shareMatch: null }) })); vi.mock('@/features/search/hooks/useShareSearch', () => ({ useShareSearch: () => ({ shareMatch: null }) }));
vi.mock('../api/subsonicSearch', () => ({ vi.mock('@/api/subsonicSearch', () => ({
search: vi.fn(() => Promise.resolve({ artists: [], albums: [], songs: [] })), search: vi.fn(() => Promise.resolve({ artists: [], albums: [], songs: [] })),
})); }));
vi.mock('../cover/AlbumCoverArtImage', () => ({ AlbumCoverArtImage: () => null })); vi.mock('@/cover/AlbumCoverArtImage', () => ({ AlbumCoverArtImage: () => null }));
vi.mock('../cover/ArtistCoverArtImage', () => ({ ArtistCoverArtImage: () => null })); vi.mock('@/cover/ArtistCoverArtImage', () => ({ ArtistCoverArtImage: () => null }));
vi.mock('../cover/CoverArtImage', () => ({ CoverArtImage: () => null })); vi.mock('@/cover/CoverArtImage', () => ({ CoverArtImage: () => null }));
const RECENT_KEY = 'psysonic_recent_searches'; const RECENT_KEY = 'psysonic_recent_searches';
@@ -1,28 +1,28 @@
import { search } from '../api/subsonicSearch'; import { search } from '@/api/subsonicSearch';
import type { SearchResults, SubsonicArtist } from '../api/subsonicTypes'; import type { SearchResults, SubsonicArtist } from '@/api/subsonicTypes';
import { songToTrack } from '../utils/playback/songToTrack'; import { songToTrack } from '@/utils/playback/songToTrack';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-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 { X, Search, Disc3, Users, Music, Music2, Clock, ChevronRight } from 'lucide-react';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage'; import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
import { AlbumCoverArtImage } from '../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { ArtistCoverArtImage } from '../cover/ArtistCoverArtImage'; import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
import { CoverArtImage } from '../cover/CoverArtImage'; import { CoverArtImage } from '@/cover/CoverArtImage';
import { albumCoverRefForSong } from '../cover/ref'; import { albumCoverRefForSong } from '@/cover/ref';
import { showToast } from '../utils/ui/toast'; import { showToast } from '@/utils/ui/toast';
import { albumArtistDisplayName } from '../utils/album/deriveAlbumHeaderArtistRefs'; import { albumArtistDisplayName } from '@/utils/album/deriveAlbumHeaderArtistRefs';
import { useShareSearch } from '../hooks/useShareSearch'; import { useShareSearch } from '@/features/search/hooks/useShareSearch';
import ShareSearchResults from './search/ShareSearchResults'; import ShareSearchResults from '@/features/search/components/ShareSearchResults';
import { import {
LiveSearchScopeBadge, LiveSearchScopeBadge,
LiveSearchScopeGhostBadge, LiveSearchScopeGhostBadge,
} from './search/liveSearchScopeUi'; } from '@/features/search/components/liveSearchScopeUi';
import { import {
createLiveSearchScopeBackspaceState, createLiveSearchScopeBackspaceState,
handleLiveSearchScopeBackspace, handleLiveSearchScopeBackspace,
@@ -32,7 +32,7 @@ import {
noteLiveSearchScopeQueryInput, noteLiveSearchScopeQueryInput,
resetLiveSearchScopeBackspaceState, resetLiveSearchScopeBackspaceState,
resolveLiveSearchScopeGhost, resolveLiveSearchScopeGhost,
} from './search/liveSearchScope'; } from '@/features/search/components/liveSearchScope';
const STORAGE_KEY = 'psysonic_recent_searches'; const STORAGE_KEY = 'psysonic_recent_searches';
const MAX_RECENT = 6; const MAX_RECENT = 6;
@@ -2,16 +2,16 @@ import React, { useEffect } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Music, X } from 'lucide-react'; import { Music, X } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { SubsonicSong } from '../../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import type { ServerProfile } from '../../store/authStoreTypes'; import type { ServerProfile } from '@/store/authStoreTypes';
import { formatTrackTime } from '../../utils/format/formatDuration'; import { formatTrackTime } from '@/utils/format/formatDuration';
import type { ShareQueuePreviewState } from '../../hooks/useShareQueuePreview'; import type { ShareQueuePreviewState } from '@/features/search/hooks/useShareQueuePreview';
import { sharePayloadTotal, type QueueableShareSearchPayload } from '../../utils/share/shareSearch'; import { sharePayloadTotal, type QueueableShareSearchPayload } from '@/utils/share/shareSearch';
import OverlayScrollArea from '@/ui/OverlayScrollArea'; import OverlayScrollArea from '@/ui/OverlayScrollArea';
import { usePlayerStore } from '../../store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { COVER_DENSE_SEARCH_CSS_PX } from '../../cover/layoutSizes'; import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '../../cover/types'; import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '@/cover/types';
import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
type ShareQueuePreviewModalProps = { type ShareQueuePreviewModalProps = {
open: boolean; open: boolean;
@@ -2,19 +2,19 @@ import React, { useEffect, useState } from 'react';
import { Disc3, Eye, Link2, ListPlus, Music, Users } from 'lucide-react'; import { Disc3, Eye, Link2, ListPlus, Music, Users } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { TFunction } from 'i18next'; import type { TFunction } from 'i18next';
import type { SubsonicArtist } from '../../api/subsonicTypes'; import type { SubsonicArtist } from '@/api/subsonicTypes';
import type { ServerProfile } from '../../store/authStoreTypes'; import type { ServerProfile } from '@/store/authStoreTypes';
import { songToTrack } from '../../utils/playback/songToTrack'; import { songToTrack } from '@/utils/playback/songToTrack';
import { activateShareSearchServer } from '../../utils/share/enqueueShareSearchPayload'; import { activateShareSearchServer } from '@/utils/share/enqueueShareSearchPayload';
import { sharePayloadTotal, type ShareSearchMatch } from '../../utils/share/shareSearch'; import { sharePayloadTotal, type ShareSearchMatch } from '@/utils/share/shareSearch';
import type { ShareSearchPreviewState } from '../../hooks/useShareSearchPreview'; import type { ShareSearchPreviewState } from '@/features/search/hooks/useShareSearchPreview';
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage'; import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
import { AlbumCoverArtImage } from '../../cover/AlbumCoverArtImage'; import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
import { ArtistCoverArtImage } from '../../cover/ArtistCoverArtImage'; import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
import { COVER_DENSE_SEARCH_CSS_PX } from '../../cover/layoutSizes'; import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '../../cover/types'; import { COVER_SCOPE_ACTIVE, type CoverServerScope } from '@/cover/types';
import { useShareQueuePreview } from '../../hooks/useShareQueuePreview'; import { useShareQueuePreview } from '@/features/search/hooks/useShareQueuePreview';
import ShareQueuePreviewModal from './ShareQueuePreviewModal'; import ShareQueuePreviewModal from '@/features/search/components/ShareQueuePreviewModal';
type ShareSearchResultsProps = { type ShareSearchResultsProps = {
variant: 'desktop' | 'mobile'; variant: 'desktop' | 'mobile';
@@ -1,10 +1,10 @@
import type { KeyboardEvent, MouseEvent } from 'react'; import type { KeyboardEvent, MouseEvent } from 'react';
import { ALL_NAV_ITEMS } from '../../config/navItems'; import { ALL_NAV_ITEMS } from '@/config/navItems';
import type { LiveSearchScope } from '../../store/liveSearchScopeStore'; import type { LiveSearchScope } from '@/store/liveSearchScopeStore';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '../../store/albumBrowseSessionStore'; import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
import { isTracksBrowsePath } from '../../store/advancedSearchSessionStore'; import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
import { isArtistsBrowsePath } from '../../store/artistBrowseSessionStore'; import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore';
import { isComposersBrowsePath } from '../../store/composerBrowseSessionStore'; import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
export const SCOPE_NAV_ITEM: Record<LiveSearchScope, keyof typeof ALL_NAV_ITEMS> = { export const SCOPE_NAV_ITEM: Record<LiveSearchScope, keyof typeof ALL_NAV_ITEMS> = {
artists: 'artists', artists: 'artists',
@@ -11,7 +11,7 @@ import {
noteLiveSearchScopeQueryInput, noteLiveSearchScopeQueryInput,
resetLiveSearchScopeBackspaceState, resetLiveSearchScopeBackspaceState,
resolveLiveSearchScopeGhost, resolveLiveSearchScopeGhost,
} from './liveSearchScope'; } from '@/features/search/components/liveSearchScope';
function keyEvent( function keyEvent(
key: string, key: string,
@@ -1,13 +1,13 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ALL_NAV_ITEMS } from '../../config/navItems'; import { ALL_NAV_ITEMS } from '@/config/navItems';
import type { LiveSearchScope } from '../../store/liveSearchScopeStore'; import type { LiveSearchScope } from '@/store/liveSearchScopeStore';
import { import {
SCOPE_NAV_ITEM, SCOPE_NAV_ITEM,
handleLiveSearchScopeBadgeClick, handleLiveSearchScopeBadgeClick,
handleLiveSearchScopeGhostClick, handleLiveSearchScopeGhostClick,
liveSearchScopeBadgeTooltipKey, liveSearchScopeBadgeTooltipKey,
liveSearchScopeGhostTooltipKey, liveSearchScopeGhostTooltipKey,
} from './liveSearchScope'; } from '@/features/search/components/liveSearchScope';
type LiveSearchScopeIconProps = { type LiveSearchScopeIconProps = {
scope: LiveSearchScope; scope: LiveSearchScope;
@@ -1,6 +1,6 @@
import { describe, expect, it, beforeEach } from 'vitest'; import { describe, expect, it, beforeEach } from 'vitest';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
import { syncLiveSearchRouteScope } from './useLiveSearchRouteScope'; import { syncLiveSearchRouteScope } from '@/features/search/hooks/useLiveSearchRouteScope';
describe('syncLiveSearchRouteScope', () => { describe('syncLiveSearchRouteScope', () => {
beforeEach(() => { beforeEach(() => {
@@ -1,10 +1,10 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '../store/albumBrowseSessionStore'; import { isAlbumsBrowsePath, isNewReleasesBrowsePath } from '@/store/albumBrowseSessionStore';
import { isArtistsBrowsePath } from '../store/artistBrowseSessionStore'; import { isArtistsBrowsePath } from '@/store/artistBrowseSessionStore';
import { isTracksBrowsePath } from '../store/advancedSearchSessionStore'; import { isTracksBrowsePath } from '@/store/advancedSearchSessionStore';
import { isComposersBrowsePath } from '../store/composerBrowseSessionStore'; import { isComposersBrowsePath } from '@/store/composerBrowseSessionStore';
import { useLiveSearchScopeStore } from '../store/liveSearchScopeStore'; import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore';
/** Keep scope badge in sync with browse routes; clear field text when leaving browse. */ /** Keep scope badge in sync with browse routes; clear field text when leaving browse. */
export function syncLiveSearchRouteScope(pathname: string): void { export function syncLiveSearchRouteScope(pathname: string): void {
@@ -1,10 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { SubsonicSong } from '../api/subsonicTypes'; import type { SubsonicSong } from '@/api/subsonicTypes';
import { import {
resolveShareSearchPayload, resolveShareSearchPayload,
type ShareSearchResolveResult, type ShareSearchResolveResult,
} from '../utils/share/enqueueShareSearchPayload'; } from '@/utils/share/enqueueShareSearchPayload';
import type { QueueableShareSearchPayload } from '../utils/share/shareSearch'; import type { QueueableShareSearchPayload } from '@/utils/share/shareSearch';
export type ShareQueuePreviewState = export type ShareQueuePreviewState =
| { status: 'idle' } | { status: 'idle' }
@@ -1,18 +1,18 @@
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useNavigateToAlbum } from './useNavigateToAlbum'; import { useNavigateToAlbum } from '@/hooks/useNavigateToAlbum';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { import {
activateShareSearchServer, activateShareSearchServer,
enqueueShareSearchPayload, enqueueShareSearchPayload,
} from '../utils/share/enqueueShareSearchPayload'; } from '@/utils/share/enqueueShareSearchPayload';
import type { ServerProfile } from '../store/authStoreTypes'; import type { ServerProfile } from '@/store/authStoreTypes';
import { findServerIdForShareUrl } from '../utils/share/shareLink'; import { findServerIdForShareUrl } from '@/utils/share/shareLink';
import { shareServerOriginLabel } from '../utils/share/shareServerOriginLabel'; import { shareServerOriginLabel } from '@/utils/share/shareServerOriginLabel';
import { parseShareSearchText } from '../utils/share/shareSearch'; import { parseShareSearchText } from '@/utils/share/shareSearch';
import { serverIndexKeyFromUrl } from '../utils/server/serverIndexKey'; import { serverIndexKeyFromUrl } from '@/utils/server/serverIndexKey';
import { useShareSearchPreview } from './useShareSearchPreview'; import { useShareSearchPreview } from '@/features/search/hooks/useShareSearchPreview';
export function useShareSearch(query: string, onSuccess?: () => void) { export function useShareSearch(query: string, onSuccess?: () => void) {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,11 +1,11 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonicTypes'; import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from '@/api/subsonicTypes';
import { import {
resolveShareSearchAlbum, resolveShareSearchAlbum,
resolveShareSearchArtist, resolveShareSearchArtist,
resolveShareSearchPayload, resolveShareSearchPayload,
} from '../utils/share/enqueueShareSearchPayload'; } from '@/utils/share/enqueueShareSearchPayload';
import type { ShareSearchMatch } from '../utils/share/shareSearch'; import type { ShareSearchMatch } from '@/utils/share/shareSearch';
export interface ShareSearchPreviewState { export interface ShareSearchPreviewState {
shareTrackSong: SubsonicSong | null; shareTrackSong: SubsonicSong | null;
+15
View File
@@ -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';
@@ -1,32 +1,32 @@
import { getGenres, getAlbumsByGenre } from '../api/subsonicGenres'; import { getGenres, getAlbumsByGenre } from '@/api/subsonicGenres';
import { search, searchSongsPaged } from '../api/subsonicSearch'; import { search, searchSongsPaged } from '@/api/subsonicSearch';
import { getRandomSongs } from '../api/subsonicLibrary'; import { getRandomSongs } from '@/api/subsonicLibrary';
import type { SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong } from '../api/subsonicTypes'; import type { SubsonicGenre, SubsonicArtist, SubsonicAlbum, SubsonicSong } from '@/api/subsonicTypes';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useLocation, useNavigate, useNavigationType, useSearchParams } from 'react-router-dom'; import { useLocation, useNavigate, useNavigationType, useSearchParams } from 'react-router-dom';
import { SlidersVertical, Search, X } from 'lucide-react'; import { SlidersVertical, Search, X } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import AlbumRow from '../components/AlbumRow'; import AlbumRow from '@/components/AlbumRow';
import ArtistRow from '../components/ArtistRow'; import ArtistRow from '@/components/ArtistRow';
import PagedSongList from '../components/PagedSongList'; import PagedSongList from '@/components/PagedSongList';
import CustomSelect from '@/ui/CustomSelect'; import CustomSelect from '@/ui/CustomSelect';
import StarFilterButton from '../components/StarFilterButton'; import StarFilterButton from '@/components/StarFilterButton';
import { tooltipAttrs } from '@/ui/tooltipAttrs'; import { tooltipAttrs } from '@/ui/tooltipAttrs';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll'; import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '@/store/authStore';
import { usePlayerStore } from '../store/playerStore'; import { usePlayerStore } from '@/store/playerStore';
import { isAdvancedSearchLeaveTargetPath } from '../store/albumBrowseSessionStore'; import { isAdvancedSearchLeaveTargetPath } from '@/store/albumBrowseSessionStore';
import { import {
isAdvancedSearchPath, isAdvancedSearchPath,
isAdvancedSearchPanelPath, isAdvancedSearchPanelPath,
isTracksBrowsePath, isTracksBrowsePath,
useAdvancedSearchSessionStore, useAdvancedSearchSessionStore,
type AdvancedSearchSessionStash, type AdvancedSearchSessionStash,
} from '../store/advancedSearchSessionStore'; } from '@/store/advancedSearchSessionStore';
import { import {
readAdvancedSearchRestore, readAdvancedSearchRestore,
shouldRestoreAdvancedSearchSession, shouldRestoreAdvancedSearchSession,
} from '../utils/navigation/albumDetailNavigation'; } from '@/utils/navigation/albumDetailNavigation';
import { import {
clearAdvancedSearchLeaveSnapshots, clearAdvancedSearchLeaveSnapshots,
consumeAdvancedSearchLeavingForDetail, consumeAdvancedSearchLeavingForDetail,
@@ -35,36 +35,36 @@ import {
registerAdvancedSearchSessionProvider, registerAdvancedSearchSessionProvider,
resolveAdvancedSearchLeaveSnapshot, resolveAdvancedSearchLeaveSnapshot,
type AdvancedSearchLeaveSnapshot, type AdvancedSearchLeaveSnapshot,
} from '../utils/navigation/advancedSearchScrollSnapshot'; } from '@/utils/navigation/advancedSearchScrollSnapshot';
import { restoreMainViewportScroll } from '../utils/navigation/restoreMainViewportScroll'; import { restoreMainViewportScroll } from '@/utils/navigation/restoreMainViewportScroll';
import { import {
loadMoreLocalSongs, loadMoreLocalSongs,
runNetworkAdvancedTextSearch, runNetworkAdvancedTextSearch,
runNetworkAdvancedYearAlbums, runNetworkAdvancedYearAlbums,
tryRunLocalAdvancedSearch, tryRunLocalAdvancedSearch,
} from '../utils/library/advancedSearchLocal'; } from '@/utils/library/advancedSearchLocal';
import { isLosslessSuffix } from '../utils/library/losslessFormats'; import { isLosslessSuffix } from '@/utils/library/losslessFormats';
import { LOSSLESS_MODE_QUERY } from '../utils/library/losslessMode'; import { LOSSLESS_MODE_QUERY } from '@/utils/library/losslessMode';
import { OXIMEDIA_MOOD_SEARCH_ENABLED } from '../utils/library/trackEnrichment'; import { OXIMEDIA_MOOD_SEARCH_ENABLED } from '@/utils/library/trackEnrichment';
import { raceSearchSources } from '../utils/library/searchRace'; import { raceSearchSources } from '@/utils/library/searchRace';
import { logLibrarySearch } from '../utils/library/libraryDevLog'; import { logLibrarySearch } from '@/utils/library/libraryDevLog';
import { import {
browseRaceCountsFullSearch, browseRaceCountsFullSearch,
loadMoreLocalBrowseSongs, loadMoreLocalBrowseSongs,
raceBrowseWithLocalFallback, raceBrowseWithLocalFallback,
runLocalBrowseFullSearch, runLocalBrowseFullSearch,
runNetworkBrowseFullSearch, runNetworkBrowseFullSearch,
} from '../utils/library/browseTextSearch'; } from '@/utils/library/browseTextSearch';
import { useLibraryIndexStore } from '../store/libraryIndexStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { MOOD_GROUP_IDS } from '../config/moodGroups'; import { MOOD_GROUP_IDS } from '@/config/moodGroups';
import { usePerfProbeFlags } from '../utils/perf/perfFlags'; import { usePerfProbeFlags } from '@/utils/perf/perfFlags';
import { useSongBrowseList, type SongBrowseListRestore } from '../hooks/useSongBrowseList'; import { useSongBrowseList, type SongBrowseListRestore } from '@/hooks/useSongBrowseList';
import TracksPageChrome from '../components/tracks/TracksPageChrome'; import TracksPageChrome from '@/components/tracks/TracksPageChrome';
import SongBrowseSection from '../components/tracks/SongBrowseSection'; import SongBrowseSection from '@/components/tracks/SongBrowseSection';
import { import {
useLiveSearchScopeStore, useLiveSearchScopeStore,
useScopedBrowseSearchQuery, useScopedBrowseSearchQuery,
} from '../store/liveSearchScopeStore'; } from '@/store/liveSearchScopeStore';
const MOOD_UI_ENABLED = OXIMEDIA_MOOD_SEARCH_ENABLED; const MOOD_UI_ENABLED = OXIMEDIA_MOOD_SEARCH_ENABLED;