From ece4ae651c5152d40363f24a4b6b9329585a401e Mon Sep 17 00:00:00 2001
From: Psychotoxical <171614930+Psychotoxical@users.noreply.github.com>
Date: Tue, 30 Jun 2026 23:21:17 +0200
Subject: [PATCH] refactor(search): extract LiveSearch result thumbnails into a
sibling module
---
src/features/search/components/LiveSearch.tsx | 77 ++-----------------
.../components/liveSearchResultThumbs.tsx | 72 +++++++++++++++++
2 files changed, 78 insertions(+), 71 deletions(-)
create mode 100644 src/features/search/components/liveSearchResultThumbs.tsx
diff --git a/src/features/search/components/LiveSearch.tsx b/src/features/search/components/LiveSearch.tsx
index c40b4fca..bef9bf80 100644
--- a/src/features/search/components/LiveSearch.tsx
+++ b/src/features/search/components/LiveSearch.tsx
@@ -1,5 +1,5 @@
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 {
LIVE_SEARCH_DEBOUNCE_NETWORK_MS,
@@ -29,13 +29,11 @@ import { useAuthStore } from '@/store/authStore';
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
import { useTranslation } from 'react-i18next';
import { albumArtistDisplayName } from '@/features/album';
-import { FETCH_QUEUE_BIAS_SEARCH_ARTIST_OVER_ALBUM } from '@/ui/CachedImage';
-import type { SubsonicSong } 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 {
+ LiveSearchAlbumThumb,
+ LiveSearchSongThumb,
+ LiveSearchArtistThumb,
+} from '@/features/search/components/liveSearchResultThumbs';
import { showToast } from '@/lib/dom/toast';
import { useShareSearch } from '@/features/search/hooks/useShareSearch';
import ShareSearchResults from '@/features/search/components/ShareSearchResults';
@@ -58,69 +56,6 @@ import { resolveIndexKey } from '@/lib/server/serverIndexKey';
type LiveSearchSource = 'local' | 'network';
-function LiveSearchAlbumThumb({ albumId, coverArt }: { albumId: string; coverArt: string }) {
- return (
-
- );
-}
-
-function LiveSearchSongThumb({ song }: { song: Pick }) {
- // 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-_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
;
- return (
-
- );
-}
-
-function LiveSearchArtistThumb({ artist }: { artist: Pick }) {
- 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
;
- return (
- setFailed(true)}
- />
- );
-}
-
export default function LiveSearch() {
const { t } = useTranslation();
const query = useLiveSearchScopeStore(s => s.query);
diff --git a/src/features/search/components/liveSearchResultThumbs.tsx b/src/features/search/components/liveSearchResultThumbs.tsx
new file mode 100644
index 00000000..613e3245
--- /dev/null
+++ b/src/features/search/components/liveSearchResultThumbs.tsx
@@ -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 (
+
+ );
+}
+
+export function LiveSearchSongThumb({ song }: { song: Pick }) {
+ // 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-_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
;
+ return (
+
+ );
+}
+
+export function LiveSearchArtistThumb({ artist }: { artist: Pick }) {
+ 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
;
+ return (
+ setFailed(true)}
+ />
+ );
+}