mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(search): co-locate search feature into features/search
This commit is contained in:
@@ -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'));
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 = [
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
+8
-8
@@ -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';
|
||||||
|
|
||||||
+17
-17
@@ -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;
|
||||||
+9
-9
@@ -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;
|
||||||
+12
-12
@@ -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';
|
||||||
+6
-6
@@ -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',
|
||||||
+1
-1
@@ -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,
|
||||||
+3
-3
@@ -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;
|
||||||
+2
-2
@@ -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(() => {
|
||||||
+5
-5
@@ -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 {
|
||||||
+3
-3
@@ -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();
|
||||||
+3
-3
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
Reference in New Issue
Block a user