mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #5607 from grafixeyehero/Refactor-Search--Page
Refactor search page
This commit is contained in:
commit
0dfc09529a
9 changed files with 654 additions and 684 deletions
|
@ -1,24 +1,25 @@
|
||||||
import React, { type FC, useEffect, useState } from 'react';
|
import React, { type FC, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
import Page from 'components/Page';
|
|
||||||
import SearchFields from 'components/search/SearchFields';
|
|
||||||
import SearchResults from 'components/search/SearchResults';
|
|
||||||
import SearchSuggestions from 'components/search/SearchSuggestions';
|
|
||||||
import LiveTVSearchResults from 'components/search/LiveTVSearchResults';
|
|
||||||
import { usePrevious } from 'hooks/usePrevious';
|
import { usePrevious } from 'hooks/usePrevious';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import SearchFields from 'components/search/SearchFields';
|
||||||
|
import SearchSuggestions from 'components/search/SearchSuggestions';
|
||||||
|
import SearchResults from 'components/search/SearchResults';
|
||||||
|
|
||||||
const COLLECTION_TYPE_PARAM = 'collectionType';
|
const COLLECTION_TYPE_PARAM = 'collectionType';
|
||||||
const PARENT_ID_PARAM = 'parentId';
|
const PARENT_ID_PARAM = 'parentId';
|
||||||
const QUERY_PARAM = 'query';
|
const QUERY_PARAM = 'query';
|
||||||
const SERVER_ID_PARAM = 'serverId';
|
|
||||||
|
|
||||||
const Search: FC = () => {
|
const Search: FC = () => {
|
||||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const parentIdQuery = searchParams.get(PARENT_ID_PARAM) || undefined;
|
||||||
|
const collectionTypeQuery = searchParams.get(COLLECTION_TYPE_PARAM) || undefined;
|
||||||
const urlQuery = searchParams.get(QUERY_PARAM) || '';
|
const urlQuery = searchParams.get(QUERY_PARAM) || '';
|
||||||
const [ query, setQuery ] = useState(urlQuery);
|
const [query, setQuery] = useState(urlQuery);
|
||||||
const prevQuery = usePrevious(query, '');
|
const prevQuery = usePrevious(query, '');
|
||||||
|
const [debouncedQuery] = useDebounceValue(query, 500);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query !== prevQuery) {
|
if (query !== prevQuery) {
|
||||||
|
@ -49,23 +50,17 @@ const Search: FC = () => {
|
||||||
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
className='mainAnimatedPage libraryPage allLibraryPage noSecondaryNavPage'
|
||||||
>
|
>
|
||||||
<SearchFields query={query} onSearch={setQuery} />
|
<SearchFields query={query} onSearch={setQuery} />
|
||||||
{!query
|
{!query ? (
|
||||||
&& <SearchSuggestions
|
<SearchSuggestions
|
||||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
parentId={parentIdQuery}
|
||||||
/>
|
/>
|
||||||
}
|
) : (
|
||||||
<SearchResults
|
<SearchResults
|
||||||
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
parentId={parentIdQuery}
|
||||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
collectionType={collectionTypeQuery}
|
||||||
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
query={debouncedQuery}
|
||||||
query={query}
|
/>
|
||||||
/>
|
)}
|
||||||
<LiveTVSearchResults
|
|
||||||
serverId={searchParams.get(SERVER_ID_PARAM) || window.ApiClient.serverId()}
|
|
||||||
parentId={searchParams.get(PARENT_ID_PARAM)}
|
|
||||||
collectionType={searchParams.get(COLLECTION_TYPE_PARAM)}
|
|
||||||
query={query}
|
|
||||||
/>
|
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,191 +0,0 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
|
||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { type FC, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useDebounceValue } from 'usehooks-ts';
|
|
||||||
|
|
||||||
import globalize from '../../scripts/globalize';
|
|
||||||
import ServerConnections from '../ServerConnections';
|
|
||||||
import SearchResultsRow from './SearchResultsRow';
|
|
||||||
|
|
||||||
const CARD_OPTIONS = {
|
|
||||||
preferThumb: true,
|
|
||||||
inheritThumb: false,
|
|
||||||
showParentTitleOrTitle: true,
|
|
||||||
showTitle: false,
|
|
||||||
coverImage: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
showAirTime: true,
|
|
||||||
showAirDateTime: true,
|
|
||||||
showChannelName: true
|
|
||||||
};
|
|
||||||
|
|
||||||
type LiveTVSearchResultsProps = {
|
|
||||||
serverId?: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
collectionType?: string | null;
|
|
||||||
query?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
* React component to display search result rows for live tv library search
|
|
||||||
*/
|
|
||||||
const LiveTVSearchResults: FC<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
|
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ kids, setKids ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ news, setNews ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ debouncedQuery ] = useDebounceValue(query, 500);
|
|
||||||
|
|
||||||
const getDefaultParameters = useCallback(() => ({
|
|
||||||
ParentId: parentId,
|
|
||||||
searchTerm: debouncedQuery,
|
|
||||||
Limit: 24,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
|
||||||
Recursive: true,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
IncludePeople: false,
|
|
||||||
IncludeMedia: false,
|
|
||||||
IncludeGenres: false,
|
|
||||||
IncludeStudios: false,
|
|
||||||
IncludeArtists: false
|
|
||||||
}), [ parentId, debouncedQuery ]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
|
||||||
apiClient?.getCurrentUserId(),
|
|
||||||
{
|
|
||||||
...getDefaultParameters(),
|
|
||||||
IncludeMedia: true,
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reset state
|
|
||||||
setMovies([]);
|
|
||||||
setEpisodes([]);
|
|
||||||
setSports([]);
|
|
||||||
setKids([]);
|
|
||||||
setNews([]);
|
|
||||||
setPrograms([]);
|
|
||||||
setChannels([]);
|
|
||||||
|
|
||||||
if (!debouncedQuery || collectionType !== CollectionType.Livetv) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
|
||||||
|
|
||||||
// Movies row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsMovie: true
|
|
||||||
})
|
|
||||||
.then(result => setMovies(result.Items || []))
|
|
||||||
.catch(() => setMovies([]));
|
|
||||||
// Episodes row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsMovie: false,
|
|
||||||
IsSeries: true,
|
|
||||||
IsSports: false,
|
|
||||||
IsKids: false,
|
|
||||||
IsNews: false
|
|
||||||
})
|
|
||||||
.then(result => setEpisodes(result.Items || []))
|
|
||||||
.catch(() => setEpisodes([]));
|
|
||||||
// Sports row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsSports: true
|
|
||||||
})
|
|
||||||
.then(result => setSports(result.Items || []))
|
|
||||||
.catch(() => setSports([]));
|
|
||||||
// Kids row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsKids: true
|
|
||||||
})
|
|
||||||
.then(result => setKids(result.Items || []))
|
|
||||||
.catch(() => setKids([]));
|
|
||||||
// News row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsNews: true
|
|
||||||
})
|
|
||||||
.then(result => setNews(result.Items || []))
|
|
||||||
.catch(() => setNews([]));
|
|
||||||
// Programs row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
IncludeItemTypes: 'LiveTvProgram',
|
|
||||||
IsMovie: false,
|
|
||||||
IsSeries: false,
|
|
||||||
IsSports: false,
|
|
||||||
IsKids: false,
|
|
||||||
IsNews: false
|
|
||||||
})
|
|
||||||
.then(result => setPrograms(result.Items || []))
|
|
||||||
.catch(() => setPrograms([]));
|
|
||||||
// Channels row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
|
||||||
.then(result => setChannels(result.Items || []))
|
|
||||||
.catch(() => setChannels([]));
|
|
||||||
}, [collectionType, debouncedQuery, getDefaultParameters, parentId, serverId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'searchResults',
|
|
||||||
'padded-bottom-page',
|
|
||||||
'padded-top',
|
|
||||||
{ 'hide': !debouncedQuery || collectionType !== CollectionType.Livetv }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Movies')}
|
|
||||||
items={movies}
|
|
||||||
cardOptions={{
|
|
||||||
...CARD_OPTIONS,
|
|
||||||
shape: 'overflowPortrait'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Episodes')}
|
|
||||||
items={episodes}
|
|
||||||
cardOptions={CARD_OPTIONS}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Sports')}
|
|
||||||
items={sports}
|
|
||||||
cardOptions={CARD_OPTIONS}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Kids')}
|
|
||||||
items={kids}
|
|
||||||
cardOptions={CARD_OPTIONS}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('News')}
|
|
||||||
items={news}
|
|
||||||
cardOptions={CARD_OPTIONS}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Programs')}
|
|
||||||
items={programs}
|
|
||||||
cardOptions={CARD_OPTIONS}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Channels')}
|
|
||||||
items={channels}
|
|
||||||
cardOptions={{ shape: 'square' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LiveTVSearchResults;
|
|
|
@ -1,25 +1,22 @@
|
||||||
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
|
import React, { type ChangeEvent, type FC, useCallback, useRef } from 'react';
|
||||||
|
|
||||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
||||||
import Input from 'elements/emby-input/Input';
|
import Input from 'elements/emby-input/Input';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
import browser from '../../scripts/browser';
|
import browser from '../../scripts/browser';
|
||||||
|
|
||||||
import 'material-design-icons-iconfont';
|
import 'material-design-icons-iconfont';
|
||||||
|
|
||||||
import '../../styles/flexstyles.scss';
|
import '../../styles/flexstyles.scss';
|
||||||
import './searchfields.scss';
|
import './searchfields.scss';
|
||||||
|
|
||||||
type SearchFieldsProps = {
|
interface SearchFieldsProps {
|
||||||
query: string,
|
query: string,
|
||||||
onSearch?: (query: string) => void
|
onSearch?: (query: string) => void
|
||||||
};
|
}
|
||||||
|
|
||||||
const SearchFields: FC<SearchFieldsProps> = ({
|
const SearchFields: FC<SearchFieldsProps> = ({
|
||||||
onSearch = () => { /* no-op */ },
|
onSearch = () => { /* no-op */ },
|
||||||
query
|
query
|
||||||
}: SearchFieldsProps) => {
|
}) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const onAlphaPicked = useCallback((e: Event) => {
|
const onAlphaPicked = useCallback((e: Event) => {
|
||||||
|
|
|
@ -1,388 +1,58 @@
|
||||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
import React, { type FC } from 'react';
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { Section, useSearchItems } from 'hooks/searchHook';
|
||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import React, { type FC, useCallback, useEffect, useState } from 'react';
|
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
|
||||||
import { useDebounceValue } from 'usehooks-ts';
|
|
||||||
|
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import ServerConnections from '../ServerConnections';
|
|
||||||
import SearchResultsRow from './SearchResultsRow';
|
|
||||||
import Loading from '../loading/LoadingComponent';
|
import Loading from '../loading/LoadingComponent';
|
||||||
|
import SearchResultsRow from './SearchResultsRow';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
|
|
||||||
interface ParametersOptions {
|
interface SearchResultsProps {
|
||||||
ParentId?: string | null;
|
parentId?: string;
|
||||||
searchTerm?: string;
|
collectionType?: string;
|
||||||
Limit?: number;
|
query?: string;
|
||||||
Fields?: string;
|
|
||||||
Recursive?: boolean;
|
|
||||||
EnableTotalRecordCount?: boolean;
|
|
||||||
ImageTypeLimit?: number;
|
|
||||||
IncludePeople?: boolean;
|
|
||||||
IncludeMedia?: boolean;
|
|
||||||
IncludeGenres?: boolean;
|
|
||||||
IncludeStudios?: boolean;
|
|
||||||
IncludeArtists?: boolean;
|
|
||||||
IsMissing?: boolean;
|
|
||||||
IncludeItemTypes?: BaseItemKind;
|
|
||||||
MediaTypes?: string;
|
|
||||||
ExcludeItemTypes?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchResultsProps = {
|
|
||||||
serverId?: string;
|
|
||||||
parentId?: string | null;
|
|
||||||
collectionType?: string | null;
|
|
||||||
query?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ensureNonNullItems = (result: BaseItemDtoQueryResult) => ({
|
|
||||||
...result,
|
|
||||||
Items: result.Items || []
|
|
||||||
});
|
|
||||||
|
|
||||||
const isMovies = (collectionType: string) => collectionType === CollectionType.Movies;
|
|
||||||
|
|
||||||
const isMusic = (collectionType: string) => collectionType === CollectionType.Music;
|
|
||||||
|
|
||||||
const isTVShows = (collectionType: string) => collectionType === CollectionType.Tvshows;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* React component to display search result rows for global search and non-live tv library search
|
* React component to display search result rows for global search and library view search
|
||||||
*/
|
*/
|
||||||
const SearchResults: FC<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
|
const SearchResults: FC<SearchResultsProps> = ({
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
parentId,
|
||||||
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
|
collectionType,
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
query
|
||||||
const [ videos, setVideos ] = useState<BaseItemDto[]>([]);
|
}) => {
|
||||||
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
const { isLoading, data } = useSearchItems(parentId, collectionType, query);
|
||||||
const [ channels, setChannels ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ playlists, setPlaylists ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ artists, setArtists ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ albums, setAlbums ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ songs, setSongs ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ photoAlbums, setPhotoAlbums ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ photos, setPhotos ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ audioBooks, setAudioBooks ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
|
||||||
const [ debouncedQuery ] = useDebounceValue(query, 500);
|
|
||||||
|
|
||||||
const getDefaultParameters = useCallback(() => ({
|
if (isLoading) return <Loading />;
|
||||||
ParentId: parentId,
|
|
||||||
searchTerm: debouncedQuery,
|
|
||||||
Limit: 100,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
|
||||||
Recursive: true,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
IncludePeople: false,
|
|
||||||
IncludeMedia: false,
|
|
||||||
IncludeGenres: false,
|
|
||||||
IncludeStudios: false,
|
|
||||||
IncludeArtists: false
|
|
||||||
}), [ parentId, debouncedQuery ]);
|
|
||||||
|
|
||||||
const fetchArtists = useCallback((apiClient: ApiClient, params: ParametersOptions = {}) => (
|
if (!data?.length) {
|
||||||
apiClient?.getArtists(
|
return (
|
||||||
apiClient.getCurrentUserId(),
|
<div className='noItemsMessage centerMessage'>
|
||||||
{
|
{globalize.translate('SearchResultsEmpty', query)}
|
||||||
...getDefaultParameters(),
|
</div>
|
||||||
IncludeArtists: true,
|
);
|
||||||
...params
|
}
|
||||||
}
|
|
||||||
).then(ensureNonNullItems)
|
|
||||||
), [getDefaultParameters]);
|
|
||||||
|
|
||||||
const fetchItems = useCallback(async (apiClient?: ApiClient, params: ParametersOptions = {}) => {
|
const renderSection = (section: Section, index: number) => {
|
||||||
if (!apiClient) {
|
return (
|
||||||
console.error('[SearchResults] no apiClient; unable to fetch items');
|
<SearchResultsRow
|
||||||
return {
|
key={`${section.title}-${index}`}
|
||||||
Items: []
|
title={globalize.translate(section.title)}
|
||||||
};
|
items={section.items}
|
||||||
}
|
cardOptions={{
|
||||||
|
shape: CardShape.AutoOverflow,
|
||||||
const options: ParametersOptions = {
|
scalable: true,
|
||||||
...getDefaultParameters(),
|
showTitle: true,
|
||||||
IncludeMedia: true,
|
overlayText: false,
|
||||||
...params
|
centerText: true,
|
||||||
};
|
allowBottomPadding: false,
|
||||||
|
...section.cardOptions
|
||||||
if (params.IncludeItemTypes === BaseItemKind.Episode) {
|
}}
|
||||||
const user = await apiClient.getCurrentUser();
|
/>
|
||||||
if (!user?.Configuration?.DisplayMissingEpisodes) {
|
);
|
||||||
options.IsMissing = false;
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiClient.getItems(
|
|
||||||
apiClient.getCurrentUserId(),
|
|
||||||
options
|
|
||||||
).then(ensureNonNullItems);
|
|
||||||
}, [getDefaultParameters]);
|
|
||||||
|
|
||||||
const fetchPeople = useCallback((apiClient: ApiClient, params: ParametersOptions = {}) => (
|
|
||||||
apiClient?.getPeople(
|
|
||||||
apiClient.getCurrentUserId(),
|
|
||||||
{
|
|
||||||
...getDefaultParameters(),
|
|
||||||
IncludePeople: true,
|
|
||||||
...params
|
|
||||||
}
|
|
||||||
).then(ensureNonNullItems)
|
|
||||||
), [getDefaultParameters]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (query) setIsLoading(true);
|
|
||||||
}, [ query ]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Reset state
|
|
||||||
setMovies([]);
|
|
||||||
setShows([]);
|
|
||||||
setEpisodes([]);
|
|
||||||
setVideos([]);
|
|
||||||
setPrograms([]);
|
|
||||||
setChannels([]);
|
|
||||||
setPlaylists([]);
|
|
||||||
setArtists([]);
|
|
||||||
setAlbums([]);
|
|
||||||
setSongs([]);
|
|
||||||
setPhotoAlbums([]);
|
|
||||||
setPhotos([]);
|
|
||||||
setAudioBooks([]);
|
|
||||||
setBooks([]);
|
|
||||||
setPeople([]);
|
|
||||||
setCollections([]);
|
|
||||||
|
|
||||||
if (!debouncedQuery) {
|
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
|
||||||
const fetchPromises = [];
|
|
||||||
|
|
||||||
// Movie libraries
|
|
||||||
if (!collectionType || isMovies(collectionType)) {
|
|
||||||
fetchPromises.push(
|
|
||||||
// Movies row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Movie' })
|
|
||||||
.then(result => setMovies(result.Items))
|
|
||||||
.catch(() => setMovies([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TV Show libraries
|
|
||||||
if (!collectionType || isTVShows(collectionType)) {
|
|
||||||
fetchPromises.push(
|
|
||||||
// Shows row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Series' })
|
|
||||||
.then(result => setShows(result.Items))
|
|
||||||
.catch(() => setShows([])),
|
|
||||||
// Episodes row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Episode' })
|
|
||||||
.then(result => setEpisodes(result.Items))
|
|
||||||
.catch(() => setEpisodes([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// People are included for Movies and TV Shows
|
|
||||||
if (!collectionType || isMovies(collectionType) || isTVShows(collectionType)) {
|
|
||||||
fetchPromises.push(
|
|
||||||
// People row
|
|
||||||
fetchPeople(apiClient)
|
|
||||||
.then(result => setPeople(result.Items))
|
|
||||||
.catch(() => setPeople([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Music libraries
|
|
||||||
if (!collectionType || isMusic(collectionType)) {
|
|
||||||
fetchPromises.push(
|
|
||||||
// Playlists row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Playlist' })
|
|
||||||
.then(results => setPlaylists(results.Items))
|
|
||||||
.catch(() => setPlaylists([])),
|
|
||||||
// Artists row
|
|
||||||
fetchArtists(apiClient)
|
|
||||||
.then(result => setArtists(result.Items))
|
|
||||||
.catch(() => setArtists([])),
|
|
||||||
// Albums row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'MusicAlbum' })
|
|
||||||
.then(result => setAlbums(result.Items))
|
|
||||||
.catch(() => setAlbums([])),
|
|
||||||
// Songs row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Audio' })
|
|
||||||
.then(result => setSongs(result.Items))
|
|
||||||
.catch(() => setSongs([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other libraries do not support in-library search currently
|
|
||||||
if (!collectionType) {
|
|
||||||
fetchPromises.push(
|
|
||||||
// Videos row
|
|
||||||
fetchItems(apiClient, {
|
|
||||||
MediaTypes: 'Video',
|
|
||||||
ExcludeItemTypes: 'Movie,Episode,TvChannel'
|
|
||||||
})
|
|
||||||
.then(result => setVideos(result.Items))
|
|
||||||
.catch(() => setVideos([])),
|
|
||||||
// Programs row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'LiveTvProgram' })
|
|
||||||
.then(result => setPrograms(result.Items))
|
|
||||||
.catch(() => setPrograms([])),
|
|
||||||
// Channels row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'TvChannel' })
|
|
||||||
.then(result => setChannels(result.Items))
|
|
||||||
.catch(() => setChannels([])),
|
|
||||||
// Photo Albums row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'PhotoAlbum' })
|
|
||||||
.then(result => setPhotoAlbums(result.Items))
|
|
||||||
.catch(() => setPhotoAlbums([])),
|
|
||||||
// Photos row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Photo' })
|
|
||||||
.then(result => setPhotos(result.Items))
|
|
||||||
.catch(() => setPhotos([])),
|
|
||||||
// Audio Books row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'AudioBook' })
|
|
||||||
.then(result => setAudioBooks(result.Items))
|
|
||||||
.catch(() => setAudioBooks([])),
|
|
||||||
// Books row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'Book' })
|
|
||||||
.then(result => setBooks(result.Items))
|
|
||||||
.catch(() => setBooks([])),
|
|
||||||
// Collections row
|
|
||||||
fetchItems(apiClient, { IncludeItemTypes: 'BoxSet' })
|
|
||||||
.then(result => setCollections(result.Items))
|
|
||||||
.catch(() => setCollections([]))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Promise.all(fetchPromises)
|
|
||||||
.then(() => {
|
|
||||||
setIsLoading(false); // Set loading to false when all fetch calls are done
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('An error occurred while fetching data:', error);
|
|
||||||
setIsLoading(false); // Set loading to false even if an error occurs
|
|
||||||
});
|
|
||||||
}, [collectionType, fetchArtists, fetchItems, fetchPeople, debouncedQuery, serverId]);
|
|
||||||
|
|
||||||
const allEmpty = [movies, shows, episodes, videos, programs, channels, playlists, artists, albums, songs, photoAlbums, photos, audioBooks, books, people, collections].every(arr => arr.length === 0);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={'searchResults, padded-top, padded-bottom-page'}>
|
||||||
className={classNames(
|
{data.map((section, index) => renderSection(section, index))}
|
||||||
'searchResults',
|
|
||||||
'padded-bottom-page',
|
|
||||||
'padded-top',
|
|
||||||
{ 'hide': !debouncedQuery || collectionType === CollectionType.Livetv }
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loading />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Movies')}
|
|
||||||
items={movies}
|
|
||||||
cardOptions={{ showYear: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Shows')}
|
|
||||||
items={shows}
|
|
||||||
cardOptions={{ showYear: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Episodes')}
|
|
||||||
items={episodes}
|
|
||||||
cardOptions={{
|
|
||||||
coverImage: true,
|
|
||||||
showParentTitle: true
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('HeaderVideos')}
|
|
||||||
items={videos}
|
|
||||||
cardOptions={{ showParentTitle: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Programs')}
|
|
||||||
items={programs}
|
|
||||||
cardOptions={{
|
|
||||||
preferThumb: true,
|
|
||||||
inheritThumb: false,
|
|
||||||
showParentTitleOrTitle: true,
|
|
||||||
showTitle: false,
|
|
||||||
coverImage: true,
|
|
||||||
overlayMoreButton: true,
|
|
||||||
showAirTime: true,
|
|
||||||
showAirDateTime: true,
|
|
||||||
showChannelName: true
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Channels')}
|
|
||||||
items={channels}
|
|
||||||
cardOptions={{ shape: 'square' }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Playlists')}
|
|
||||||
items={playlists}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Artists')}
|
|
||||||
items={artists}
|
|
||||||
cardOptions={{ coverImage: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Albums')}
|
|
||||||
items={albums}
|
|
||||||
cardOptions={{ showParentTitle: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Songs')}
|
|
||||||
items={songs}
|
|
||||||
cardOptions={{ showParentTitle: true }}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('HeaderPhotoAlbums')}
|
|
||||||
items={photoAlbums}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Photos')}
|
|
||||||
items={photos}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('HeaderAudioBooks')}
|
|
||||||
items={audioBooks}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Books')}
|
|
||||||
items={books}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('Collections')}
|
|
||||||
items={collections}
|
|
||||||
/>
|
|
||||||
<SearchResultsRow
|
|
||||||
title={globalize.translate('People')}
|
|
||||||
items={people}
|
|
||||||
cardOptions={{ coverImage: true }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{allEmpty && debouncedQuery && !isLoading && (
|
|
||||||
<div className='noItemsMessage centerMessage'>
|
|
||||||
{globalize.translate('SearchResultsEmpty', debouncedQuery)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import React, { FunctionComponent, useEffect, useRef } from 'react';
|
import React, { type FC, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import cardBuilder from '../cardbuilder/cardBuilder';
|
import cardBuilder from '../cardbuilder/cardBuilder';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
import '../../elements/emby-scroller/emby-scroller';
|
import '../../elements/emby-scroller/emby-scroller';
|
||||||
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
import '../../elements/emby-itemscontainer/emby-itemscontainer';
|
||||||
|
|
||||||
|
@ -16,46 +16,18 @@ const createScroller = ({ title = '' }) => ({
|
||||||
</div>`
|
</div>`
|
||||||
});
|
});
|
||||||
|
|
||||||
type CardOptions = {
|
interface SearchResultsRowProps {
|
||||||
itemsContainer?: HTMLElement,
|
|
||||||
parentContainer?: HTMLElement,
|
|
||||||
allowBottomPadding?: boolean,
|
|
||||||
centerText?: boolean,
|
|
||||||
coverImage?: boolean,
|
|
||||||
inheritThumb?: boolean,
|
|
||||||
overlayMoreButton?: boolean,
|
|
||||||
overlayText?: boolean,
|
|
||||||
preferThumb?: boolean,
|
|
||||||
scalable?: boolean,
|
|
||||||
shape?: string,
|
|
||||||
showParentTitle?: boolean,
|
|
||||||
showParentTitleOrTitle?: boolean,
|
|
||||||
showAirTime?: boolean,
|
|
||||||
showAirDateTime?: boolean,
|
|
||||||
showChannelName?: boolean,
|
|
||||||
showTitle?: boolean,
|
|
||||||
showYear?: boolean
|
|
||||||
};
|
|
||||||
|
|
||||||
type SearchResultsRowProps = {
|
|
||||||
title?: string;
|
title?: string;
|
||||||
items?: BaseItemDto[];
|
items?: BaseItemDto[];
|
||||||
cardOptions?: CardOptions;
|
cardOptions?: CardOptions;
|
||||||
};
|
}
|
||||||
|
|
||||||
const SearchResultsRow: FunctionComponent<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }: SearchResultsRowProps) => {
|
const SearchResultsRow: FC<SearchResultsRowProps> = ({ title, items = [], cardOptions = {} }) => {
|
||||||
const element = useRef<HTMLDivElement>(null);
|
const element = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cardBuilder.buildCards(items, {
|
cardBuilder.buildCards(items, {
|
||||||
itemsContainer: element.current?.querySelector('.itemsContainer'),
|
itemsContainer: element.current?.querySelector('.itemsContainer'),
|
||||||
parentContainer: element.current,
|
|
||||||
shape: 'autooverflow',
|
|
||||||
scalable: true,
|
|
||||||
showTitle: true,
|
|
||||||
overlayText: false,
|
|
||||||
centerText: true,
|
|
||||||
allowBottomPadding: false,
|
|
||||||
...cardOptions
|
...cardOptions
|
||||||
});
|
});
|
||||||
}, [cardOptions, items]);
|
}, [cardOptions, items]);
|
||||||
|
|
|
@ -1,57 +1,19 @@
|
||||||
import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
import React, { type FC } from 'react';
|
||||||
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
import { useSearchSuggestions } from 'hooks/searchHook';
|
||||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
import { ItemSortBy } from '@jellyfin/sdk/lib/models/api/item-sort-by';
|
|
||||||
import escapeHtml from 'escape-html';
|
|
||||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { appRouter } from '../router/appRouter';
|
import { appRouter } from '../router/appRouter';
|
||||||
import { useApi } from '../../hooks/useApi';
|
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
import LinkButton from 'elements/emby-button/LinkButton';
|
||||||
import '../../elements/emby-button/emby-button';
|
import '../../elements/emby-button/emby-button';
|
||||||
|
|
||||||
// There seems to be some compatibility issues here between
|
interface SearchSuggestionsProps {
|
||||||
// React and our legacy web components, so we need to inject
|
parentId?: string;
|
||||||
// them as an html string for now =/
|
}
|
||||||
const createSuggestionLink = ({ name, href }: { name: string, href: string }) => ({
|
|
||||||
__html: `<a
|
|
||||||
is='emby-linkbutton'
|
|
||||||
class='button-link'
|
|
||||||
style='display: inline-block; padding: 0.5em 1em;'
|
|
||||||
href='${href}'
|
|
||||||
>${escapeHtml(name)}</a>`
|
|
||||||
});
|
|
||||||
|
|
||||||
type SearchSuggestionsProps = {
|
const SearchSuggestions: FC<SearchSuggestionsProps> = ({ parentId }) => {
|
||||||
parentId?: string | null;
|
const { isLoading, data: suggestions } = useSearchSuggestions(parentId);
|
||||||
};
|
|
||||||
|
|
||||||
const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId }: SearchSuggestionsProps) => {
|
if (isLoading) return <Loading />;
|
||||||
const [ suggestions, setSuggestions ] = useState<BaseItemDto[]>([]);
|
|
||||||
const { api, user } = useApi();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (api && user?.Id) {
|
|
||||||
getItemsApi(api)
|
|
||||||
.getItems({
|
|
||||||
userId: user.Id,
|
|
||||||
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
|
||||||
includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series, BaseItemKind.MusicArtist],
|
|
||||||
limit: 20,
|
|
||||||
recursive: true,
|
|
||||||
imageTypeLimit: 0,
|
|
||||||
enableImages: false,
|
|
||||||
parentId: parentId || undefined,
|
|
||||||
enableTotalRecordCount: false
|
|
||||||
})
|
|
||||||
.then(result => setSuggestions(result.data.Items || []))
|
|
||||||
.catch(err => {
|
|
||||||
console.error('[SearchSuggestions] failed to fetch search suggestions', err);
|
|
||||||
setSuggestions([]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [ api, parentId, user ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -65,14 +27,19 @@ const SearchSuggestions: FunctionComponent<SearchSuggestionsProps> = ({ parentId
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='searchSuggestionsList padded-left padded-right'>
|
<div className='searchSuggestionsList padded-left padded-right'>
|
||||||
{suggestions.map(item => (
|
{suggestions?.map((item) => (
|
||||||
<div
|
<div key={`suggestion-${item.Id}`}>
|
||||||
key={`suggestion-${item.Id}`}
|
<LinkButton
|
||||||
dangerouslySetInnerHTML={createSuggestionLink({
|
className='button-link'
|
||||||
name: item.Name || '',
|
href={appRouter.getRouteUrl(item)}
|
||||||
href: appRouter.getRouteUrl(item)
|
style={{
|
||||||
})}
|
display: 'inline-block',
|
||||||
/>
|
padding: '0.5em 1em'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.Name}
|
||||||
|
</LinkButton>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
2
src/hooks/searchHook/index.ts
Normal file
2
src/hooks/searchHook/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from './useSearchItems';
|
||||||
|
export * from './useSearchSuggestions';
|
509
src/hooks/searchHook/useSearchItems.ts
Normal file
509
src/hooks/searchHook/useSearchItems.ts
Normal file
|
@ -0,0 +1,509 @@
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import type { Api } from '@jellyfin/sdk';
|
||||||
|
import type {
|
||||||
|
ArtistsApiGetArtistsRequest,
|
||||||
|
BaseItemDto,
|
||||||
|
ItemsApiGetItemsRequest,
|
||||||
|
PersonsApiGetPersonsRequest
|
||||||
|
} from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
|
import { ItemFields } from '@jellyfin/sdk/lib/generated-client/models/item-fields';
|
||||||
|
import { MediaType } from '@jellyfin/sdk/lib/generated-client/models/media-type';
|
||||||
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
|
import { getPersonsApi } from '@jellyfin/sdk/lib/utils/api/persons-api';
|
||||||
|
import { getArtistsApi } from '@jellyfin/sdk/lib/utils/api/artists-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import type { CardOptions } from 'types/cardOptions';
|
||||||
|
import { CardShape } from 'utils/card';
|
||||||
|
|
||||||
|
const QUERY_OPTIONS = {
|
||||||
|
limit: 100,
|
||||||
|
fields: [
|
||||||
|
ItemFields.PrimaryImageAspectRatio,
|
||||||
|
ItemFields.CanDelete,
|
||||||
|
ItemFields.MediaSourceCount
|
||||||
|
],
|
||||||
|
enableTotalRecordCount: false,
|
||||||
|
imageTypeLimit: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchItemsByType = async (
|
||||||
|
api: Api,
|
||||||
|
userId?: string,
|
||||||
|
params?: ItemsApiGetItemsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const response = await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
...QUERY_OPTIONS,
|
||||||
|
userId: userId,
|
||||||
|
recursive: true,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchPeople = async (
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params?: PersonsApiGetPersonsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const response = await getPersonsApi(api).getPersons(
|
||||||
|
{
|
||||||
|
...QUERY_OPTIONS,
|
||||||
|
userId: userId,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchArtists = async (
|
||||||
|
api: Api,
|
||||||
|
userId: string,
|
||||||
|
params?: ArtistsApiGetArtistsRequest,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
const response = await getArtistsApi(api).getArtists(
|
||||||
|
{
|
||||||
|
...QUERY_OPTIONS,
|
||||||
|
userId: userId,
|
||||||
|
...params
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMovies = (collectionType: string) =>
|
||||||
|
collectionType === CollectionType.Movies;
|
||||||
|
|
||||||
|
const isMusic = (collectionType: string) =>
|
||||||
|
collectionType === CollectionType.Music;
|
||||||
|
|
||||||
|
const isTVShows = (collectionType: string) =>
|
||||||
|
collectionType === CollectionType.Tvshows;
|
||||||
|
|
||||||
|
const isLivetv = (collectionType: string) =>
|
||||||
|
collectionType === CollectionType.Livetv;
|
||||||
|
|
||||||
|
const LIVETV_CARD_OPTIONS = {
|
||||||
|
preferThumb: true,
|
||||||
|
inheritThumb: false,
|
||||||
|
showParentTitleOrTitle: true,
|
||||||
|
showTitle: false,
|
||||||
|
coverImage: true,
|
||||||
|
overlayMoreButton: true,
|
||||||
|
showAirTime: true,
|
||||||
|
showAirDateTime: true,
|
||||||
|
showChannelName: true
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Section {
|
||||||
|
title: string
|
||||||
|
items: BaseItemDto[];
|
||||||
|
cardOptions?: CardOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSearchItems = (
|
||||||
|
parentId?: string,
|
||||||
|
collectionType?: string,
|
||||||
|
searchTerm?: string
|
||||||
|
) => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const userId = user?.Id;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['SearchItems', { parentId, collectionType, searchTerm }],
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
if (!api) throw new Error('No API instance available');
|
||||||
|
if (!userId) throw new Error('No User ID provided');
|
||||||
|
|
||||||
|
const sections: Section[] = [];
|
||||||
|
|
||||||
|
const addSection = (
|
||||||
|
title: string,
|
||||||
|
items: BaseItemDto[] | null | undefined,
|
||||||
|
cardOptions?: CardOptions
|
||||||
|
) => {
|
||||||
|
if (items && items?.length > 0) {
|
||||||
|
sections.push({ title, items, cardOptions });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Livetv libraries
|
||||||
|
if (collectionType && isLivetv(collectionType)) {
|
||||||
|
// Movies row
|
||||||
|
const moviesData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isMovie: true,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Movies', moviesData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS,
|
||||||
|
shape: CardShape.PortraitOverflow
|
||||||
|
});
|
||||||
|
|
||||||
|
// Episodes row
|
||||||
|
const episodesData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isMovie: false,
|
||||||
|
isSeries: true,
|
||||||
|
isSports: false,
|
||||||
|
isKids: false,
|
||||||
|
isNews: false,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Episodes', episodesData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sports row
|
||||||
|
const sportsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isSports: true,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Sports', sportsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Kids row
|
||||||
|
const kidsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isKids: true,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Kids', kidsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// News row
|
||||||
|
const newsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isNews: true,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('News', newsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Programs row
|
||||||
|
const programsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
isMovie: false,
|
||||||
|
isSeries: false,
|
||||||
|
isSports: false,
|
||||||
|
isKids: false,
|
||||||
|
isNews: false,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Programs', programsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Channels row
|
||||||
|
const channelsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.TvChannel],
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Channels', channelsData.Items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movie libraries
|
||||||
|
if (!collectionType || isMovies(collectionType)) {
|
||||||
|
// Movies row
|
||||||
|
const moviesData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Movie],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Movies', moviesData.Items, {
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TV Show libraries
|
||||||
|
if (!collectionType || isTVShows(collectionType)) {
|
||||||
|
// Shows row
|
||||||
|
const showsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Series],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Shows', showsData.Items, {
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Episodes row
|
||||||
|
const episodesData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Episode],
|
||||||
|
parentId: parentId,
|
||||||
|
isMissing: user?.Configuration?.DisplayMissingEpisodes,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Episodes', episodesData.Items, {
|
||||||
|
coverImage: true,
|
||||||
|
showParentTitle: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// People are included for Movies and TV Shows
|
||||||
|
if (
|
||||||
|
!collectionType
|
||||||
|
|| isMovies(collectionType)
|
||||||
|
|| isTVShows(collectionType)
|
||||||
|
) {
|
||||||
|
// People row
|
||||||
|
const peopleData = await fetchPeople(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('People', peopleData.Items, {
|
||||||
|
coverImage: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Music libraries
|
||||||
|
if (!collectionType || isMusic(collectionType)) {
|
||||||
|
// Playlists row
|
||||||
|
const playlistsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Playlist],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Playlists', playlistsData.Items);
|
||||||
|
|
||||||
|
// Artists row
|
||||||
|
const artistsData = await fetchArtists(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Artists', artistsData.Items, {
|
||||||
|
coverImage: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Albums row
|
||||||
|
const albumsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.MusicAlbum],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Albums', albumsData.Items, {
|
||||||
|
showYear: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Songs row
|
||||||
|
const songsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Audio],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Songs', songsData.Items, {
|
||||||
|
showParentTitle: true,
|
||||||
|
shape: CardShape.SquareOverflow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other libraries do not support in-library search currently
|
||||||
|
if (!collectionType) {
|
||||||
|
// Videos row
|
||||||
|
const videosData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
mediaTypes: [MediaType.Video],
|
||||||
|
excludeItemTypes: [
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.Episode,
|
||||||
|
BaseItemKind.TvChannel
|
||||||
|
],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
addSection('HeaderVideos', videosData.Items, {
|
||||||
|
showParentTitle: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Programs row
|
||||||
|
const programsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.LiveTvProgram],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Programs', programsData.Items, {
|
||||||
|
...LIVETV_CARD_OPTIONS
|
||||||
|
});
|
||||||
|
|
||||||
|
// Channels row
|
||||||
|
const channelsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.TvChannel],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Channels', channelsData.Items);
|
||||||
|
|
||||||
|
// Photo Albums row
|
||||||
|
const photoAlbumsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.PhotoAlbum],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('HeaderPhotoAlbums', photoAlbumsData.Items);
|
||||||
|
|
||||||
|
// Photos row
|
||||||
|
const photosData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Photo],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Photos', photosData.Items);
|
||||||
|
|
||||||
|
// Audio Books row
|
||||||
|
const audioBooksData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.AudioBook],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('HeaderAudioBooks', audioBooksData.Items);
|
||||||
|
|
||||||
|
// Books row
|
||||||
|
const booksData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.Book],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Books', booksData.Items);
|
||||||
|
|
||||||
|
// Collections row
|
||||||
|
const collectionsData = await fetchItemsByType(
|
||||||
|
api,
|
||||||
|
userId,
|
||||||
|
{
|
||||||
|
includeItemTypes: [BaseItemKind.BoxSet],
|
||||||
|
parentId: parentId,
|
||||||
|
searchTerm: searchTerm
|
||||||
|
},
|
||||||
|
{ signal }
|
||||||
|
);
|
||||||
|
addSection('Collections', collectionsData.Items);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections;
|
||||||
|
},
|
||||||
|
enabled: !!api && !!userId
|
||||||
|
});
|
||||||
|
};
|
49
src/hooks/searchHook/useSearchSuggestions.ts
Normal file
49
src/hooks/searchHook/useSearchSuggestions.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import type { Api } from '@jellyfin/sdk';
|
||||||
|
import { ItemSortBy } from '@jellyfin/sdk/lib/generated-client';
|
||||||
|
import { BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models/base-item-kind';
|
||||||
|
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api/items-api';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
|
||||||
|
const fetchGetItems = async (
|
||||||
|
api?: Api,
|
||||||
|
userId?: string,
|
||||||
|
parentId?: string,
|
||||||
|
options?: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
|
if (!api) throw new Error('No API instance available');
|
||||||
|
if (!userId) throw new Error('No User ID provided');
|
||||||
|
|
||||||
|
const response = await getItemsApi(api).getItems(
|
||||||
|
{
|
||||||
|
userId: userId,
|
||||||
|
sortBy: [ItemSortBy.IsFavoriteOrLiked, ItemSortBy.Random],
|
||||||
|
includeItemTypes: [
|
||||||
|
BaseItemKind.Movie,
|
||||||
|
BaseItemKind.Series,
|
||||||
|
BaseItemKind.MusicArtist
|
||||||
|
],
|
||||||
|
limit: 20,
|
||||||
|
recursive: true,
|
||||||
|
imageTypeLimit: 0,
|
||||||
|
enableImages: false,
|
||||||
|
parentId: parentId,
|
||||||
|
enableTotalRecordCount: false
|
||||||
|
},
|
||||||
|
options
|
||||||
|
);
|
||||||
|
return response.data.Items || [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSearchSuggestions = (parentId?: string) => {
|
||||||
|
const { api, user } = useApi();
|
||||||
|
const userId = user?.Id;
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['SearchSuggestions', { parentId }],
|
||||||
|
queryFn: ({ signal }) =>
|
||||||
|
fetchGetItems(api, userId, parentId, { signal }),
|
||||||
|
enabled: !!api && !!userId
|
||||||
|
});
|
||||||
|
};
|
Loading…
Add table
Add a link
Reference in a new issue