reimplement search page

This commit is contained in:
jeffvli
2025-11-15 13:37:03 -08:00
parent e246e4c0b7
commit 48feb9f656
3 changed files with 178 additions and 164 deletions
@@ -1,93 +1,106 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Suspense } from 'react';
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { MutableRefObject } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { useParams, useSearchParams } from 'react-router'; import { useParams, useSearchParams } from 'react-router';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { import {
ALBUM_CONTEXT_MENU_ITEMS, AlbumListView,
ARTIST_CONTEXT_MENU_ITEMS, OverrideAlbumListQuery,
SONG_CONTEXT_MENU_ITEMS, } from '/@/renderer/features/albums/components/album-list-content';
} from '/@/renderer/features/context-menu/context-menu-items'; import {
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add'; AlbumArtistListView,
import { AppRoute } from '/@/renderer/router/routes'; OverrideAlbumArtistListQuery,
import { useCurrentServer, useListStoreByKey, usePlayButtonBehavior } from '/@/renderer/store'; } from '/@/renderer/features/artists/components/album-artist-list-content';
import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types'; import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import {
OverrideSongListQuery,
SongListView,
} from '/@/renderer/features/songs/components/song-list-content';
import { useListSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import {
AlbumArtistListSort,
AlbumListSort,
LibraryItem,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface SearchContentProps { export const SearchContent = () => {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const SearchContent = ({ tableRef }: SearchContentProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const { itemType } = useParams() as { itemType: LibraryItem }; const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams] = useSearchParams();
const pageKey = itemType;
const { filter } = useListStoreByKey({
filter: { searchTerm: searchParams.get('query') || '' },
key: itemType,
});
const handlePlayQueueAdd = usePlayQueueAdd(); return (
const playButtonBehavior = usePlayButtonBehavior(); <AnimatedPage>
<Suspense fallback={<Spinner container />}>
const contextMenuItems = () => { {itemType === LibraryItem.ALBUM && <AlbumSearch />}
switch (itemType) { {itemType === LibraryItem.SONG && <SongSearch />}
case LibraryItem.ALBUM: {itemType === LibraryItem.ALBUM_ARTIST && <ArtistSearch />}
return ALBUM_CONTEXT_MENU_ITEMS; </Suspense>
case LibraryItem.ALBUM_ARTIST: </AnimatedPage>
return ARTIST_CONTEXT_MENU_ITEMS; );
case LibraryItem.SONG: };
return SONG_CONTEXT_MENU_ITEMS;
default: const AlbumSearch = () => {
return []; const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM);
} const [searchParams] = useSearchParams();
};
const albumQuery: OverrideAlbumListQuery = {
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => { searchTerm: searchParams.get('query') || '',
if (!e.data) return; sortBy: AlbumListSort.NAME,
switch (itemType) { sortOrder: SortOrder.ASC,
case LibraryItem.ALBUM: };
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
break; return (
case LibraryItem.ALBUM_ARTIST: <AlbumListView
navigate( display={display}
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, { grid={grid}
albumArtistId: e.data.id, itemsPerPage={itemsPerPage}
}), overrideQuery={albumQuery}
); pagination={pagination}
break; table={table}
case LibraryItem.SONG: />
handlePlayQueueAdd?.({ );
byItemType: { };
id: [],
type: LibraryItem.SONG, const SongSearch = () => {
}, const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.SONG);
initialSongId: e.data.id, const [searchParams] = useSearchParams();
playType: playButtonBehavior,
query: { const songQuery: OverrideSongListQuery = {
startIndex: 0, searchTerm: searchParams.get('query') || '',
...filter, sortBy: SongListSort.NAME,
}, sortOrder: SortOrder.ASC,
}); };
break;
} return (
}; <SongListView
display={display}
const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); grid={grid}
itemsPerPage={itemsPerPage}
const tableProps = useVirtualTable<SongListQuery>({ overrideQuery={songQuery}
contextMenu: contextMenuItems(), pagination={pagination}
customFilters: filter, table={table}
itemType, />
pageKey, );
server, };
tableRef,
}); const ArtistSearch = () => {
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ARTIST);
return null; const [searchParams] = useSearchParams();
const albumArtistQuery: OverrideAlbumArtistListQuery = {
searchTerm: searchParams.get('query') || '',
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
};
return (
<AlbumArtistListView
display={display}
grid={grid}
itemsPerPage={itemsPerPage}
overrideQuery={albumArtistQuery}
pagination={pagination}
table={table}
/>
);
}; };
@@ -1,54 +1,53 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ChangeEvent, MutableRefObject } from 'react'; import { ChangeEvent } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, Link, useParams, useSearchParams } from 'react-router'; import { generatePath, Link, useParams, useSearchParams } from 'react-router';
import { ALBUM_ARTIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { PageHeader } from '/@/renderer/components/page-header/page-header'; import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { ALBUM_TABLE_COLUMNS, SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar'; import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { SearchInput } from '/@/renderer/features/shared/components/search-input'; import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store'; import { Button, ButtonGroup } from '/@/shared/components/button/button';
import { Button } from '/@/shared/components/button/button';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { import { LibraryItem } from '/@/shared/types/domain-types';
AlbumArtistListQuery, import { ItemListKey } from '/@/shared/types/types';
AlbumListQuery,
LibraryItem,
SongListQuery,
} from '/@/shared/types/domain-types';
interface SearchHeaderProps { interface SearchHeaderProps {
navigationId: string; navigationId: string;
tableRef: MutableRefObject<AgGridReactType | null>;
} }
export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => { export const SearchHeader = ({ navigationId }: SearchHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { itemType } = useParams() as { itemType: LibraryItem }; const { itemType } = useParams() as { itemType: LibraryItem };
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const cq = useContainerQuery(); const cq = useContainerQuery();
const server = useCurrentServer();
const { filter } = useListStoreByKey<AlbumArtistListQuery | AlbumListQuery | SongListQuery>({
key: itemType,
});
const { handleRefreshTable } = useListFilterRefresh({
itemType,
server,
});
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
setSearchParams({ query: e.target.value }, { replace: true, state: { navigationId } }); setSearchParams({ query: e.target.value }, { replace: true, state: { navigationId } });
handleRefreshTable(tableRef, { ...filter, searchTerm: e.target.value });
}, 200); }, 200);
const listConfigMenuProps = {
[LibraryItem.ALBUM]: {
listKey: ItemListKey.ALBUM,
tableColumnsData: ALBUM_TABLE_COLUMNS,
},
[LibraryItem.ALBUM_ARTIST]: {
listKey: ItemListKey.ALBUM_ARTIST,
tableColumnsData: ALBUM_ARTIST_TABLE_COLUMNS,
},
[LibraryItem.SONG]: {
listKey: ItemListKey.SONG,
tableColumnsData: SONG_TABLE_COLUMNS,
},
};
return ( return (
<Stack gap={0} ref={cq.ref}> <Stack gap={0} ref={cq.ref}>
<PageHeader> <PageHeader>
@@ -65,54 +64,59 @@ export const SearchHeader = ({ navigationId, tableRef }: SearchHeaderProps) => {
</Flex> </Flex>
</PageHeader> </PageHeader>
<FilterBar> <FilterBar>
<Group> <Flex justify="space-between" w="100%">
<Button <ButtonGroup>
component={Link} <Button
fw={600} component={Link}
replace fw={600}
size="compact-md" replace
state={{ navigationId }} size="compact-md"
to={{ state={{ navigationId }}
pathname: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }), to={{
search: searchParams.toString(), pathname: generatePath(AppRoute.SEARCH, {
}} itemType: LibraryItem.SONG,
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'} }),
> search: searchParams.toString(),
{t('entity.track_other', { postProcess: 'sentenceCase' })} }}
</Button> variant={itemType === LibraryItem.SONG ? 'filled' : 'default'}
<Button >
component={Link} {t('entity.track_other', { postProcess: 'sentenceCase' })}
fw={600} </Button>
replace <Button
size="compact-md" component={Link}
state={{ navigationId }} fw={600}
to={{ replace
pathname: generatePath(AppRoute.SEARCH, { size="compact-md"
itemType: LibraryItem.ALBUM, state={{ navigationId }}
}), to={{
search: searchParams.toString(), pathname: generatePath(AppRoute.SEARCH, {
}} itemType: LibraryItem.ALBUM,
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'} }),
> search: searchParams.toString(),
{t('entity.album_other', { postProcess: 'sentenceCase' })} }}
</Button> variant={itemType === LibraryItem.ALBUM ? 'filled' : 'default'}
<Button >
component={Link} {t('entity.album_other', { postProcess: 'sentenceCase' })}
fw={600} </Button>
replace <Button
size="compact-md" component={Link}
state={{ navigationId }} fw={600}
to={{ replace
pathname: generatePath(AppRoute.SEARCH, { size="compact-md"
itemType: LibraryItem.ALBUM_ARTIST, state={{ navigationId }}
}), to={{
search: searchParams.toString(), pathname: generatePath(AppRoute.SEARCH, {
}} itemType: LibraryItem.ALBUM_ARTIST,
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'} }),
> search: searchParams.toString(),
{t('entity.artist_other', { postProcess: 'sentenceCase' })} }}
</Button> variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'default'}
</Group> >
{t('entity.artist_other', { postProcess: 'sentenceCase' })}
</Button>
</ButtonGroup>
<ListConfigMenu {...listConfigMenuProps[itemType]} />
</Flex>
</FilterBar> </FilterBar>
</Stack> </Stack>
); );
@@ -1,6 +1,4 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useId } from 'react';
import { useId, useRef } from 'react';
import { useLocation, useParams } from 'react-router'; import { useLocation, useParams } from 'react-router';
import { SearchContent } from '/@/renderer/features/search/components/search-content'; import { SearchContent } from '/@/renderer/features/search/components/search-content';
@@ -12,12 +10,11 @@ const SearchRoute = () => {
const localNavigationId = useId(); const localNavigationId = useId();
const navigationId = locationState?.navigationId || localNavigationId; const navigationId = locationState?.navigationId || localNavigationId;
const { itemType } = useParams() as { itemType: string }; const { itemType } = useParams() as { itemType: string };
const tableRef = useRef<AgGridReactType | null>(null);
return ( return (
<AnimatedPage key={`search-${navigationId}`}> <AnimatedPage key={`search-${navigationId}`}>
<SearchHeader navigationId={navigationId} tableRef={tableRef} /> <SearchHeader navigationId={navigationId} />
<SearchContent key={`page-${itemType}`} tableRef={tableRef} /> <SearchContent key={`page-${itemType}`} />
</AnimatedPage> </AnimatedPage>
); );
}; };