mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-22 22:16:13 -04:00
Convert Page components to TypeScript
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import KeyboardShortcutsModalContentConnector from './KeyboardShortcutsModalContentConnector';
|
||||
|
||||
function KeyboardShortcutsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.SMALL}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<KeyboardShortcutsModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
KeyboardShortcutsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default KeyboardShortcutsModal;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import KeyboardShortcutsModalContent from './KeyboardShortcutsModalContent';
|
||||
|
||||
interface KeyboardShortcutsModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function KeyboardShortcutsModal(props: KeyboardShortcutsModalProps) {
|
||||
const { isOpen, onModalClose } = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.SMALL} onModalClose={onModalClose}>
|
||||
<KeyboardShortcutsModalContent onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyboardShortcutsModal;
|
||||
+24
-39
@@ -1,16 +1,17 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Shortcut, shortcuts } from 'Components/keyboardShortcuts';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './KeyboardShortcutsModalContent.css';
|
||||
|
||||
function getShortcuts() {
|
||||
const allShortcuts = [];
|
||||
const allShortcuts: Shortcut[] = [];
|
||||
|
||||
Object.keys(shortcuts).forEach((key) => {
|
||||
allShortcuts.push(shortcuts[key]);
|
||||
@@ -19,7 +20,7 @@ function getShortcuts() {
|
||||
return allShortcuts;
|
||||
}
|
||||
|
||||
function getShortcutKey(combo, isOsx) {
|
||||
function getShortcutKey(combo: string, isOsx: boolean) {
|
||||
const comboMatch = combo.match(/(.+?)\+(.)/);
|
||||
|
||||
if (!comboMatch) {
|
||||
@@ -37,55 +38,39 @@ function getShortcutKey(combo, isOsx) {
|
||||
return `${osModifier} + ${key}`;
|
||||
}
|
||||
|
||||
function KeyboardShortcutsModalContent(props) {
|
||||
const {
|
||||
isOsx,
|
||||
onModalClose
|
||||
} = props;
|
||||
interface KeyboardShortcutsModalContentProps {
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function KeyboardShortcutsModalContent({
|
||||
onModalClose,
|
||||
}: KeyboardShortcutsModalContentProps) {
|
||||
const { isOsx } = useSelector(createSystemStatusSelector());
|
||||
const allShortcuts = getShortcuts();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('KeyboardShortcuts')}
|
||||
</ModalHeader>
|
||||
<ModalHeader>{translate('KeyboardShortcuts')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
allShortcuts.map((shortcut) => {
|
||||
return (
|
||||
<div
|
||||
key={shortcut.name}
|
||||
className={styles.shortcut}
|
||||
>
|
||||
<div className={styles.key}>
|
||||
{getShortcutKey(shortcut.key, isOsx)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{shortcut.name}
|
||||
</div>
|
||||
{allShortcuts.map((shortcut) => {
|
||||
return (
|
||||
<div key={shortcut.name} className={styles.shortcut}>
|
||||
<div className={styles.key}>
|
||||
{getShortcutKey(shortcut.key, isOsx)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div>{shortcut.name}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
KeyboardShortcutsModalContent.propTypes = {
|
||||
isOsx: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default KeyboardShortcutsModalContent;
|
||||
@@ -1,17 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import KeyboardShortcutsModalContent from './KeyboardShortcutsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSystemStatusSelector(),
|
||||
(systemStatus) => {
|
||||
return {
|
||||
isOsx: systemStatus.isOsx
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(KeyboardShortcutsModalContent);
|
||||
@@ -1,106 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import SeriesSearchInputConnector from './SeriesSearchInputConnector';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
class PageHeader extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isKeyboardShortcutsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.onOpenKeyboardShortcutsModal);
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
onOpenKeyboardShortcutsModal = () => {
|
||||
this.setState({ isKeyboardShortcutsModalOpen: true });
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onKeyboardShortcutsModalClose = () => {
|
||||
this.setState({ isKeyboardShortcutsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onSidebarToggle
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link
|
||||
className={styles.logoLink}
|
||||
to={'/'}
|
||||
>
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Sonarr.urlBase}/Content/Images/logo.svg`}
|
||||
alt="Sonarr Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.sidebarToggleContainer}>
|
||||
<IconButton
|
||||
id="sidebar-toggle-button"
|
||||
name={icons.NAVBAR_COLLAPSE}
|
||||
onPress={onSidebarToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SeriesSearchInputConnector />
|
||||
|
||||
<div className={styles.right}>
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label={translate('Donate')}
|
||||
to="https://sonarr.tv/donate.html"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<KeyboardShortcutsModal
|
||||
isOpen={this.state.isKeyboardShortcutsModalOpen}
|
||||
onModalClose={this.onKeyboardShortcutsModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageHeader.propTypes = {
|
||||
onSidebarToggle: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(PageHeader);
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import KeyboardShortcutsModal from './KeyboardShortcutsModal';
|
||||
import PageHeaderActionsMenu from './PageHeaderActionsMenu';
|
||||
import SeriesSearchInput from './SeriesSearchInput';
|
||||
import styles from './PageHeader.css';
|
||||
|
||||
function PageHeader() {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isSidebarVisible } = useSelector((state: AppState) => state.app);
|
||||
|
||||
const [isKeyboardShortcutsModalOpen, setIsKeyboardShortcutsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
|
||||
const handleSidebarToggle = useCallback(() => {
|
||||
dispatch(setIsSidebarVisible({ isSidebarVisible: !isSidebarVisible }));
|
||||
}, [isSidebarVisible, dispatch]);
|
||||
|
||||
const handleOpenKeyboardShortcutsModal = useCallback(() => {
|
||||
setIsKeyboardShortcutsModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleKeyboardShortcutsModalClose = useCallback(() => {
|
||||
setIsKeyboardShortcutsModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut(
|
||||
'openKeyboardShortcutsModal',
|
||||
handleOpenKeyboardShortcutsModal
|
||||
);
|
||||
|
||||
return () => {
|
||||
unbindShortcut('openKeyboardShortcutsModal');
|
||||
};
|
||||
}, [handleOpenKeyboardShortcutsModal, bindShortcut, unbindShortcut]);
|
||||
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<div className={styles.logoContainer}>
|
||||
<Link className={styles.logoLink} to="/">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={`${window.Sonarr.urlBase}/Content/Images/logo.svg`}
|
||||
alt="Sonarr Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className={styles.sidebarToggleContainer}>
|
||||
<IconButton
|
||||
id="sidebar-toggle-button"
|
||||
name={icons.NAVBAR_COLLAPSE}
|
||||
onPress={handleSidebarToggle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SeriesSearchInput />
|
||||
|
||||
<div className={styles.right}>
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label={translate('Donate')}
|
||||
to="https://sonarr.tv/donate.html"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
|
||||
<PageHeaderActionsMenu
|
||||
onKeyboardShortcutsPress={handleOpenKeyboardShortcutsModal}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<KeyboardShortcutsModal
|
||||
isOpen={isKeyboardShortcutsModalOpen}
|
||||
onModalClose={handleKeyboardShortcutsModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageHeader;
|
||||
@@ -1,346 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import Icon from 'Components/Icon';
|
||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FuseWorker from './fuse.worker';
|
||||
import SeriesSearchResult from './SeriesSearchResult';
|
||||
import styles from './SeriesSearchInput.css';
|
||||
|
||||
const ADD_NEW_TYPE = 'addNew';
|
||||
|
||||
class SeriesSearchInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._autosuggest = null;
|
||||
this._worker = null;
|
||||
|
||||
this.state = {
|
||||
value: '',
|
||||
suggestions: []
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._worker) {
|
||||
this._worker.removeEventListener('message', this.onSuggestionsReceived, false);
|
||||
this._worker.terminate();
|
||||
this._worker = null;
|
||||
}
|
||||
}
|
||||
|
||||
getWorker() {
|
||||
if (!this._worker) {
|
||||
this._worker = new FuseWorker();
|
||||
this._worker.addEventListener('message', this.onSuggestionsReceived, false);
|
||||
}
|
||||
|
||||
return this._worker;
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setAutosuggestRef = (ref) => {
|
||||
this._autosuggest = ref;
|
||||
};
|
||||
|
||||
focusInput = (event) => {
|
||||
event.preventDefault();
|
||||
this._autosuggest.input.focus();
|
||||
};
|
||||
|
||||
getSectionSuggestions(section) {
|
||||
return section.suggestions;
|
||||
}
|
||||
|
||||
renderSectionTitle(section) {
|
||||
return (
|
||||
<div className={styles.sectionTitle}>
|
||||
{section.title}
|
||||
|
||||
{
|
||||
section.loading &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
rippleClassName={styles.ripple}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getSuggestionValue({ title }) {
|
||||
return title;
|
||||
}
|
||||
|
||||
renderSuggestion(item, { query }) {
|
||||
if (item.type === ADD_NEW_TYPE) {
|
||||
return (
|
||||
<div className={styles.addNewSeriesSuggestion}>
|
||||
{translate('SearchForQuery', { query })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SeriesSearchResult
|
||||
{...item.item}
|
||||
match={item.matches[0]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
goToSeries(item) {
|
||||
this.setState({ value: '' });
|
||||
this.props.onGoToSeries(item.item.titleSlug);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({
|
||||
value: '',
|
||||
suggestions: [],
|
||||
loading: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = (event, { newValue, method }) => {
|
||||
if (method === 'up' || method === 'down') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ value: newValue });
|
||||
};
|
||||
|
||||
onKeyDown = (event) => {
|
||||
if (event.shiftKey || event.altKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Tab' && event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
suggestions,
|
||||
value
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
highlightedSectionIndex,
|
||||
highlightedSuggestionIndex
|
||||
} = this._autosuggest.state;
|
||||
|
||||
if (!suggestions.length || highlightedSectionIndex) {
|
||||
this.props.onGoToAddNewSeries(value);
|
||||
this._autosuggest.input.blur();
|
||||
this.reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If an suggestion is not selected go to the first series,
|
||||
// otherwise go to the selected series.
|
||||
|
||||
if (highlightedSuggestionIndex == null) {
|
||||
this.goToSeries(suggestions[0]);
|
||||
} else {
|
||||
this.goToSeries(suggestions[highlightedSuggestionIndex]);
|
||||
}
|
||||
|
||||
this._autosuggest.input.blur();
|
||||
this.reset();
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.reset();
|
||||
};
|
||||
|
||||
onSuggestionsFetchRequested = ({ value }) => {
|
||||
if (!this.state.loading) {
|
||||
this.setState({
|
||||
loading: true
|
||||
});
|
||||
}
|
||||
|
||||
this.requestSuggestions(value);
|
||||
};
|
||||
|
||||
requestSuggestions = _.debounce((value) => {
|
||||
if (!this.state.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
const requestLoading = this.state.requestLoading;
|
||||
|
||||
this.setState({
|
||||
requestValue: value,
|
||||
requestLoading: true
|
||||
});
|
||||
|
||||
if (!requestLoading) {
|
||||
const payload = {
|
||||
value,
|
||||
series: this.props.series
|
||||
};
|
||||
|
||||
this.getWorker().postMessage(payload);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
onSuggestionsReceived = (message) => {
|
||||
const {
|
||||
value,
|
||||
suggestions
|
||||
} = message.data;
|
||||
|
||||
if (!this.state.loading) {
|
||||
this.setState({
|
||||
requestValue: null,
|
||||
requestLoading: false
|
||||
});
|
||||
} else if (value === this.state.requestValue) {
|
||||
this.setState({
|
||||
suggestions,
|
||||
requestValue: null,
|
||||
requestLoading: false,
|
||||
loading: false
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
suggestions,
|
||||
requestLoading: true
|
||||
});
|
||||
|
||||
const payload = {
|
||||
value: this.state.requestValue,
|
||||
series: this.props.series
|
||||
};
|
||||
|
||||
this.getWorker().postMessage(payload);
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.setState({
|
||||
suggestions: [],
|
||||
loading: false
|
||||
});
|
||||
};
|
||||
|
||||
onSuggestionSelected = (event, { suggestion }) => {
|
||||
if (suggestion.type === ADD_NEW_TYPE) {
|
||||
this.props.onGoToAddNewSeries(this.state.value);
|
||||
} else {
|
||||
this.goToSeries(suggestion);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value,
|
||||
loading,
|
||||
suggestions
|
||||
} = this.state;
|
||||
|
||||
const suggestionGroups = [];
|
||||
|
||||
if (suggestions.length || loading) {
|
||||
suggestionGroups.push({
|
||||
title: translate('ExistingSeries'),
|
||||
loading,
|
||||
suggestions
|
||||
});
|
||||
}
|
||||
|
||||
suggestionGroups.push({
|
||||
title: translate('AddNewSeries'),
|
||||
suggestions: [
|
||||
{
|
||||
type: ADD_NEW_TYPE,
|
||||
title: value
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const inputProps = {
|
||||
ref: this.setInputRef,
|
||||
className: styles.input,
|
||||
name: 'seriesSearch',
|
||||
value,
|
||||
placeholder: translate('Search'),
|
||||
autoComplete: 'off',
|
||||
spellCheck: false,
|
||||
onChange: this.onChange,
|
||||
onKeyDown: this.onKeyDown,
|
||||
onBlur: this.onBlur,
|
||||
onFocus: this.onFocus
|
||||
};
|
||||
|
||||
const theme = {
|
||||
container: styles.container,
|
||||
containerOpen: styles.containerOpen,
|
||||
suggestionsContainer: styles.seriesContainer,
|
||||
suggestionsList: styles.list,
|
||||
suggestion: styles.listItem,
|
||||
suggestionHighlighted: styles.highlighted
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
|
||||
<Autosuggest
|
||||
ref={this.setAutosuggestRef}
|
||||
id={name}
|
||||
inputProps={inputProps}
|
||||
theme={theme}
|
||||
focusInputOnSuggestionClick={false}
|
||||
multiSection={true}
|
||||
suggestions={suggestionGroups}
|
||||
getSectionSuggestions={this.getSectionSuggestions}
|
||||
renderSectionTitle={this.renderSectionTitle}
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SeriesSearchInput.propTypes = {
|
||||
series: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onGoToSeries: PropTypes.func.isRequired,
|
||||
onGoToAddNewSeries: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(SeriesSearchInput);
|
||||
@@ -0,0 +1,460 @@
|
||||
import { push } from 'connected-react-router';
|
||||
import { ExtendedKeyboardEvent } from 'mousetrap';
|
||||
import React, {
|
||||
FormEvent,
|
||||
KeyboardEvent,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
|
||||
import useKeyboardShortcuts from 'Helpers/Hooks/useKeyboardShortcuts';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SeriesSearchResult from './SeriesSearchResult';
|
||||
import styles from './SeriesSearchInput.css';
|
||||
|
||||
const ADD_NEW_TYPE = 'addNew';
|
||||
|
||||
interface Match {
|
||||
key: string;
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
interface AddNewSeriesSuggestion {
|
||||
type: 'addNew';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface SuggestedSeries
|
||||
extends Pick<
|
||||
Series,
|
||||
| 'title'
|
||||
| 'titleSlug'
|
||||
| 'sortTitle'
|
||||
| 'images'
|
||||
| 'alternateTitles'
|
||||
| 'tvdbId'
|
||||
| 'tvMazeId'
|
||||
| 'imdbId'
|
||||
| 'tmdbId'
|
||||
> {
|
||||
firstCharacter: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
interface SeriesSuggestion {
|
||||
title: string;
|
||||
indices: number[];
|
||||
item: SuggestedSeries;
|
||||
matches: Match[];
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
interface Section {
|
||||
title: string;
|
||||
loading?: boolean;
|
||||
suggestions: SeriesSuggestion[] | AddNewSeriesSuggestion[];
|
||||
}
|
||||
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createAllSeriesSelector(),
|
||||
createTagsSelector(),
|
||||
(allSeries, allTags) => {
|
||||
return allSeries.map((series): SuggestedSeries => {
|
||||
const {
|
||||
title,
|
||||
titleSlug,
|
||||
sortTitle,
|
||||
images,
|
||||
alternateTitles = [],
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags = [],
|
||||
} = series;
|
||||
|
||||
return {
|
||||
title,
|
||||
titleSlug,
|
||||
sortTitle,
|
||||
images,
|
||||
alternateTitles,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
firstCharacter: title.charAt(0).toLowerCase(),
|
||||
tags: tags.reduce<Tag[]>((acc, id) => {
|
||||
const matchingTag = allTags.find((tag) => tag.id === id);
|
||||
|
||||
if (matchingTag) {
|
||||
acc.push(matchingTag);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createSeriesSelector() {
|
||||
return createDeepEqualSelector(
|
||||
createUnoptimizedSelector(),
|
||||
(series) => series
|
||||
);
|
||||
}
|
||||
|
||||
function SeriesSearchInput() {
|
||||
const series = useSelector(createSeriesSelector());
|
||||
const dispatch = useDispatch();
|
||||
const { bindShortcut, unbindShortcut } = useKeyboardShortcuts();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
const [requestLoading, setRequestLoading] = useState(false);
|
||||
const [suggestions, setSuggestions] = useState<SeriesSuggestion[]>([]);
|
||||
|
||||
const autosuggestRef = useRef<Autosuggest>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const worker = useRef<Worker | null>(null);
|
||||
const isLoading = useRef(false);
|
||||
const requestValue = useRef<string | null>(null);
|
||||
|
||||
const suggestionGroups = useMemo(() => {
|
||||
const result: Section[] = [];
|
||||
|
||||
if (suggestions.length || isLoading.current) {
|
||||
result.push({
|
||||
title: translate('ExistingSeries'),
|
||||
loading: isLoading.current,
|
||||
suggestions,
|
||||
});
|
||||
}
|
||||
|
||||
result.push({
|
||||
title: translate('AddNewSeries'),
|
||||
suggestions: [
|
||||
{
|
||||
type: ADD_NEW_TYPE,
|
||||
title: value,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return result;
|
||||
}, [suggestions, value]);
|
||||
|
||||
const handleSuggestionsReceived = useCallback(
|
||||
(message: { data: { value: string; suggestions: SeriesSuggestion[] } }) => {
|
||||
const { value, suggestions } = message.data;
|
||||
|
||||
if (!isLoading.current) {
|
||||
requestValue.current = null;
|
||||
setRequestLoading(false);
|
||||
} else if (value === requestValue.current) {
|
||||
setSuggestions(suggestions);
|
||||
requestValue.current = null;
|
||||
setRequestLoading(false);
|
||||
isLoading.current = false;
|
||||
// setLoading(false);
|
||||
} else {
|
||||
setSuggestions(suggestions);
|
||||
setRequestLoading(true);
|
||||
|
||||
const payload = {
|
||||
value: requestValue,
|
||||
series,
|
||||
};
|
||||
|
||||
worker.current?.postMessage(payload);
|
||||
}
|
||||
},
|
||||
[series]
|
||||
);
|
||||
|
||||
const requestSuggestions = useDebouncedCallback((value: string) => {
|
||||
if (!isLoading.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestValue.current = value;
|
||||
setRequestLoading(true);
|
||||
|
||||
if (!requestLoading) {
|
||||
const payload = {
|
||||
value,
|
||||
series,
|
||||
};
|
||||
|
||||
worker.current?.postMessage(payload);
|
||||
}
|
||||
}, 250);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setValue('');
|
||||
setSuggestions([]);
|
||||
// setLoading(false);
|
||||
isLoading.current = false;
|
||||
}, []);
|
||||
|
||||
const focusInput = useCallback((event: ExtendedKeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
inputRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const getSectionSuggestions = useCallback((section: Section) => {
|
||||
return section.suggestions;
|
||||
}, []);
|
||||
|
||||
const renderSectionTitle = useCallback((section: Section) => {
|
||||
return (
|
||||
<div className={styles.sectionTitle}>
|
||||
{section.title}
|
||||
|
||||
{section.loading && (
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
rippleClassName={styles.ripple}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getSuggestionValue = useCallback(({ title }: { title: string }) => {
|
||||
return title;
|
||||
}, []);
|
||||
|
||||
const renderSuggestion = useCallback(
|
||||
(
|
||||
item: AddNewSeriesSuggestion | SeriesSuggestion,
|
||||
{ query }: { query: string }
|
||||
) => {
|
||||
if ('type' in item) {
|
||||
return (
|
||||
<div className={styles.addNewSeriesSuggestion}>
|
||||
{translate('SearchForQuery', { query })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <SeriesSearchResult {...item.item} match={item.matches[0]} />;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(
|
||||
_event: FormEvent<HTMLElement>,
|
||||
{
|
||||
newValue,
|
||||
method,
|
||||
}: {
|
||||
newValue: string;
|
||||
method: 'down' | 'up' | 'escape' | 'enter' | 'click' | 'type';
|
||||
}
|
||||
) => {
|
||||
if (method === 'up' || method === 'down') {
|
||||
return;
|
||||
}
|
||||
|
||||
setValue(newValue);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLElement>) => {
|
||||
if (event.shiftKey || event.altKey || event.ctrlKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key === 'Escape') {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== 'Tab' && event.key !== 'Enter') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!autosuggestRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { highlightedSectionIndex, highlightedSuggestionIndex } =
|
||||
autosuggestRef.current.state;
|
||||
|
||||
if (!suggestions.length || highlightedSectionIndex) {
|
||||
dispatch(
|
||||
push(
|
||||
`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(value)}`
|
||||
)
|
||||
);
|
||||
|
||||
inputRef.current?.blur();
|
||||
reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// If an suggestion is not selected go to the first series,
|
||||
// otherwise go to the selected series.
|
||||
|
||||
const selectedSuggestion =
|
||||
highlightedSuggestionIndex == null
|
||||
? suggestions[0]
|
||||
: suggestions[highlightedSuggestionIndex];
|
||||
|
||||
dispatch(
|
||||
push(
|
||||
`${window.Sonarr.urlBase}/series/${selectedSuggestion.item.titleSlug}`
|
||||
)
|
||||
);
|
||||
|
||||
inputRef.current?.blur();
|
||||
reset();
|
||||
},
|
||||
[value, suggestions, dispatch, reset]
|
||||
);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
reset();
|
||||
}, [reset]);
|
||||
|
||||
const handleSuggestionsFetchRequested = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
isLoading.current = true;
|
||||
|
||||
requestSuggestions(value);
|
||||
},
|
||||
[requestSuggestions]
|
||||
);
|
||||
|
||||
const handleSuggestionsClearRequested = useCallback(() => {
|
||||
setSuggestions([]);
|
||||
isLoading.current = false;
|
||||
}, []);
|
||||
|
||||
const handleSuggestionSelected = useCallback(
|
||||
(
|
||||
_event: SyntheticEvent,
|
||||
{ suggestion }: { suggestion: SeriesSuggestion | AddNewSeriesSuggestion }
|
||||
) => {
|
||||
if ('type' in suggestion) {
|
||||
dispatch(
|
||||
push(
|
||||
`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(value)}`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setValue('');
|
||||
dispatch(
|
||||
push(`${window.Sonarr.urlBase}/series/${suggestion.item.titleSlug}`)
|
||||
);
|
||||
}
|
||||
},
|
||||
[value, dispatch]
|
||||
);
|
||||
|
||||
const inputProps = {
|
||||
ref: inputRef,
|
||||
className: styles.input,
|
||||
name: 'seriesSearch',
|
||||
value,
|
||||
placeholder: translate('Search'),
|
||||
autoComplete: 'off',
|
||||
spellCheck: false,
|
||||
onChange: handleChange,
|
||||
onKeyDown: handleKeyDown,
|
||||
onBlur: handleBlur,
|
||||
};
|
||||
|
||||
const theme = {
|
||||
container: styles.container,
|
||||
containerOpen: styles.containerOpen,
|
||||
suggestionsContainer: styles.seriesContainer,
|
||||
suggestionsList: styles.list,
|
||||
suggestion: styles.listItem,
|
||||
suggestionHighlighted: styles.highlighted,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
worker.current = new Worker(new URL('./fuse.worker.ts', import.meta.url));
|
||||
|
||||
return () => {
|
||||
if (worker.current) {
|
||||
worker.current.terminate();
|
||||
worker.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
worker.current?.addEventListener(
|
||||
'message',
|
||||
handleSuggestionsReceived,
|
||||
false
|
||||
);
|
||||
|
||||
return () => {
|
||||
if (worker.current) {
|
||||
worker.current.removeEventListener(
|
||||
'message',
|
||||
handleSuggestionsReceived,
|
||||
false
|
||||
);
|
||||
}
|
||||
};
|
||||
}, [handleSuggestionsReceived]);
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('focusSeriesSearchInput', focusInput);
|
||||
|
||||
return () => {
|
||||
unbindShortcut('focusSeriesSearchInput');
|
||||
};
|
||||
}, [bindShortcut, unbindShortcut, focusInput]);
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
|
||||
<Autosuggest
|
||||
ref={autosuggestRef}
|
||||
inputProps={inputProps}
|
||||
theme={theme}
|
||||
focusInputOnSuggestionClick={false}
|
||||
multiSection={true}
|
||||
suggestions={suggestionGroups}
|
||||
getSectionSuggestions={getSectionSuggestions}
|
||||
renderSectionTitle={renderSectionTitle}
|
||||
getSuggestionValue={getSuggestionValue}
|
||||
renderSuggestion={renderSuggestion}
|
||||
onSuggestionSelected={handleSuggestionSelected}
|
||||
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesSearchInput;
|
||||
@@ -1,77 +0,0 @@
|
||||
import { push } from 'connected-react-router';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import SeriesSearchInput from './SeriesSearchInput';
|
||||
|
||||
function createCleanSeriesSelector() {
|
||||
return createSelector(
|
||||
createAllSeriesSelector(),
|
||||
createTagsSelector(),
|
||||
(allSeries, allTags) => {
|
||||
return allSeries.map((series) => {
|
||||
const {
|
||||
title,
|
||||
titleSlug,
|
||||
sortTitle,
|
||||
images,
|
||||
alternateTitles = [],
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags = []
|
||||
} = series;
|
||||
|
||||
return {
|
||||
title,
|
||||
titleSlug,
|
||||
sortTitle,
|
||||
images,
|
||||
alternateTitles,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
firstCharacter: title.charAt(0).toLowerCase(),
|
||||
tags: tags.reduce((acc, id) => {
|
||||
const matchingTag = allTags.find((tag) => tag.id === id);
|
||||
|
||||
if (matchingTag) {
|
||||
acc.push(matchingTag);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [])
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createDeepEqualSelector(
|
||||
createCleanSeriesSelector(),
|
||||
(series) => {
|
||||
return {
|
||||
series
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onGoToSeries(titleSlug) {
|
||||
dispatch(push(`${window.Sonarr.urlBase}/series/${titleSlug}`));
|
||||
},
|
||||
|
||||
onGoToAddNewSeries(query) {
|
||||
dispatch(push(`${window.Sonarr.urlBase}/add/new?term=${encodeURIComponent(query)}`));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(SeriesSearchInput);
|
||||
@@ -1,114 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import styles from './SeriesSearchResult.css';
|
||||
|
||||
function SeriesSearchResult(props) {
|
||||
const {
|
||||
match,
|
||||
title,
|
||||
images,
|
||||
alternateTitles,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags
|
||||
} = props;
|
||||
|
||||
let alternateTitle = null;
|
||||
let tag = null;
|
||||
|
||||
if (match.key === 'alternateTitles.title') {
|
||||
alternateTitle = alternateTitles[match.refIndex];
|
||||
} else if (match.key === 'tags.label') {
|
||||
tag = tags[match.refIndex];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.result}>
|
||||
<SeriesPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
|
||||
<div className={styles.titles}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{
|
||||
alternateTitle ?
|
||||
<div className={styles.alternateTitle}>
|
||||
{alternateTitle.title}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tvdbId' && tvdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TvdbId: {tvdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tvMazeId' && tvMazeId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TvMazeId: {tvMazeId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'imdbId' && imdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
ImdbId: {imdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
match.key === 'tmdbId' && tmdbId ?
|
||||
<div className={styles.alternateTitle}>
|
||||
TmdbId: {tmdbId}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
tag ?
|
||||
<div className={styles.tagContainer}>
|
||||
<Label
|
||||
key={tag.id}
|
||||
kind={kinds.INFO}
|
||||
>
|
||||
{tag.label}
|
||||
</Label>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SeriesSearchResult.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
tvdbId: PropTypes.number,
|
||||
tvMazeId: PropTypes.number,
|
||||
imdbId: PropTypes.string,
|
||||
tmdbId: PropTypes.number,
|
||||
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
match: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default SeriesSearchResult;
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import { SuggestedSeries } from './SeriesSearchInput';
|
||||
import styles from './SeriesSearchResult.css';
|
||||
|
||||
interface Match {
|
||||
key: string;
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
interface SeriesSearchResultProps extends SuggestedSeries {
|
||||
match: Match;
|
||||
}
|
||||
|
||||
function SeriesSearchResult(props: SeriesSearchResultProps) {
|
||||
const {
|
||||
match,
|
||||
title,
|
||||
images,
|
||||
alternateTitles,
|
||||
tvdbId,
|
||||
tvMazeId,
|
||||
imdbId,
|
||||
tmdbId,
|
||||
tags,
|
||||
} = props;
|
||||
|
||||
let alternateTitle = null;
|
||||
let tag: Tag | null = null;
|
||||
|
||||
if (match.key === 'alternateTitles.title') {
|
||||
alternateTitle = alternateTitles[match.refIndex];
|
||||
} else if (match.key === 'tags.label') {
|
||||
tag = tags[match.refIndex];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.result}>
|
||||
<SeriesPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
|
||||
<div className={styles.titles}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
|
||||
{alternateTitle ? (
|
||||
<div className={styles.alternateTitle}>{alternateTitle.title}</div>
|
||||
) : null}
|
||||
|
||||
{match.key === 'tvdbId' && tvdbId ? (
|
||||
<div className={styles.alternateTitle}>TvdbId: {tvdbId}</div>
|
||||
) : null}
|
||||
|
||||
{match.key === 'tvMazeId' && tvMazeId ? (
|
||||
<div className={styles.alternateTitle}>TvMazeId: {tvMazeId}</div>
|
||||
) : null}
|
||||
|
||||
{match.key === 'imdbId' && imdbId ? (
|
||||
<div className={styles.alternateTitle}>ImdbId: {imdbId}</div>
|
||||
) : null}
|
||||
|
||||
{match.key === 'tmdbId' && tmdbId ? (
|
||||
<div className={styles.alternateTitle}>TmdbId: {tmdbId}</div>
|
||||
) : null}
|
||||
|
||||
{tag ? (
|
||||
<div className={styles.tagContainer}>
|
||||
<Label key={tag.id} kind={kinds.INFO}>
|
||||
{tag.label}
|
||||
</Label>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesSearchResult;
|
||||
+13
-15
@@ -1,4 +1,7 @@
|
||||
// eslint-disable filenames/match-exported
|
||||
|
||||
import Fuse from 'fuse.js';
|
||||
import { SuggestedSeries } from './SeriesSearchInput';
|
||||
|
||||
const fuseOptions = {
|
||||
shouldSort: true,
|
||||
@@ -14,11 +17,11 @@ const fuseOptions = {
|
||||
'tvMazeId',
|
||||
'imdbId',
|
||||
'tmdbId',
|
||||
'tags.label'
|
||||
]
|
||||
'tags.label',
|
||||
],
|
||||
};
|
||||
|
||||
function getSuggestions(series, value) {
|
||||
function getSuggestions(series: SuggestedSeries[], value: string) {
|
||||
const limit = 10;
|
||||
let suggestions = [];
|
||||
|
||||
@@ -28,16 +31,14 @@ function getSuggestions(series, value) {
|
||||
if (s.firstCharacter === value.toLowerCase()) {
|
||||
suggestions.push({
|
||||
item: series[i],
|
||||
indices: [
|
||||
[0, 0]
|
||||
],
|
||||
indices: [[0, 0]],
|
||||
matches: [
|
||||
{
|
||||
value: s.title,
|
||||
key: 'title'
|
||||
}
|
||||
key: 'title',
|
||||
},
|
||||
],
|
||||
refIndex: 0
|
||||
refIndex: 0,
|
||||
});
|
||||
if (suggestions.length > limit) {
|
||||
break;
|
||||
@@ -52,21 +53,18 @@ function getSuggestions(series, value) {
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
onmessage = function(e) {
|
||||
onmessage = function (e) {
|
||||
if (!e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
series,
|
||||
value
|
||||
} = e.data;
|
||||
const { series, value } = e.data;
|
||||
|
||||
const suggestions = getSuggestions(series, value);
|
||||
|
||||
const results = {
|
||||
value,
|
||||
suggestions
|
||||
suggestions,
|
||||
};
|
||||
|
||||
self.postMessage(results);
|
||||
Reference in New Issue
Block a user