mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2024-10-07 03:13:43 +00:00
Fix issues with search url param
This commit is contained in:
parent
d6b8ce0f49
commit
bdecaa9930
29
package-lock.json
generated
29
package-lock.json
generated
@ -58,6 +58,7 @@
|
|||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.5",
|
||||||
|
"usehooks-ts": "2.14.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
@ -12671,8 +12672,7 @@
|
|||||||
"node_modules/lodash.debounce": {
|
"node_modules/lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
@ -21900,6 +21900,20 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/usehooks-ts": {
|
||||||
|
"version": "2.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||||
|
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
@ -31866,8 +31880,7 @@
|
|||||||
"lodash.debounce": {
|
"lodash.debounce": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
|
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.memoize": {
|
"lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
@ -38668,6 +38681,14 @@
|
|||||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"usehooks-ts": {
|
||||||
|
"version": "2.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||||
|
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||||
|
"requires": {
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"util-deprecate": {
|
"util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
@ -119,6 +119,7 @@
|
|||||||
"screenfull": "6.0.2",
|
"screenfull": "6.0.2",
|
||||||
"sortablejs": "1.15.2",
|
"sortablejs": "1.15.2",
|
||||||
"swiper": "11.0.5",
|
"swiper": "11.0.5",
|
||||||
|
"usehooks-ts": "2.14.0",
|
||||||
"webcomponents.js": "0.7.24",
|
"webcomponents.js": "0.7.24",
|
||||||
"whatwg-fetch": "3.6.20"
|
"whatwg-fetch": "3.6.20"
|
||||||
},
|
},
|
||||||
|
@ -1,41 +1,36 @@
|
|||||||
import React, { FunctionComponent, useEffect, useRef, useState } from 'react';
|
import React, { type FC, useEffect, useState } from 'react';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import Page from '../../../components/Page';
|
import Page from 'components/Page';
|
||||||
import SearchFields from '../../../components/search/SearchFields';
|
import SearchFields from 'components/search/SearchFields';
|
||||||
import SearchResults from '../../../components/search/SearchResults';
|
import SearchResults from 'components/search/SearchResults';
|
||||||
import SearchSuggestions from '../../../components/search/SearchSuggestions';
|
import SearchSuggestions from 'components/search/SearchSuggestions';
|
||||||
import LiveTVSearchResults from '../../../components/search/LiveTVSearchResults';
|
import LiveTVSearchResults from 'components/search/LiveTVSearchResults';
|
||||||
import globalize from '../../../scripts/globalize';
|
import { usePrevious } from 'hooks/usePrevious';
|
||||||
import { history } from '../../../components/router/appRouter';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
function usePrevious(value: string) {
|
const Search: FC = () => {
|
||||||
const ref = useRef<string>('');
|
const navigate = useNavigate();
|
||||||
useEffect(() => {
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
ref.current = value;
|
|
||||||
});
|
|
||||||
return ref.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Search: FunctionComponent = () => {
|
|
||||||
const [ searchParams ] = useSearchParams();
|
|
||||||
const urlQuery = searchParams.get('query') || '';
|
const urlQuery = searchParams.get('query') || '';
|
||||||
const [ query, setQuery ] = useState<string>(urlQuery);
|
const [ query, setQuery ] = useState(urlQuery);
|
||||||
const prevQuery = usePrevious(query);
|
const prevQuery = usePrevious(query, '');
|
||||||
|
|
||||||
if (query == prevQuery && urlQuery != query) {
|
|
||||||
setQuery(urlQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newSearch = query ? `?query=${query}` : '';
|
if (query !== prevQuery) {
|
||||||
if (query != prevQuery && newSearch != history.location.search) {
|
if (query === '' && urlQuery !== '') {
|
||||||
/* Explicitly using `window.history.pushState` instead of `history.replace` as the use of the latter
|
// The query input has been cleared; navigate back to the search landing page
|
||||||
triggers a re-rendering of this component, resulting in double-execution searches. If there's a
|
navigate(-1);
|
||||||
way to use `history` without this side effect, it would likely be preferable. */
|
} else if (query !== urlQuery) {
|
||||||
window.history.pushState({}, '', `/#${history.location.pathname}${newSearch}`);
|
// Update the query url param value
|
||||||
|
searchParams.set('query', query);
|
||||||
|
setSearchParams(searchParams, { replace: !!urlQuery });
|
||||||
|
}
|
||||||
|
} else if (query !== urlQuery) {
|
||||||
|
// Update the query if the query url param has changed
|
||||||
|
setQuery(urlQuery);
|
||||||
}
|
}
|
||||||
}, [query, prevQuery]);
|
}, [query, prevQuery, navigate, searchParams, setSearchParams, urlQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
|
@ -2,7 +2,8 @@ import type { BaseItemDto } from '@jellyfin/sdk/lib/generated-client';
|
|||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
import type { ApiClient } from 'jellyfin-apiclient';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
import React, { type FC, useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDebounceValue } from 'usehooks-ts';
|
||||||
|
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
import ServerConnections from '../ServerConnections';
|
import ServerConnections from '../ServerConnections';
|
||||||
@ -30,7 +31,7 @@ type LiveTVSearchResultsProps = {
|
|||||||
/*
|
/*
|
||||||
* React component to display search result rows for live tv library search
|
* React component to display search result rows for live tv library search
|
||||||
*/
|
*/
|
||||||
const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
|
const LiveTVSearchResults: FC<LiveTVSearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: LiveTVSearchResultsProps) => {
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
||||||
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
|
const [ sports, setSports ] = useState<BaseItemDto[]>([]);
|
||||||
@ -38,23 +39,24 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||||||
const [ news, setNews ] = useState<BaseItemDto[]>([]);
|
const [ news, setNews ] = useState<BaseItemDto[]>([]);
|
||||||
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
const [ programs, setPrograms ] = useState<BaseItemDto[]>([]);
|
||||||
const [ channels, setChannels ] = 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(() => {
|
useEffect(() => {
|
||||||
const getDefaultParameters = () => ({
|
|
||||||
ParentId: parentId,
|
|
||||||
searchTerm: query,
|
|
||||||
Limit: 24,
|
|
||||||
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
|
||||||
Recursive: true,
|
|
||||||
EnableTotalRecordCount: false,
|
|
||||||
ImageTypeLimit: 1,
|
|
||||||
IncludePeople: false,
|
|
||||||
IncludeMedia: false,
|
|
||||||
IncludeGenres: false,
|
|
||||||
IncludeStudios: false,
|
|
||||||
IncludeArtists: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
const fetchItems = (apiClient: ApiClient, params = {}) => apiClient?.getItems(
|
||||||
apiClient?.getCurrentUserId(),
|
apiClient?.getCurrentUserId(),
|
||||||
{
|
{
|
||||||
@ -73,65 +75,67 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||||||
setPrograms([]);
|
setPrograms([]);
|
||||||
setChannels([]);
|
setChannels([]);
|
||||||
|
|
||||||
if (query && collectionType === CollectionType.Livetv) {
|
if (!debouncedQuery || collectionType !== CollectionType.Livetv) {
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
return;
|
||||||
|
|
||||||
// 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, parentId, query, serverId]);
|
|
||||||
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -139,7 +143,7 @@ const LiveTVSearchResults: FunctionComponent<LiveTVSearchResultsProps> = ({ serv
|
|||||||
'searchResults',
|
'searchResults',
|
||||||
'padded-bottom-page',
|
'padded-bottom-page',
|
||||||
'padded-top',
|
'padded-top',
|
||||||
{ 'hide': !query || collectionType !== CollectionType.Livetv }
|
{ 'hide': !debouncedQuery || collectionType !== CollectionType.Livetv }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<SearchResultsRow
|
<SearchResultsRow
|
||||||
|
@ -1,95 +1,61 @@
|
|||||||
import debounce from 'lodash-es/debounce';
|
import React, { type ChangeEvent, type FC, useCallback } from 'react';
|
||||||
import React, { FunctionComponent, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
import AlphaPicker from '../alphaPicker/AlphaPickerComponent';
|
||||||
|
import Input from 'elements/emby-input/Input';
|
||||||
import globalize from '../../scripts/globalize';
|
import globalize from '../../scripts/globalize';
|
||||||
|
|
||||||
import 'material-design-icons-iconfont';
|
|
||||||
|
|
||||||
import '../../elements/emby-input/emby-input';
|
|
||||||
import '../../styles/flexstyles.scss';
|
|
||||||
import './searchfields.scss';
|
|
||||||
import layoutManager from '../layoutManager';
|
import layoutManager from '../layoutManager';
|
||||||
import browser from '../../scripts/browser';
|
import browser from '../../scripts/browser';
|
||||||
|
|
||||||
// There seems to be some compatibility issues here between
|
import 'material-design-icons-iconfont';
|
||||||
// React and our legacy web components, so we need to inject
|
|
||||||
// them as an html string for now =/
|
|
||||||
const createInputElement = () => ({
|
|
||||||
__html: `<input
|
|
||||||
is="emby-input"
|
|
||||||
class="searchfields-txtSearch"
|
|
||||||
type="text"
|
|
||||||
data-keyboard="true"
|
|
||||||
placeholder="${globalize.translate('Search')}"
|
|
||||||
autocomplete="off"
|
|
||||||
maxlength="40"
|
|
||||||
autofocus
|
|
||||||
/>`
|
|
||||||
});
|
|
||||||
|
|
||||||
const normalizeInput = (value = '') => value.trim();
|
import '../../styles/flexstyles.scss';
|
||||||
|
import './searchfields.scss';
|
||||||
|
|
||||||
type SearchFieldsProps = {
|
type SearchFieldsProps = {
|
||||||
query: string,
|
query: string,
|
||||||
onSearch?: (query: string) => void
|
onSearch?: (query: string) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
const SearchFields: FC<SearchFieldsProps> = ({
|
||||||
const SearchFields: FunctionComponent<SearchFieldsProps> = ({ onSearch = () => {}, query }: SearchFieldsProps) => {
|
onSearch = () => { /* no-op */ },
|
||||||
const element = useRef<HTMLDivElement>(null);
|
query
|
||||||
|
}: SearchFieldsProps) => {
|
||||||
const getSearchInput = () => element?.current?.querySelector<HTMLInputElement>('.searchfields-txtSearch');
|
|
||||||
|
|
||||||
const debouncedOnSearch = useMemo(() => debounce(onSearch, 400), [onSearch]);
|
|
||||||
|
|
||||||
const initSearchInput = getSearchInput();
|
|
||||||
if (initSearchInput) {
|
|
||||||
initSearchInput.value = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getSearchInput()?.addEventListener('input', e => {
|
|
||||||
debouncedOnSearch(normalizeInput((e.target as HTMLInputElement).value));
|
|
||||||
});
|
|
||||||
getSearchInput()?.focus();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
debouncedOnSearch.cancel();
|
|
||||||
};
|
|
||||||
}, [debouncedOnSearch]);
|
|
||||||
|
|
||||||
const onAlphaPicked = useCallback((e: Event) => {
|
const onAlphaPicked = useCallback((e: Event) => {
|
||||||
const value = (e as CustomEvent).detail.value;
|
const value = (e as CustomEvent).detail.value;
|
||||||
const searchInput = getSearchInput();
|
|
||||||
|
|
||||||
if (!searchInput) {
|
|
||||||
console.error('Unexpected null reference');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value === 'backspace') {
|
if (value === 'backspace') {
|
||||||
const currentValue = searchInput.value;
|
onSearch(query.length ? query.substring(0, query.length - 1) : '');
|
||||||
searchInput.value = currentValue.length ? currentValue.substring(0, currentValue.length - 1) : '';
|
|
||||||
} else {
|
} else {
|
||||||
searchInput.value += value;
|
onSearch(query + value);
|
||||||
}
|
}
|
||||||
|
}, [ onSearch, query ]);
|
||||||
|
|
||||||
searchInput.dispatchEvent(new CustomEvent('input', { bubbles: true }));
|
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
}, []);
|
onSearch(e.target.value);
|
||||||
|
}, [ onSearch ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='padded-left padded-right searchFields'>
|
||||||
className='padded-left padded-right searchFields'
|
|
||||||
ref={element}
|
|
||||||
>
|
|
||||||
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
<div className='searchFieldsInner flex align-items-center justify-content-center'>
|
||||||
<span className='searchfields-icon material-icons search' aria-hidden='true' />
|
<span className='searchfields-icon material-icons search' aria-hidden='true' />
|
||||||
<div
|
<div
|
||||||
className='inputContainer flex-grow'
|
className='inputContainer flex-grow'
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
dangerouslySetInnerHTML={createInputElement()}
|
>
|
||||||
/>
|
<Input
|
||||||
|
id='searchTextInput'
|
||||||
|
className='searchfields-txtSearch'
|
||||||
|
type='text'
|
||||||
|
data-keyboard='true'
|
||||||
|
placeholder={globalize.translate('Search')}
|
||||||
|
autoComplete='off'
|
||||||
|
maxLength={40}
|
||||||
|
// eslint-disable-next-line jsx-a11y/no-autofocus
|
||||||
|
autoFocus
|
||||||
|
value={query}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{layoutManager.tv && !browser.tv
|
{layoutManager.tv && !browser.tv
|
||||||
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
|
&& <AlphaPicker onAlphaPicked={onAlphaPicked} />
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
import type { BaseItemDto, BaseItemDtoQueryResult } from '@jellyfin/sdk/lib/generated-client';
|
||||||
import type { ApiClient } from 'jellyfin-apiclient';
|
import type { ApiClient } from 'jellyfin-apiclient';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react';
|
import React, { type FC, useCallback, useEffect, useState } from 'react';
|
||||||
import { CollectionType } from '@jellyfin/sdk/lib/generated-client/models/collection-type';
|
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 ServerConnections from '../ServerConnections';
|
||||||
@ -30,7 +31,7 @@ const isTVShows = (collectionType: string) => collectionType === CollectionType.
|
|||||||
/*
|
/*
|
||||||
* 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 non-live tv library search
|
||||||
*/
|
*/
|
||||||
const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
|
const SearchResults: FC<SearchResultsProps> = ({ serverId = window.ApiClient.serverId(), parentId, collectionType, query }: SearchResultsProps) => {
|
||||||
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
const [ movies, setMovies ] = useState<BaseItemDto[]>([]);
|
||||||
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
|
const [ shows, setShows ] = useState<BaseItemDto[]>([]);
|
||||||
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
const [ episodes, setEpisodes ] = useState<BaseItemDto[]>([]);
|
||||||
@ -47,11 +48,12 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
const [ books, setBooks ] = useState<BaseItemDto[]>([]);
|
||||||
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
const [ people, setPeople ] = useState<BaseItemDto[]>([]);
|
||||||
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
const [ collections, setCollections ] = useState<BaseItemDto[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
|
const [ debouncedQuery ] = useDebounceValue(query, 500);
|
||||||
|
|
||||||
const getDefaultParameters = useCallback(() => ({
|
const getDefaultParameters = useCallback(() => ({
|
||||||
ParentId: parentId,
|
ParentId: parentId,
|
||||||
searchTerm: query,
|
searchTerm: debouncedQuery,
|
||||||
Limit: 100,
|
Limit: 100,
|
||||||
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
Fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
|
||||||
Recursive: true,
|
Recursive: true,
|
||||||
@ -62,7 +64,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
IncludeGenres: false,
|
IncludeGenres: false,
|
||||||
IncludeStudios: false,
|
IncludeStudios: false,
|
||||||
IncludeArtists: false
|
IncludeArtists: false
|
||||||
}), [parentId, query]);
|
}), [ parentId, debouncedQuery ]);
|
||||||
|
|
||||||
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
|
const fetchArtists = useCallback((apiClient: ApiClient, params = {}) => (
|
||||||
apiClient?.getArtists(
|
apiClient?.getArtists(
|
||||||
@ -97,6 +99,10 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
).then(ensureNonNullItems)
|
).then(ensureNonNullItems)
|
||||||
), [getDefaultParameters]);
|
), [getDefaultParameters]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query) setIsLoading(true);
|
||||||
|
}, [ query ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Reset state
|
// Reset state
|
||||||
setMovies([]);
|
setMovies([]);
|
||||||
@ -116,13 +122,11 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
setPeople([]);
|
setPeople([]);
|
||||||
setCollections([]);
|
setCollections([]);
|
||||||
|
|
||||||
if (!query) {
|
if (!debouncedQuery) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true);
|
|
||||||
|
|
||||||
const apiClient = ServerConnections.getApiClient(serverId);
|
const apiClient = ServerConnections.getApiClient(serverId);
|
||||||
const fetchPromises = [];
|
const fetchPromises = [];
|
||||||
|
|
||||||
@ -230,7 +234,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
console.error('An error occurred while fetching data:', error);
|
console.error('An error occurred while fetching data:', error);
|
||||||
setIsLoading(false); // Set loading to false even if an error occurs
|
setIsLoading(false); // Set loading to false even if an error occurs
|
||||||
});
|
});
|
||||||
}, [collectionType, fetchArtists, fetchItems, fetchPeople, query, serverId]);
|
}, [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);
|
const allEmpty = [movies, shows, episodes, videos, programs, channels, playlists, artists, albums, songs, photoAlbums, photos, audioBooks, books, people, collections].every(arr => arr.length === 0);
|
||||||
|
|
||||||
@ -240,7 +244,7 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
'searchResults',
|
'searchResults',
|
||||||
'padded-bottom-page',
|
'padded-bottom-page',
|
||||||
'padded-top',
|
'padded-top',
|
||||||
{ 'hide': !query || collectionType === CollectionType.Livetv }
|
{ 'hide': !debouncedQuery || collectionType === CollectionType.Livetv }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -335,8 +339,8 @@ const SearchResults: FunctionComponent<SearchResultsProps> = ({ serverId = windo
|
|||||||
cardOptions={{ coverImage: true }}
|
cardOptions={{ coverImage: true }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{allEmpty && query && !isLoading && (
|
{allEmpty && debouncedQuery && !isLoading && (
|
||||||
<div className='sorry-text'>{globalize.translate('SearchResultsEmpty', query)}</div>
|
<div className='sorry-text'>{globalize.translate('SearchResultsEmpty', debouncedQuery)}</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
59
src/elements/emby-input/Input.tsx
Normal file
59
src/elements/emby-input/Input.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import React, { type DetailedHTMLProps, type InputHTMLAttributes, type FC, useState, useCallback } from 'react';
|
||||||
|
|
||||||
|
import './emby-input.scss';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
interface InputProps extends DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement> {
|
||||||
|
id: string,
|
||||||
|
label?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input: FC<InputProps> = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
onBlur,
|
||||||
|
onFocus,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const [ isFocused, setIsFocused ] = useState(false);
|
||||||
|
|
||||||
|
const onBlurInternal = useCallback(e => {
|
||||||
|
setIsFocused(false);
|
||||||
|
onBlur?.(e);
|
||||||
|
}, [ onBlur ]);
|
||||||
|
|
||||||
|
const onFocusInternal = useCallback(e => {
|
||||||
|
setIsFocused(true);
|
||||||
|
onFocus?.(e);
|
||||||
|
}, [ onFocus ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className={classNames(
|
||||||
|
'inputLabel',
|
||||||
|
{
|
||||||
|
inputLabelUnfocused: !isFocused,
|
||||||
|
inputLabelFocused: isFocused
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className={classNames(
|
||||||
|
'emby-input',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onBlur={onBlurInternal}
|
||||||
|
onFocus={onFocusInternal}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Input;
|
17
src/hooks/usePrevious.ts
Normal file
17
src/hooks/usePrevious.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns the previous value of a stateful value.
|
||||||
|
* @param value A stateful value created by a `useState` hook.
|
||||||
|
* @param initialValue The default value.
|
||||||
|
* @returns The previous value.
|
||||||
|
*/
|
||||||
|
export function usePrevious<T>(value: T, initialValue?: T): T | undefined {
|
||||||
|
const ref = useRef<T | undefined>(initialValue);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user