diff --git a/src/features/search/components/LiveSearch.tsx b/src/features/search/components/LiveSearch.tsx index 81e6ee55..dbda5cd1 100644 --- a/src/features/search/components/LiveSearch.tsx +++ b/src/features/search/components/LiveSearch.tsx @@ -23,21 +23,15 @@ import { import React, { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { useNavigateToAlbum } from '@/features/album'; -import { Search, Disc3, Users, Music, TextSearch, Database, Globe } from 'lucide-react'; +import { Search, TextSearch } from 'lucide-react'; import { usePlayerStore } from '@/features/playback/store/playerStore'; import { useAuthStore } from '@/store/authStore'; import { useLibraryIndexStore } from '@/store/libraryIndexStore'; import { useTranslation } from 'react-i18next'; -import { albumArtistDisplayName } from '@/features/album'; -import { - LiveSearchAlbumThumb, - LiveSearchSongThumb, - LiveSearchArtistThumb, -} from '@/features/search/components/liveSearchResultThumbs'; import { useLiveSearchHeaderCollapse } from '@/features/search/hooks/useLiveSearchHeaderCollapse'; +import LiveSearchDropdown, { type LiveSearchSource } from '@/features/search/components/LiveSearchDropdown'; import { showToast } from '@/lib/dom/toast'; import { useShareSearch } from '@/features/search/hooks/useShareSearch'; -import ShareSearchResults from '@/features/search/components/ShareSearchResults'; import { LiveSearchScopeBadge, LiveSearchScopeGhostBadge, @@ -55,8 +49,6 @@ import { import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; import { resolveIndexKey } from '@/lib/server/serverIndexKey'; -type LiveSearchSource = 'local' | 'network'; - export default function LiveSearch() { const { t } = useTranslation(); const query = useLiveSearchScopeStore(s => s.query); @@ -79,10 +71,7 @@ export default function LiveSearch() { const navigate = useNavigate(); const navigateToAlbum = useNavigateToAlbum(); const enqueue = usePlayerStore(state => state.enqueue); - const openContextMenu = usePlayerStore(state => state.openContextMenu); const ctxIsOpen = usePlayerStore(state => state.contextMenu.isOpen); - const ctxItemId = usePlayerStore(state => (state.contextMenu.item as { id?: string } | null)?.id); - const ctxType = usePlayerStore(state => state.contextMenu.type); const ref = useRef(null); const dropdownRef = useRef(null); const inputRef = useRef(null); @@ -366,10 +355,6 @@ export default function LiveSearch() { return () => document.removeEventListener('mousedown', handler); }, [ctxIsOpen]); - const hasResults = - !!share.shareMatch || - (results && (results.artists.length || results.albums.length || results.songs.length)); - // Flat list of all navigable items for keyboard nav const flatItems = share.shareMatch && share.hasShareKeyboardTarget ? [ { @@ -523,155 +508,15 @@ export default function LiveSearch() { {open && !isLiveSearchDropdownBlocked(scope) && ( -
- {searchSource && !share.shareMatch && ( -
- {searchSource === 'local' ? ( - - ) : ( - - )} - - {t( - searchSource === 'local' - ? 'search.localIndexBadge' - : 'search.networkSearchBadge', - )} - -
- )} - - {!hasResults && !loading && ( -
{t('search.noResults', { query: query.trim() })}
- )} - - {share.shareMatch && ( - void share.enqueueShareMatch()} - onOpenAlbum={share.openShareAlbum} - onOpenArtist={share.openShareArtist} - onOpenComposer={share.openShareComposer} - onContextMenu={(e, item, type) => openContextMenu(e.clientX, e.clientY, item, type)} - shareTrackSong={share.shareTrackSong} - shareTrackResolving={share.shareTrackResolving} - shareTrackUnavailable={share.shareTrackUnavailable} - shareAlbum={share.shareAlbum} - shareAlbumResolving={share.shareAlbumResolving} - shareAlbumUnavailable={share.shareAlbumUnavailable} - shareArtist={share.shareArtist} - shareArtistResolving={share.shareArtistResolving} - shareArtistUnavailable={share.shareArtistUnavailable} - shareComposer={share.shareComposer} - shareComposerResolving={share.shareComposerResolving} - shareComposerUnavailable={share.shareComposerUnavailable} - /> - )} - - {(() => { - if (share.shareMatch) return null; - let idx = 0; - return <> - {results?.artists.length ? ( -
-
{t('search.artists')}
- {results.artists.map(a => { - const i = idx++; - const isCtxActive = ctxIsOpen && ctxType === 'artist' && ctxItemId === a.id; - return ( - - ); - })} -
- ) : null} - - {results?.albums.length ? ( -
-
{t('search.albums')}
- {results.albums.map(a => { - const i = idx++; - const isCtxActive = ctxIsOpen && ctxType === 'album' && ctxItemId === a.id; - return ( - - ); - })} -
- ) : null} - - {results?.songs.length ? ( -
-
{t('search.songs')}
- {results.songs.map(s => { - const i = idx++; - const isCtxActive = ctxIsOpen && ctxType === 'song' && ctxItemId === s.id; - return ( - - ); - })} -
- ) : null} - ; - })()} -
+ )} ); diff --git a/src/features/search/components/LiveSearchDropdown.tsx b/src/features/search/components/LiveSearchDropdown.tsx new file mode 100644 index 00000000..7ae378a2 --- /dev/null +++ b/src/features/search/components/LiveSearchDropdown.tsx @@ -0,0 +1,207 @@ +import { type RefObject } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Users, Disc3, Music, Database, Globe } from 'lucide-react'; +import { useNavigateToAlbum, albumArtistDisplayName } from '@/features/album'; +import { usePlayerStore } from '@/features/playback/store/playerStore'; +import { useLiveSearchScopeStore } from '@/store/liveSearchScopeStore'; +import { songToTrack } from '@/lib/media/songToTrack'; +import { showToast } from '@/lib/dom/toast'; +import type { SearchResults } from '@/lib/api/subsonicTypes'; +import ShareSearchResults from '@/features/search/components/ShareSearchResults'; +import { + LiveSearchAlbumThumb, + LiveSearchSongThumb, + LiveSearchArtistThumb, +} from '@/features/search/components/liveSearchResultThumbs'; +import type { useShareSearch } from '@/features/search/hooks/useShareSearch'; + +export type LiveSearchSource = 'local' | 'network'; + +interface LiveSearchDropdownProps { + dropdownRef: RefObject; + results: SearchResults | null; + searchSource: LiveSearchSource | null; + activeIndex: number; + loading: boolean; + share: ReturnType; + setOpen: (open: boolean) => void; +} + +/** Live-search results overlay: source badge, share match, and the artist/album/song sections. */ +export default function LiveSearchDropdown({ + dropdownRef, + results, + searchSource, + activeIndex, + loading, + share, + setOpen, +}: LiveSearchDropdownProps) { + const { t } = useTranslation(); + const query = useLiveSearchScopeStore(s => s.query); + const setQuery = useLiveSearchScopeStore(s => s.setQuery); + const navigate = useNavigate(); + const navigateToAlbum = useNavigateToAlbum(); + const enqueue = usePlayerStore(state => state.enqueue); + const openContextMenu = usePlayerStore(state => state.openContextMenu); + const ctxIsOpen = usePlayerStore(state => state.contextMenu.isOpen); + const ctxItemId = usePlayerStore(state => (state.contextMenu.item as { id?: string } | null)?.id); + const ctxType = usePlayerStore(state => state.contextMenu.type); + + const hasResults = + !!share.shareMatch || + (results && (results.artists.length || results.albums.length || results.songs.length)); + + return ( +
+ {searchSource && !share.shareMatch && ( +
+ {searchSource === 'local' ? ( + + ) : ( + + )} + + {t( + searchSource === 'local' + ? 'search.localIndexBadge' + : 'search.networkSearchBadge', + )} + +
+ )} + + {!hasResults && !loading && ( +
{t('search.noResults', { query: query.trim() })}
+ )} + + {share.shareMatch && ( + void share.enqueueShareMatch()} + onOpenAlbum={share.openShareAlbum} + onOpenArtist={share.openShareArtist} + onOpenComposer={share.openShareComposer} + onContextMenu={(e, item, type) => openContextMenu(e.clientX, e.clientY, item, type)} + shareTrackSong={share.shareTrackSong} + shareTrackResolving={share.shareTrackResolving} + shareTrackUnavailable={share.shareTrackUnavailable} + shareAlbum={share.shareAlbum} + shareAlbumResolving={share.shareAlbumResolving} + shareAlbumUnavailable={share.shareAlbumUnavailable} + shareArtist={share.shareArtist} + shareArtistResolving={share.shareArtistResolving} + shareArtistUnavailable={share.shareArtistUnavailable} + shareComposer={share.shareComposer} + shareComposerResolving={share.shareComposerResolving} + shareComposerUnavailable={share.shareComposerUnavailable} + /> + )} + + {(() => { + if (share.shareMatch) return null; + let idx = 0; + return <> + {results?.artists.length ? ( +
+
{t('search.artists')}
+ {results.artists.map(a => { + const i = idx++; + const isCtxActive = ctxIsOpen && ctxType === 'artist' && ctxItemId === a.id; + return ( + + ); + })} +
+ ) : null} + + {results?.albums.length ? ( +
+
{t('search.albums')}
+ {results.albums.map(a => { + const i = idx++; + const isCtxActive = ctxIsOpen && ctxType === 'album' && ctxItemId === a.id; + return ( + + ); + })} +
+ ) : null} + + {results?.songs.length ? ( +
+
{t('search.songs')}
+ {results.songs.map(s => { + const i = idx++; + const isCtxActive = ctxIsOpen && ctxType === 'song' && ctxItemId === s.id; + return ( + + ); + })} +
+ ) : null} + ; + })()} +
+ ); +}