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 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'));
+2 -2
View File
@@ -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';
+1 -1
View File
@@ -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 = [
+2 -2
View File
@@ -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,
@@ -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';
@@ -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';
@@ -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;
@@ -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;
@@ -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';
@@ -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<LiveSearchScope, keyof typeof ALL_NAV_ITEMS> = {
artists: 'artists',
@@ -11,7 +11,7 @@ import {
noteLiveSearchScopeQueryInput,
resetLiveSearchScopeBackspaceState,
resolveLiveSearchScopeGhost,
} from './liveSearchScope';
} from '@/features/search/components/liveSearchScope';
function keyEvent(
key: string,
@@ -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;
@@ -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(() => {
@@ -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 {
@@ -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' }
@@ -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();
@@ -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;
+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 { 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;