mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
refactor(search): extract advanced-search results area into a component
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { type MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlbumRow } from '@/features/album';
|
||||
import { ArtistRow } from '@/features/artist';
|
||||
import PagedSongList from '@/features/search/components/PagedSongList';
|
||||
import { LOSSLESS_MODE_QUERY } from '@/lib/library/losslessMode';
|
||||
import type { Results, SearchOpts } from '@/features/search/searchBrowseTypes';
|
||||
|
||||
interface AdvancedSearchResultsProps {
|
||||
showAdvancedPanel: boolean;
|
||||
hasSearched: boolean;
|
||||
loading: boolean;
|
||||
basicSearchMode: boolean;
|
||||
query: string;
|
||||
filteredResults: Results | null;
|
||||
activeSearch: SearchOpts | null;
|
||||
genreNote: boolean;
|
||||
songsHasMore: boolean;
|
||||
loadingMoreSongs: boolean;
|
||||
loadMoreSongs: () => void;
|
||||
artistRowScrollLeftRestoreRef: MutableRefObject<number>;
|
||||
artistRowScrollLeftRef: MutableRefObject<number>;
|
||||
albumRowScrollLeftRestoreRef: MutableRefObject<number>;
|
||||
albumRowScrollLeftRef: MutableRefObject<number>;
|
||||
}
|
||||
|
||||
/** Results area for the search shell: empty/loading/no-results states + artist/album/song sections. */
|
||||
export default function AdvancedSearchResults({
|
||||
showAdvancedPanel,
|
||||
hasSearched,
|
||||
loading,
|
||||
basicSearchMode,
|
||||
query,
|
||||
filteredResults,
|
||||
activeSearch,
|
||||
genreNote,
|
||||
songsHasMore,
|
||||
loadingMoreSongs,
|
||||
loadMoreSongs,
|
||||
artistRowScrollLeftRestoreRef,
|
||||
artistRowScrollLeftRef,
|
||||
albumRowScrollLeftRestoreRef,
|
||||
albumRowScrollLeftRef,
|
||||
}: AdvancedSearchResultsProps) {
|
||||
const { t } = useTranslation();
|
||||
const total = filteredResults
|
||||
? filteredResults.artists.length + filteredResults.albums.length + filteredResults.songs.length
|
||||
: 0;
|
||||
|
||||
if (showAdvancedPanel && !hasSearched) {
|
||||
return (
|
||||
<div className="empty-state" style={{ opacity: 0.6 }}>
|
||||
{t('search.advancedEmpty')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (total === 0) {
|
||||
return (
|
||||
<div className="empty-state">
|
||||
{basicSearchMode && query.trim()
|
||||
? t('search.noResults', { query })
|
||||
: t('search.advancedNoResults')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||
|
||||
{filteredResults && filteredResults.artists.length > 0 && (
|
||||
<div data-advanced-search-artist-row>
|
||||
<ArtistRow
|
||||
title={
|
||||
basicSearchMode
|
||||
? t('search.artists')
|
||||
: `${t('search.artists')} (${filteredResults.artists.length})`
|
||||
}
|
||||
artists={filteredResults.artists}
|
||||
artistLinkQuery={activeSearch?.losslessOnly ? LOSSLESS_MODE_QUERY : undefined}
|
||||
restoreScrollLeft={
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
artistRowScrollLeftRestoreRef.current > 0
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
? artistRowScrollLeftRestoreRef.current
|
||||
: undefined
|
||||
}
|
||||
onScrollLeftSnapshot={(left) => {
|
||||
artistRowScrollLeftRef.current = left;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredResults && filteredResults.albums.length > 0 && (
|
||||
<div data-advanced-search-album-row>
|
||||
<AlbumRow
|
||||
title={
|
||||
basicSearchMode
|
||||
? t('search.albums')
|
||||
: `${t('search.albums')} (${filteredResults.albums.length})`
|
||||
}
|
||||
albums={filteredResults.albums}
|
||||
albumLinkQuery={activeSearch?.losslessOnly ? LOSSLESS_MODE_QUERY : undefined}
|
||||
windowArtworkByViewport
|
||||
initialArtworkBudget={12}
|
||||
restoreScrollLeft={
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
albumRowScrollLeftRestoreRef.current > 0
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
? albumRowScrollLeftRestoreRef.current
|
||||
: undefined
|
||||
}
|
||||
onScrollLeftSnapshot={(left) => {
|
||||
albumRowScrollLeftRef.current = left;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredResults && filteredResults.songs.length > 0 && (
|
||||
<section>
|
||||
<h2 className="section-title" style={{ marginBottom: '0.75rem' }}>
|
||||
{t('search.songs')}
|
||||
{genreNote && (
|
||||
<span style={{ fontSize: 12, fontWeight: 400, color: 'var(--text-muted)', marginLeft: '0.75rem' }}>
|
||||
— {t('search.advancedGenreNote')}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<PagedSongList
|
||||
songs={filteredResults.songs}
|
||||
hasMore={songsHasMore}
|
||||
loadingMore={loadingMoreSongs}
|
||||
onLoadMore={loadMoreSongs}
|
||||
showBpm={!!(activeSearch?.bpmFrom || activeSearch?.bpmTo)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,8 @@ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSta
|
||||
import { useLocation, useNavigate, useNavigationType, useSearchParams } from 'react-router-dom';
|
||||
import { SlidersVertical, Search } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlbumRow } from '@/features/album';
|
||||
import { ArtistRow } from '@/features/artist';
|
||||
import PagedSongList from '@/features/search/components/PagedSongList';
|
||||
import AdvancedSearchFilterPanel from '@/features/search/components/AdvancedSearchFilterPanel';
|
||||
import AdvancedSearchResults from '@/features/search/components/AdvancedSearchResults';
|
||||
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '@/constants/appScroll';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { usePlayerStore } from '@/features/playback/store/playerStore';
|
||||
@@ -43,7 +41,6 @@ import {
|
||||
tryRunLocalAdvancedSearch,
|
||||
} from '@/lib/library/advancedSearchLocal';
|
||||
import { isLosslessSuffix } from '@/lib/library/losslessFormats';
|
||||
import { LOSSLESS_MODE_QUERY } from '@/lib/library/losslessMode';
|
||||
import { OXIMEDIA_MOOD_SEARCH_ENABLED } from '@/lib/library/trackEnrichment';
|
||||
import { raceSearchSources } from '@/lib/library/searchRace';
|
||||
import { logLibrarySearch } from '@/lib/library/libraryDevLog';
|
||||
@@ -113,9 +110,6 @@ export default function SearchBrowsePage() {
|
||||
songs: results.songs.filter(s => isFav(s.id, s.starred)),
|
||||
};
|
||||
}, [results, starredOnly, starredOverrides]);
|
||||
const total = filteredResults
|
||||
? filteredResults.artists.length + filteredResults.albums.length + filteredResults.songs.length
|
||||
: 0;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(() => restoreStash?.hasSearched ?? false);
|
||||
const [genreNote, setGenreNote] = useState(() => restoreStash?.genreNote ?? false);
|
||||
@@ -911,98 +905,23 @@ export default function SearchBrowsePage() {
|
||||
)}
|
||||
|
||||
{/* ── Results ───────────────────────────────────────────── */}
|
||||
{showAdvancedPanel && !hasSearched ? (
|
||||
<div className="empty-state" style={{ opacity: 0.6 }}>
|
||||
{t('search.advancedEmpty')}
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: '4rem' }}>
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
) : total === 0 ? (
|
||||
<div className="empty-state">
|
||||
{basicSearchMode && query.trim()
|
||||
? t('search.noResults', { query })
|
||||
: t('search.advancedNoResults')}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '3rem' }}>
|
||||
|
||||
{filteredResults && filteredResults.artists.length > 0 && (
|
||||
<div data-advanced-search-artist-row>
|
||||
<ArtistRow
|
||||
title={
|
||||
basicSearchMode
|
||||
? t('search.artists')
|
||||
: `${t('search.artists')} (${filteredResults.artists.length})`
|
||||
}
|
||||
artists={filteredResults.artists}
|
||||
artistLinkQuery={activeSearch?.losslessOnly ? LOSSLESS_MODE_QUERY : undefined}
|
||||
restoreScrollLeft={
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
artistRowScrollLeftRestoreRef.current > 0
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
? artistRowScrollLeftRestoreRef.current
|
||||
: undefined
|
||||
}
|
||||
onScrollLeftSnapshot={(left) => {
|
||||
artistRowScrollLeftRef.current = left;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredResults && filteredResults.albums.length > 0 && (
|
||||
<div data-advanced-search-album-row>
|
||||
<AlbumRow
|
||||
title={
|
||||
basicSearchMode
|
||||
? t('search.albums')
|
||||
: `${t('search.albums')} (${filteredResults.albums.length})`
|
||||
}
|
||||
albums={filteredResults.albums}
|
||||
albumLinkQuery={activeSearch?.losslessOnly ? LOSSLESS_MODE_QUERY : undefined}
|
||||
windowArtworkByViewport
|
||||
initialArtworkBudget={12}
|
||||
restoreScrollLeft={
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
albumRowScrollLeftRestoreRef.current > 0
|
||||
// React Compiler refs rule: ref read imperatively outside reactive rendering; not used to compute the render output.
|
||||
// eslint-disable-next-line react-hooks/refs
|
||||
? albumRowScrollLeftRestoreRef.current
|
||||
: undefined
|
||||
}
|
||||
onScrollLeftSnapshot={(left) => {
|
||||
albumRowScrollLeftRef.current = left;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filteredResults && filteredResults.songs.length > 0 && (
|
||||
<section>
|
||||
<h2 className="section-title" style={{ marginBottom: '0.75rem' }}>
|
||||
{t('search.songs')}
|
||||
{genreNote && (
|
||||
<span style={{ fontSize: 12, fontWeight: 400, color: 'var(--text-muted)', marginLeft: '0.75rem' }}>
|
||||
— {t('search.advancedGenreNote')}
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<PagedSongList
|
||||
songs={filteredResults.songs}
|
||||
hasMore={songsHasMore}
|
||||
loadingMore={loadingMoreSongs}
|
||||
onLoadMore={loadMoreSongs}
|
||||
showBpm={!!(activeSearch?.bpmFrom || activeSearch?.bpmTo)}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<AdvancedSearchResults
|
||||
showAdvancedPanel={showAdvancedPanel}
|
||||
hasSearched={hasSearched}
|
||||
loading={loading}
|
||||
basicSearchMode={basicSearchMode}
|
||||
query={query}
|
||||
filteredResults={filteredResults}
|
||||
activeSearch={activeSearch}
|
||||
genreNote={genreNote}
|
||||
songsHasMore={songsHasMore}
|
||||
loadingMoreSongs={loadingMoreSongs}
|
||||
loadMoreSongs={loadMoreSongs}
|
||||
artistRowScrollLeftRestoreRef={artistRowScrollLeftRestoreRef}
|
||||
artistRowScrollLeftRef={artistRowScrollLeftRef}
|
||||
albumRowScrollLeftRestoreRef={albumRowScrollLeftRestoreRef}
|
||||
albumRowScrollLeftRef={albumRowScrollLeftRef}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user