add list search links to command palette

This commit is contained in:
jeffvli
2026-03-18 02:51:27 -07:00
parent c16eccaecb
commit 67231753e4
6 changed files with 149 additions and 13 deletions
@@ -1,8 +1,6 @@
.root {
display: flex;
flex-direction: column;
}
@@ -32,6 +30,11 @@
opacity: 0.9;
}
.subtitle {
display: flex;
align-items: center;
}
.items {
display: flex;
flex-direction: column;
@@ -12,7 +12,7 @@ interface CollapsibleCommandGroupProps {
expanded?: boolean;
heading: string;
onToggle?: () => void;
subtitle?: string;
subtitle?: ReactNode;
}
export function CollapsibleCommandGroup({
@@ -48,7 +48,7 @@ export function CollapsibleCommandGroup({
return (
<div className={styles.root}>
<Paper p="xs" radius="sm" withBorder>
<Paper p="sm" radius="sm" withBorder>
<div
className={styles.heading}
onClick={toggle}
@@ -1,14 +1,18 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { nanoid } from 'nanoid/non-secure';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router';
import { createSearchParams, 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 { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -47,14 +51,50 @@ export function SearchAlbumArtistsSection({
const showSection = isHome;
const numberOfResults = hasNextPage ? `${artists.length}+` : artists.length;
const handleGoToPage = useCallback(() => {
navigate(
{
pathname: AppRoute.LIBRARY_ALBUM_ARTISTS,
search: createSearchParams({
[FILTER_KEYS.SHARED.SEARCH_TERM]: debouncedQuery || query,
}).toString(),
},
{ state: { navigationId: nanoid() } },
);
onSelectResult();
}, [debouncedQuery, navigate, onSelectResult, query]);
if (!showSection) return null;
return (
<CollapsibleCommandGroup
expanded={expanded}
heading="Artists"
heading={t('entity.albumArtist', { count: 2, postProcess: 'titleCase' })}
onToggle={onToggle}
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
subtitle={
isFetched ? (
<>
{query ? (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleGoToPage();
}}
onKeyDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
size="compact-xs"
variant="filled"
w="8rem"
>
{t('common.numberOfResults', { numberOfResults })}
</Button>
) : null}
</>
) : undefined
}
>
{isLoading ? (
<Box p="md">
@@ -1,14 +1,18 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { nanoid } from 'nanoid/non-secure';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router';
import { createSearchParams, 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 { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -47,14 +51,50 @@ export function SearchAlbumsSection({
const showSection = isHome;
const numberOfResults = hasNextPage ? `${albums.length}+` : albums.length;
const handleGoToPage = useCallback(() => {
navigate(
{
pathname: AppRoute.LIBRARY_ALBUMS,
search: createSearchParams({
[FILTER_KEYS.SHARED.SEARCH_TERM]: debouncedQuery || query,
}).toString(),
},
{ state: { navigationId: nanoid() } },
);
onSelectResult();
}, [debouncedQuery, navigate, onSelectResult, query]);
if (!showSection) return null;
return (
<CollapsibleCommandGroup
expanded={expanded}
heading="Albums"
heading={t('entity.album', { count: 2, postProcess: 'titleCase' })}
onToggle={onToggle}
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
subtitle={
isFetched ? (
<>
{query ? (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleGoToPage();
}}
onKeyDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
size="compact-xs"
variant="filled"
w="8rem"
>
{t('common.numberOfResults', { numberOfResults })}
</Button>
) : null}
</>
) : undefined
}
>
{isLoading ? (
<Box p="md">
@@ -1,14 +1,18 @@
import { useInfiniteQuery } from '@tanstack/react-query';
import { nanoid } from 'nanoid/non-secure';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router';
import { createSearchParams, 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 { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { Box } from '/@/shared/components/box/box';
import { Button } from '/@/shared/components/button/button';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { Text } from '/@/shared/components/text/text';
import { LibraryItem } from '/@/shared/types/domain-types';
@@ -47,14 +51,50 @@ export function SearchSongsSection({
const showSection = isHome;
const numberOfResults = hasNextPage ? `${songs.length}+` : songs.length;
const handleGoToPage = useCallback(() => {
navigate(
{
pathname: AppRoute.LIBRARY_SONGS,
search: createSearchParams({
[FILTER_KEYS.SHARED.SEARCH_TERM]: debouncedQuery || query,
}).toString(),
},
{ state: { navigationId: nanoid() } },
);
onSelectResult();
}, [debouncedQuery, navigate, onSelectResult, query]);
if (!showSection) return null;
return (
<CollapsibleCommandGroup
expanded={expanded}
heading="Tracks"
heading={t('entity.track', { count: 2, postProcess: 'titleCase' })}
onToggle={onToggle}
subtitle={isFetched ? t('common.numberOfResults', { numberOfResults }) : undefined}
subtitle={
isFetched ? (
<>
{query ? (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleGoToPage();
}}
onKeyDown={(e) => {
e.preventDefault();
e.stopPropagation();
}}
size="compact-xs"
variant="filled"
w="8rem"
>
{t('common.numberOfResults', { numberOfResults })}
</Button>
) : null}
</>
) : undefined
}
>
{isLoading ? (
<Box p="md">
@@ -1,12 +1,25 @@
import { useLocation } from 'react-router';
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
function navigationIdFromState(state: unknown): string | undefined {
if (state && typeof state === 'object' && 'navigationId' in state) {
const id = (state as { navigationId: unknown }).navigationId;
return typeof id === 'string' ? id : undefined;
}
return undefined;
}
export const ListSearchInput = () => {
const { searchTerm, setSearchTerm } = useSearchTermFilter();
const { state } = useLocation();
const navigationId = navigationIdFromState(state);
return (
<SearchInput
defaultValue={searchTerm}
key={navigationId ?? 'list-search-input'}
onChange={(e) => setSearchTerm(e.target.value || null)}
/>
);