From 615f9c351549f795aa5d312d2bd2537830253bae Mon Sep 17 00:00:00 2001 From: jeffvli Date: Wed, 18 Mar 2026 00:59:04 -0700 Subject: [PATCH] refactor search into individual sections by itemtype, add infinite loader --- src/i18n/locales/en.json | 1 + src/renderer/api/query-keys.ts | 5 + .../features/search/api/search-api.ts | 105 +++++- .../collapsible-command-group.module.css | 6 +- .../components/collapsible-command-group.tsx | 8 +- .../search/components/command-palette.tsx | 312 ++++++------------ .../search-album-artists-section.tsx | 122 +++++++ .../components/search-albums-section.tsx | 123 +++++++ .../components/search-songs-section.tsx | 122 +++++++ 9 files changed, 593 insertions(+), 211 deletions(-) create mode 100644 src/renderer/features/search/components/search-album-artists-section.tsx create mode 100644 src/renderer/features/search/components/search-albums-section.tsx create mode 100644 src/renderer/features/search/components/search-songs-section.tsx diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 906c77f56..cdddffe39 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -114,6 +114,7 @@ "no": "no", "none": "none", "noResultsFromQuery": "the query returned no results", + "numberOfResults": "{{numberOfResults}} results", "noFilters": "no filters configured", "note": "note", "ok": "ok", diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index d3a4ef029..84f0f2121 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -347,6 +347,11 @@ export const queryKeys: Record< list: (serverId: string) => [serverId, 'roles'] as const, }, search: { + infiniteList: ( + serverId: string, + type: 'albumArtists' | 'albums' | 'songs', + searchTerm: string, + ) => [serverId, 'search', 'infiniteList', type, searchTerm] as const, list: (serverId: string, query?: SearchQuery) => { if (query) return [serverId, 'search', 'list', query] as const; return [serverId, 'search', 'list'] as const; diff --git a/src/renderer/features/search/api/search-api.ts b/src/renderer/features/search/api/search-api.ts index 38a3c744f..42abf7fca 100644 --- a/src/renderer/features/search/api/search-api.ts +++ b/src/renderer/features/search/api/search-api.ts @@ -1,9 +1,11 @@ -import { queryOptions } from '@tanstack/react-query'; +import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { QueryHookArgs } from '/@/renderer/lib/react-query'; -import { SearchQuery } from '/@/shared/types/domain-types'; +import { SearchQuery, SearchResponse } from '/@/shared/types/domain-types'; + +const SEARCH_PAGE_SIZE = 4; export const searchQueries = { search: (args: QueryHookArgs) => { @@ -18,4 +20,103 @@ export const searchQueries = { ...args.options, }); }, + searchAlbumArtistsInfinite: (args: { + enabled?: boolean; + searchTerm: string; + serverId: string | undefined; + }) => { + const { enabled = true, searchTerm, serverId } = args; + return infiniteQueryOptions({ + enabled: Boolean(serverId && searchTerm && enabled), + getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => { + const len = lastPage.albumArtists.length; + if (len < SEARCH_PAGE_SIZE) return undefined; + return allPages.length * SEARCH_PAGE_SIZE; + }, + initialPageParam: 0, + queryFn: ({ pageParam, signal }) => { + if (!serverId) throw new Error('serverId required'); + const startIndex = (pageParam ?? 0) as number; + return api.controller.search({ + apiClientProps: { serverId, signal }, + query: { + albumArtistLimit: SEARCH_PAGE_SIZE, + albumArtistStartIndex: startIndex, + albumLimit: 0, + albumStartIndex: 0, + query: searchTerm, + songLimit: 0, + songStartIndex: 0, + }, + }); + }, + queryKey: queryKeys.search.infiniteList(serverId ?? '', 'albumArtists', searchTerm), + }); + }, + searchAlbumsInfinite: (args: { + enabled?: boolean; + searchTerm: string; + serverId: string | undefined; + }) => { + const { enabled = true, searchTerm, serverId } = args; + return infiniteQueryOptions({ + enabled: Boolean(serverId && searchTerm && enabled), + getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => { + const len = lastPage.albums.length; + if (len < SEARCH_PAGE_SIZE) return undefined; + return allPages.length * SEARCH_PAGE_SIZE; + }, + initialPageParam: 0, + queryFn: ({ pageParam, signal }) => { + if (!serverId) throw new Error('serverId required'); + const startIndex = (pageParam ?? 0) as number; + return api.controller.search({ + apiClientProps: { serverId, signal }, + query: { + albumArtistLimit: 0, + albumArtistStartIndex: 0, + albumLimit: SEARCH_PAGE_SIZE, + albumStartIndex: startIndex, + query: searchTerm, + songLimit: 0, + songStartIndex: 0, + }, + }); + }, + queryKey: queryKeys.search.infiniteList(serverId ?? '', 'albums', searchTerm), + }); + }, + searchSongsInfinite: (args: { + enabled?: boolean; + searchTerm: string; + serverId: string | undefined; + }) => { + const { enabled = true, searchTerm, serverId } = args; + return infiniteQueryOptions({ + enabled: Boolean(serverId && searchTerm && enabled), + getNextPageParam: (lastPage: SearchResponse, allPages: SearchResponse[]) => { + const len = lastPage.songs.length; + if (len < SEARCH_PAGE_SIZE) return undefined; + return allPages.length * SEARCH_PAGE_SIZE; + }, + initialPageParam: 0, + queryFn: ({ pageParam, signal }) => { + if (!serverId) throw new Error('serverId required'); + const startIndex = (pageParam ?? 0) as number; + return api.controller.search({ + apiClientProps: { serverId, signal }, + query: { + albumArtistLimit: 0, + albumArtistStartIndex: 0, + albumLimit: 0, + albumStartIndex: 0, + query: searchTerm, + songLimit: SEARCH_PAGE_SIZE, + songStartIndex: startIndex, + }, + }); + }, + queryKey: queryKeys.search.infiniteList(serverId ?? '', 'songs', searchTerm), + }); + }, }; diff --git a/src/renderer/features/search/components/collapsible-command-group.module.css b/src/renderer/features/search/components/collapsible-command-group.module.css index 73e4122f4..aacecb7bc 100644 --- a/src/renderer/features/search/components/collapsible-command-group.module.css +++ b/src/renderer/features/search/components/collapsible-command-group.module.css @@ -1,11 +1,8 @@ .root { display: flex; flex-direction: column; - gap: var(--theme-spacing-xs); - &:not(:last-child) { - margin-bottom: var(--theme-spacing-xs); - } + } @@ -38,5 +35,4 @@ .items { display: flex; flex-direction: column; - gap: 0; } diff --git a/src/renderer/features/search/components/collapsible-command-group.tsx b/src/renderer/features/search/components/collapsible-command-group.tsx index ed1be16d3..320bc39e7 100644 --- a/src/renderer/features/search/components/collapsible-command-group.tsx +++ b/src/renderer/features/search/components/collapsible-command-group.tsx @@ -2,6 +2,7 @@ import { ReactNode, useCallback, useState } from 'react'; import styles from './collapsible-command-group.module.css'; +import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Paper } from '/@/shared/components/paper/paper'; @@ -11,6 +12,7 @@ interface CollapsibleCommandGroupProps { expanded?: boolean; heading: string; onToggle?: () => void; + subtitle?: string; } export function CollapsibleCommandGroup({ @@ -19,6 +21,7 @@ export function CollapsibleCommandGroup({ expanded: controlledExpanded, heading, onToggle, + subtitle, }: CollapsibleCommandGroupProps) { const [internalExpanded, setInternalExpanded] = useState(defaultExpanded); @@ -54,7 +57,10 @@ export function CollapsibleCommandGroup({ tabIndex={0} > - {heading} + + {heading} + {subtitle && {subtitle}} + {expanded &&
{children}
} diff --git a/src/renderer/features/search/components/command-palette.tsx b/src/renderer/features/search/components/command-palette.tsx index d192dfc4b..3f4de942b 100644 --- a/src/renderer/features/search/components/command-palette.tsx +++ b/src/renderer/features/search/components/command-palette.tsx @@ -1,18 +1,13 @@ -import { useQuery } from '@tanstack/react-query'; import { useCallback, useRef, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { generatePath, useNavigate } from 'react-router'; -import { searchQueries } from '/@/renderer/features/search/api/search-api'; -import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; -import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; import { GoToCommands } from '/@/renderer/features/search/components/go-to-commands'; import { HomeCommands } from '/@/renderer/features/search/components/home-commands'; -import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; +import { SearchAlbumArtistsSection } from '/@/renderer/features/search/components/search-album-artists-section'; +import { SearchAlbumsSection } from '/@/renderer/features/search/components/search-albums-section'; +import { SearchSongsSection } from '/@/renderer/features/search/components/search-songs-section'; import { ServerCommands } from '/@/renderer/features/search/components/server-commands'; -import { AppRoute } from '/@/renderer/router/routes'; -import { useAppStore, useCurrentServer } from '/@/renderer/store'; +import { useAppStore } from '/@/renderer/store'; import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; import { Breadcrumb } from '/@/shared/components/breadcrumb/breadcrumb'; import { Button } from '/@/shared/components/button/button'; @@ -21,11 +16,10 @@ import { Group } from '/@/shared/components/group/group'; import { Icon } from '/@/shared/components/icon/icon'; import { Kbd } from '/@/shared/components/kbd/kbd'; import { Modal } from '/@/shared/components/modal/modal'; -import { Spinner } from '/@/shared/components/spinner/spinner'; +import { Stack } from '/@/shared/components/stack/stack'; import { TextInput } from '/@/shared/components/text-input/text-input'; import { useDebouncedValue } from '/@/shared/hooks/use-debounced-value'; import { useDisclosure } from '/@/shared/hooks/use-disclosure'; -import { LibraryItem } from '/@/shared/types/domain-types'; interface CommandPaletteProps { modalProps: (typeof useDisclosure)['arguments']; @@ -37,23 +31,110 @@ const SEARCH_SECTION_IDS = { tracks: 'tracks', } as const; -export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { - const navigate = useNavigate(); - const server = useCurrentServer(); +interface CommandPaletteSearchProps { + children?: React.ReactNode; + isHome: boolean; + onSelectResult: () => void; + query: string; + searchInputRef: React.RefObject; + setQuery: (query: string) => void; +} + +function CommandPaletteSearch({ + children, + isHome, + onSelectResult, + query, + searchInputRef, + setQuery, +}: CommandPaletteSearchProps) { + const [debouncedQuery] = useDebouncedValue(query, 400); const searchSectionsExpanded = useAppStore( (state) => state.commandPaletteSearchSectionsExpanded, ); const setSearchSectionExpanded = useAppStore( (state) => state.actions.setCommandPaletteSearchSectionExpanded, ); + + return ( + <> + } + onChange={(e) => setQuery(e.currentTarget.value)} + ref={searchInputRef} + rightSection={ + query && ( + { + setQuery(''); + searchInputRef.current?.focus(); + }} + variant="transparent" + > + + + ) + } + size="sm" + value={query} + /> + + + + + setSearchSectionExpanded( + SEARCH_SECTION_IDS.albums, + !(searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true), + ) + } + query={query} + /> + + setSearchSectionExpanded( + SEARCH_SECTION_IDS.artists, + !(searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true), + ) + } + query={query} + /> + + setSearchSectionExpanded( + SEARCH_SECTION_IDS.tracks, + !(searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true), + ) + } + query={query} + /> + + {children} + + + ); +} + +export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { const [value, setValue] = useState(''); const [query, setQuery] = useState(''); - const [debouncedQuery] = useDebouncedValue(query, 400); const [pages, setPages] = useState([CommandPalettePages.HOME]); const activePage = pages[pages.length - 1]; const isHome = activePage === CommandPalettePages.HOME; const searchInputRef = useRef(null); - const { t } = useTranslation(); const popPage = useCallback(() => { setPages((pages) => { @@ -63,25 +144,10 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { }); }, []); - const { data, isLoading } = useQuery( - searchQueries.search({ - options: { enabled: isHome && debouncedQuery !== '' && query !== '' }, - query: { - albumArtistLimit: 4, - albumArtistStartIndex: 0, - albumLimit: 4, - albumStartIndex: 0, - query: debouncedQuery, - songLimit: 4, - songStartIndex: 0, - }, - serverId: server?.id, - }), - ); - - const showAlbumGroup = isHome && Boolean(query && data && data?.albums?.length > 0); - const showArtistGroup = isHome && Boolean(query && data && data?.albumArtists?.length > 0); - const showTrackGroup = isHome && Boolean(query && data && data?.songs?.length > 0); + const handleSelectResult = useCallback(() => { + modalProps.handlers.close(); + setQuery(''); + }, [modalProps.handlers]); return ( { }} label="Global Command Menu" onKeyDown={(e) => { - // Focus the search input when navigating with arrow keys - // to prevent the focus from staying on the command-item ActionIcon if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { searchInputRef.current?.focus(); } @@ -129,171 +193,13 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => { onValueChange={setValue} value={value} > - } - onChange={(e) => setQuery(e.currentTarget.value)} - ref={searchInputRef} - rightSection={ - isLoading ? ( - - ) : ( - query && ( - { - setQuery(''); - searchInputRef.current?.focus(); - }} - variant="transparent" - > - - - ) - ) - } - size="sm" - value={query} - /> - - - - {t('common.noResultsFromQuery', { postProcess: 'sentenceCase' })} - - {showAlbumGroup && ( - - setSearchSectionExpanded( - SEARCH_SECTION_IDS.albums, - !(searchSectionsExpanded[SEARCH_SECTION_IDS.albums] ?? true), - ) - } - > - {data?.albums?.map((album) => ( - { - navigate( - generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { - albumId: album.id, - }), - ); - modalProps.handlers.close(); - setQuery(''); - }} - value={`search-${album.id}`} - > - {({ isHighlighted }) => ( - artist.name) - .join(', ')} - title={album.name} - /> - )} - - ))} - - )} - {showArtistGroup && ( - - setSearchSectionExpanded( - SEARCH_SECTION_IDS.artists, - !(searchSectionsExpanded[SEARCH_SECTION_IDS.artists] ?? true), - ) - } - > - {data?.albumArtists.map((artist) => ( - { - navigate( - generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { - albumArtistId: artist.id, - }), - ); - modalProps.handlers.close(); - setQuery(''); - }} - value={`search-${artist.id}`} - > - {({ isHighlighted }) => ( - - )} - - ))} - - )} - {showTrackGroup && ( - - setSearchSectionExpanded( - SEARCH_SECTION_IDS.tracks, - !(searchSectionsExpanded[SEARCH_SECTION_IDS.tracks] ?? true), - ) - } - > - {data?.songs.map((song) => ( - { - navigate( - generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { - albumId: song.albumId, - }), - ); - modalProps.handlers.close(); - setQuery(''); - }} - value={`search-${song.id}`} - > - {({ isHighlighted }) => ( - artist.name) - .join(', ')} - title={song.name} - /> - )} - - ))} - - )} + {activePage === CommandPalettePages.HOME && ( { setQuery={setQuery} /> )} - + diff --git a/src/renderer/features/search/components/search-album-artists-section.tsx b/src/renderer/features/search/components/search-album-artists-section.tsx new file mode 100644 index 000000000..03b14ed4f --- /dev/null +++ b/src/renderer/features/search/components/search-album-artists-section.tsx @@ -0,0 +1,122 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { generatePath, useNavigate } from 'react-router'; + +import { searchQueries } from '/@/renderer/features/search/api/search-api'; +import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group'; +import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; +import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer } from '/@/renderer/store'; +import { Box } from '/@/shared/components/box/box'; +import { Spinner } from '/@/shared/components/spinner/spinner'; +import { Text } from '/@/shared/components/text/text'; +import { LibraryItem } from '/@/shared/types/domain-types'; + +interface SearchAlbumArtistsSectionProps { + debouncedQuery: string; + expanded: boolean; + isHome: boolean; + onSelectResult: () => void; + onToggle: () => void; + query: string; +} + +export function SearchAlbumArtistsSection({ + debouncedQuery, + expanded, + isHome, + onSelectResult, + onToggle, + query, +}: SearchAlbumArtistsSectionProps) { + const navigate = useNavigate(); + const server = useCurrentServer(); + const { t } = useTranslation(); + + const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } = + useInfiniteQuery( + searchQueries.searchAlbumArtistsInfinite({ + enabled: isHome && debouncedQuery !== '' && query !== '', + searchTerm: debouncedQuery, + serverId: server?.id, + }), + ); + + const artists = data?.pages.flatMap((p) => p.albumArtists) ?? []; + const showSection = isHome; + const numberOfResults = hasNextPage ? `${artists.length}+` : artists.length; + + if (!showSection) return null; + + return ( + + {isLoading ? ( + + + + ) : ( + <> + {artists.map((artist) => ( + { + navigate( + generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { + albumArtistId: artist.id, + }), + ); + onSelectResult(); + }} + value={`search-artist-${artist.id}`} + > + {({ isHighlighted }) => ( + + )} + + ))} + {hasNextPage && ( + fetchNextPage()} + value="search-artists-load-more" + > + {() => ( + + {isFetchingNextPage ? ( + + ) : ( + + {t('action.viewMore', { postProcess: 'titleCase' })} + + )} + + )} + + )} + + )} + + ); +} diff --git a/src/renderer/features/search/components/search-albums-section.tsx b/src/renderer/features/search/components/search-albums-section.tsx new file mode 100644 index 000000000..a22cb1262 --- /dev/null +++ b/src/renderer/features/search/components/search-albums-section.tsx @@ -0,0 +1,123 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { generatePath, useNavigate } from 'react-router'; + +import { searchQueries } from '/@/renderer/features/search/api/search-api'; +import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group'; +import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; +import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer } from '/@/renderer/store'; +import { Box } from '/@/shared/components/box/box'; +import { Spinner } from '/@/shared/components/spinner/spinner'; +import { Text } from '/@/shared/components/text/text'; +import { LibraryItem } from '/@/shared/types/domain-types'; + +interface SearchAlbumsSectionProps { + debouncedQuery: string; + expanded: boolean; + isHome: boolean; + onSelectResult: () => void; + onToggle: () => void; + query: string; +} + +export function SearchAlbumsSection({ + debouncedQuery, + expanded, + isHome, + onSelectResult, + onToggle, + query, +}: SearchAlbumsSectionProps) { + const navigate = useNavigate(); + const server = useCurrentServer(); + const { t } = useTranslation(); + + const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } = + useInfiniteQuery( + searchQueries.searchAlbumsInfinite({ + enabled: isHome && debouncedQuery !== '' && query !== '', + searchTerm: debouncedQuery, + serverId: server?.id, + }), + ); + + const albums = data?.pages.flatMap((p) => p.albums) ?? []; + const showSection = isHome; + const numberOfResults = hasNextPage ? `${albums.length}+` : albums.length; + + if (!showSection) return null; + + return ( + + {isLoading ? ( + + + + ) : ( + <> + {albums.map((album) => ( + { + navigate( + generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { + albumId: album.id, + }), + ); + onSelectResult(); + }} + value={`search-album-${album.id}`} + > + {({ isHighlighted }) => ( + artist.name) + .join(', ')} + title={album.name} + /> + )} + + ))} + {hasNextPage && ( + fetchNextPage()} + value="search-albums-load-more" + > + {() => ( +
+ {isFetchingNextPage ? ( + + ) : ( + + {t('action.viewMore', { postProcess: 'titleCase' })} + + )} +
+ )} +
+ )} + + )} +
+ ); +} diff --git a/src/renderer/features/search/components/search-songs-section.tsx b/src/renderer/features/search/components/search-songs-section.tsx new file mode 100644 index 000000000..fdd671e64 --- /dev/null +++ b/src/renderer/features/search/components/search-songs-section.tsx @@ -0,0 +1,122 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; +import { generatePath, useNavigate } from 'react-router'; + +import { searchQueries } from '/@/renderer/features/search/api/search-api'; +import { CollapsibleCommandGroup } from '/@/renderer/features/search/components/collapsible-command-group'; +import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; +import { LibraryCommandItem } from '/@/renderer/features/search/components/library-command-item'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer } from '/@/renderer/store'; +import { Box } from '/@/shared/components/box/box'; +import { Spinner } from '/@/shared/components/spinner/spinner'; +import { Text } from '/@/shared/components/text/text'; +import { LibraryItem } from '/@/shared/types/domain-types'; + +interface SearchSongsSectionProps { + debouncedQuery: string; + expanded: boolean; + isHome: boolean; + onSelectResult: () => void; + onToggle: () => void; + query: string; +} + +export function SearchSongsSection({ + debouncedQuery, + expanded, + isHome, + onSelectResult, + onToggle, + query, +}: SearchSongsSectionProps) { + const navigate = useNavigate(); + const server = useCurrentServer(); + const { t } = useTranslation(); + + const { data, fetchNextPage, hasNextPage, isFetched, isFetchingNextPage, isLoading } = + useInfiniteQuery( + searchQueries.searchSongsInfinite({ + enabled: isHome && debouncedQuery !== '' && query !== '', + searchTerm: debouncedQuery, + serverId: server?.id, + }), + ); + + const songs = data?.pages.flatMap((p) => p.songs) ?? []; + const showSection = isHome; + const numberOfResults = hasNextPage ? `${songs.length}+` : songs.length; + + if (!showSection) return null; + + return ( + + {isLoading ? ( + + + + ) : ( + <> + {songs.map((song) => ( + { + navigate( + generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { + albumId: song.albumId, + }), + ); + onSelectResult(); + }} + value={`search-song-${song.id}`} + > + {({ isHighlighted }) => ( + artist.name).join(', ')} + title={song.name} + /> + )} + + ))} + {hasNextPage && ( + fetchNextPage()} + value="search-songs-load-more" + > + {() => ( +
+ {isFetchingNextPage ? ( + + ) : ( + + {t('action.viewMore', { postProcess: 'titleCase' })} + + )} +
+ )} +
+ )} + + )} +
+ ); +}