mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-28 10:06:47 +00:00
refactor(search): extract LiveSearch result thumbnails into a sibling module
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '@/lib/api/library';
|
import { subscribeLibrarySyncIdle, subscribeLibrarySyncProgress } from '@/lib/api/library';
|
||||||
import type { SearchResults, SubsonicArtist } from '@/lib/api/subsonicTypes';
|
import type { SearchResults } from '@/lib/api/subsonicTypes';
|
||||||
import { songToTrack } from '@/lib/media/songToTrack';
|
import { songToTrack } from '@/lib/media/songToTrack';
|
||||||
import {
|
import {
|
||||||
LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
|
LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
|
||||||
@@ -29,13 +29,11 @@ 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 '@/features/album';
|
import { albumArtistDisplayName } from '@/features/album';
|
||||||
import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
|
import {
|
||||||
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
LiveSearchAlbumThumb,
|
||||||
import { AlbumCoverArtImage } from '@/cover/AlbumCoverArtImage';
|
LiveSearchSongThumb,
|
||||||
import { ArtistCoverArtImage } from '@/cover/ArtistCoverArtImage';
|
LiveSearchArtistThumb,
|
||||||
import { CoverArtImage } from '@/cover/CoverArtImage';
|
} from '@/features/search/components/liveSearchResultThumbs';
|
||||||
import { COVER_DENSE_SEARCH_CSS_PX } from '@/cover/layoutSizes';
|
|
||||||
import { albumCoverRef } from '@/cover/ref';
|
|
||||||
import { showToast } from '@/lib/dom/toast';
|
import { showToast } from '@/lib/dom/toast';
|
||||||
import { useShareSearch } from '@/features/search/hooks/useShareSearch';
|
import { useShareSearch } from '@/features/search/hooks/useShareSearch';
|
||||||
import ShareSearchResults from '@/features/search/components/ShareSearchResults';
|
import ShareSearchResults from '@/features/search/components/ShareSearchResults';
|
||||||
@@ -58,69 +56,6 @@ import { resolveIndexKey } from '@/lib/server/serverIndexKey';
|
|||||||
|
|
||||||
type LiveSearchSource = 'local' | 'network';
|
type LiveSearchSource = 'local' | 'network';
|
||||||
|
|
||||||
function LiveSearchAlbumThumb({ albumId, coverArt }: { albumId: string; coverArt: string }) {
|
|
||||||
return (
|
|
||||||
<AlbumCoverArtImage
|
|
||||||
albumId={albumId}
|
|
||||||
coverArt={coverArt}
|
|
||||||
libraryResolve={false}
|
|
||||||
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
|
||||||
surface="dense"
|
|
||||||
className="search-result-thumb"
|
|
||||||
alt=""
|
|
||||||
ensurePriority="high"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LiveSearchSongThumb({ song }: { song: Pick<SubsonicSong, 'id' | 'albumId' | 'coverArt' | 'discNumber'> }) {
|
|
||||||
// Search results carry the per-track `mf-…` coverArt id, which the cover
|
|
||||||
// pipeline fails to resolve and the thumbnail goes blank. The album-scoped
|
|
||||||
// `al-<albumId>_0` id is what actually loads (verified in the RC1 blank-thumb
|
|
||||||
// investigation), and a song's search thumbnail is its album cover anyway —
|
|
||||||
// so fetch the album cover from the albumId. Falls back to a music glyph when
|
|
||||||
// there is no album to key on.
|
|
||||||
const albumId = song.albumId?.trim();
|
|
||||||
const coverRef = React.useMemo(
|
|
||||||
() => (albumId ? albumCoverRef(albumId, `al-${albumId}_0`) : undefined),
|
|
||||||
[albumId],
|
|
||||||
);
|
|
||||||
if (!coverRef) return <div className="search-result-icon"><Music size={14} /></div>;
|
|
||||||
return (
|
|
||||||
<CoverArtImage
|
|
||||||
coverRef={coverRef}
|
|
||||||
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
|
||||||
surface="dense"
|
|
||||||
className="search-result-thumb"
|
|
||||||
alt=""
|
|
||||||
ensurePriority="high"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LiveSearchArtistThumb({ artist }: { artist: Pick<SubsonicArtist, 'id' | 'coverArt'> }) {
|
|
||||||
const [failed, setFailed] = useState(false);
|
|
||||||
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
||||||
useEffect(() => { setFailed(false); }, [artist.id, artist.coverArt]);
|
|
||||||
if (failed) return <div className="search-result-icon"><Users size={14} /></div>;
|
|
||||||
return (
|
|
||||||
<ArtistCoverArtImage
|
|
||||||
artistId={artist.id}
|
|
||||||
coverArt={artist.coverArt}
|
|
||||||
libraryResolve={false}
|
|
||||||
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
|
||||||
surface="dense"
|
|
||||||
className="search-result-thumb"
|
|
||||||
alt=""
|
|
||||||
loading="eager"
|
|
||||||
ensurePriority="high"
|
|
||||||
fetchQueueBias={FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM}
|
|
||||||
onError={() => setFailed(true)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function LiveSearch() {
|
export default function LiveSearch() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const query = useLiveSearchScopeStore(s => s.query);
|
const query = useLiveSearchScopeStore(s => s.query);
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Music, Users } from 'lucide-react';
|
||||||
|
import type { SubsonicSong, SubsonicArtist } from '@/lib/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 { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
|
||||||
|
|
||||||
|
export function LiveSearchAlbumThumb({ albumId, coverArt }: { albumId: string; coverArt: string }) {
|
||||||
|
return (
|
||||||
|
<AlbumCoverArtImage
|
||||||
|
albumId={albumId}
|
||||||
|
coverArt={coverArt}
|
||||||
|
libraryResolve={false}
|
||||||
|
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
||||||
|
surface="dense"
|
||||||
|
className="search-result-thumb"
|
||||||
|
alt=""
|
||||||
|
ensurePriority="high"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LiveSearchSongThumb({ song }: { song: Pick<SubsonicSong, 'id' | 'albumId' | 'coverArt' | 'discNumber'> }) {
|
||||||
|
// Search results carry the per-track `mf-…` coverArt id, which the cover
|
||||||
|
// pipeline fails to resolve and the thumbnail goes blank. The album-scoped
|
||||||
|
// `al-<albumId>_0` id is what actually loads (verified in the RC1 blank-thumb
|
||||||
|
// investigation), and a song's search thumbnail is its album cover anyway —
|
||||||
|
// so fetch the album cover from the albumId. Falls back to a music glyph when
|
||||||
|
// there is no album to key on.
|
||||||
|
const albumId = song.albumId?.trim();
|
||||||
|
const coverRef = React.useMemo(
|
||||||
|
() => (albumId ? albumCoverRef(albumId, `al-${albumId}_0`) : undefined),
|
||||||
|
[albumId],
|
||||||
|
);
|
||||||
|
if (!coverRef) return <div className="search-result-icon"><Music size={14} /></div>;
|
||||||
|
return (
|
||||||
|
<CoverArtImage
|
||||||
|
coverRef={coverRef}
|
||||||
|
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
||||||
|
surface="dense"
|
||||||
|
className="search-result-thumb"
|
||||||
|
alt=""
|
||||||
|
ensurePriority="high"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LiveSearchArtistThumb({ artist }: { artist: Pick<SubsonicArtist, 'id' | 'coverArt'> }) {
|
||||||
|
const [failed, setFailed] = useState(false);
|
||||||
|
// React Compiler set-state-in-effect rule: state set from an async result resolved in this effect.
|
||||||
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
|
useEffect(() => { setFailed(false); }, [artist.id, artist.coverArt]);
|
||||||
|
if (failed) return <div className="search-result-icon"><Users size={14} /></div>;
|
||||||
|
return (
|
||||||
|
<ArtistCoverArtImage
|
||||||
|
artistId={artist.id}
|
||||||
|
coverArt={artist.coverArt}
|
||||||
|
libraryResolve={false}
|
||||||
|
displayCssPx={COVER_DENSE_SEARCH_CSS_PX}
|
||||||
|
surface="dense"
|
||||||
|
className="search-result-thumb"
|
||||||
|
alt=""
|
||||||
|
loading="eager"
|
||||||
|
ensurePriority="high"
|
||||||
|
fetchQueueBias={FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM}
|
||||||
|
onError={() => setFailed(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user