parent
9c196c5fa0
commit
37cc66ce66
@ -1,215 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
|
||||||
import styles from './AddNewSeries.css';
|
|
||||||
|
|
||||||
class AddNewSeries extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
term: props.term || '',
|
|
||||||
isFetching: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const term = this.state.term;
|
|
||||||
|
|
||||||
if (term) {
|
|
||||||
this.props.onSeriesLookupChange(term);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
term,
|
|
||||||
isFetching
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (term && term !== prevProps.term) {
|
|
||||||
this.setState({
|
|
||||||
term,
|
|
||||||
isFetching: true
|
|
||||||
});
|
|
||||||
this.props.onSeriesLookupChange(term);
|
|
||||||
} else if (isFetching !== prevProps.isFetching) {
|
|
||||||
this.setState({
|
|
||||||
isFetching
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSearchInputChange = ({ value }) => {
|
|
||||||
const hasValue = !!value.trim();
|
|
||||||
|
|
||||||
this.setState({ term: value, isFetching: hasValue }, () => {
|
|
||||||
if (hasValue) {
|
|
||||||
this.props.onSeriesLookupChange(value);
|
|
||||||
} else {
|
|
||||||
this.props.onClearSeriesLookup();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearSeriesLookupPress = () => {
|
|
||||||
this.setState({ term: '' });
|
|
||||||
this.props.onClearSeriesLookup();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
hasExistingSeries
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const term = this.state.term;
|
|
||||||
const isFetching = this.state.isFetching;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('AddNewSeries')}>
|
|
||||||
<PageContentBody>
|
|
||||||
<div className={styles.searchContainer}>
|
|
||||||
<div className={styles.searchIconContainer}>
|
|
||||||
<Icon
|
|
||||||
name={icons.SEARCH}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
className={styles.searchInput}
|
|
||||||
name="seriesLookup"
|
|
||||||
value={term}
|
|
||||||
placeholder="eg. Breaking Bad, tvdb:####"
|
|
||||||
autoFocus={true}
|
|
||||||
onChange={this.onSearchInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className={styles.clearLookupButton}
|
|
||||||
onPress={this.onClearSeriesLookupPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={icons.REMOVE}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error ?
|
|
||||||
<div className={styles.message}>
|
|
||||||
<div className={styles.helpText}>
|
|
||||||
{translate('AddNewSeriesError')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
|
||||||
</div> : null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error && !!items.length &&
|
|
||||||
<div className={styles.searchResults}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<AddNewSeriesSearchResultConnector
|
|
||||||
key={item.tvdbId}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !error && !items.length && !!term &&
|
|
||||||
<div className={styles.message}>
|
|
||||||
<div className={styles.noResults}>{translate('CouldNotFindResults', { term })}</div>
|
|
||||||
<div>{translate('SearchByTvdbId')}</div>
|
|
||||||
<div>
|
|
||||||
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
|
||||||
{translate('WhyCantIFindMyShow')}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
term ?
|
|
||||||
null :
|
|
||||||
<div className={styles.message}>
|
|
||||||
<div className={styles.helpText}>
|
|
||||||
{translate('AddNewSeriesHelpText')}
|
|
||||||
</div>
|
|
||||||
<div>{translate('SearchByTvdbId')}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!term && !hasExistingSeries ?
|
|
||||||
<div className={styles.message}>
|
|
||||||
<div className={styles.noSeriesText}>
|
|
||||||
{translate('NoSeriesHaveBeenAdded')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
to="/add/import"
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
>
|
|
||||||
{translate('ImportExistingSeries')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<div />
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeries.propTypes = {
|
|
||||||
term: PropTypes.string,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isAdding: PropTypes.bool.isRequired,
|
|
||||||
addError: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasExistingSeries: PropTypes.bool.isRequired,
|
|
||||||
onSeriesLookupChange: PropTypes.func.isRequired,
|
|
||||||
onClearSeriesLookup: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewSeries;
|
|
@ -0,0 +1,153 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AddSeries } from 'App/State/AddSeriesAppState';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||||
|
import useDebounce from 'Helpers/Hooks/useDebounce';
|
||||||
|
import useQueryParams from 'Helpers/Hooks/useQueryParams';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddNewSeriesSearchResult from './AddNewSeriesSearchResult';
|
||||||
|
import styles from './AddNewSeries.css';
|
||||||
|
|
||||||
|
function AddNewSeries() {
|
||||||
|
const { term: initialTerm = '' } = useQueryParams<{ term: string }>();
|
||||||
|
|
||||||
|
const seriesCount = useSelector(
|
||||||
|
(state: AppState) => state.series.items.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const [term, setTerm] = useState(initialTerm);
|
||||||
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
const query = useDebounce(term, term ? 300 : 0);
|
||||||
|
|
||||||
|
const handleSearchInputChange = useCallback(
|
||||||
|
({ value }: InputChanged<string>) => {
|
||||||
|
setTerm(value);
|
||||||
|
setIsFetching(!!value.trim());
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleClearSeriesLookupPress = useCallback(() => {
|
||||||
|
setTerm('');
|
||||||
|
setIsFetching(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching: isFetchingApi,
|
||||||
|
error,
|
||||||
|
data = [],
|
||||||
|
} = useApiQuery<AddSeries[]>({
|
||||||
|
path: `/series/lookup?term=${query}`,
|
||||||
|
queryOptions: {
|
||||||
|
enabled: !!query,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsFetching(isFetchingApi);
|
||||||
|
}, [isFetchingApi]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTerm(initialTerm);
|
||||||
|
}, [initialTerm]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('AddNewSeries')}>
|
||||||
|
<PageContentBody>
|
||||||
|
<div className={styles.searchContainer}>
|
||||||
|
<div className={styles.searchIconContainer}>
|
||||||
|
<Icon name={icons.SEARCH} size={20} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.searchInput}
|
||||||
|
name="seriesLookup"
|
||||||
|
value={term}
|
||||||
|
placeholder="eg. Breaking Bad, tvdb:####"
|
||||||
|
autoFocus={true}
|
||||||
|
onChange={handleSearchInputChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.clearLookupButton}
|
||||||
|
onPress={handleClearSeriesLookupPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.REMOVE} size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && !!error ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
{translate('AddNewSeriesError')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error && !!data.length ? (
|
||||||
|
<div className={styles.searchResults}>
|
||||||
|
{data.map((item) => {
|
||||||
|
return <AddNewSeriesSearchResult key={item.tvdbId} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isFetching && !error && !data.length && term ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.noResults}>
|
||||||
|
{translate('CouldNotFindResults', { term })}
|
||||||
|
</div>
|
||||||
|
<div>{translate('SearchByTvdbId')}</div>
|
||||||
|
<div>
|
||||||
|
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
||||||
|
{translate('WhyCantIFindMyShow')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{term ? null : (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
{translate('AddNewSeriesHelpText')}
|
||||||
|
</div>
|
||||||
|
<div>{translate('SearchByTvdbId')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!term && !seriesCount ? (
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.noSeriesText}>
|
||||||
|
{translate('NoSeriesHaveBeenAdded')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button to="/add/import" kind={kinds.PRIMARY}>
|
||||||
|
{translate('ImportExistingSeries')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div />
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewSeries;
|
@ -1,104 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearAddSeries, lookupSeries } from 'Store/Actions/addSeriesActions';
|
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import parseUrl from 'Utilities/String/parseUrl';
|
|
||||||
import AddNewSeries from './AddNewSeries';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.addSeries,
|
|
||||||
(state) => state.series.items.length,
|
|
||||||
(state) => state.router.location,
|
|
||||||
(addSeries, existingSeriesCount, location) => {
|
|
||||||
const { params } = parseUrl(location.search);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...addSeries,
|
|
||||||
term: params.term,
|
|
||||||
hasExistingSeries: existingSeriesCount > 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
lookupSeries,
|
|
||||||
clearAddSeries,
|
|
||||||
fetchRootFolders
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddNewSeriesConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._seriesLookupTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchRootFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
if (this._seriesLookupTimeout) {
|
|
||||||
clearTimeout(this._seriesLookupTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.clearAddSeries();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSeriesLookupChange = (term) => {
|
|
||||||
if (this._seriesLookupTimeout) {
|
|
||||||
clearTimeout(this._seriesLookupTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (term.trim() === '') {
|
|
||||||
this.props.clearAddSeries();
|
|
||||||
} else {
|
|
||||||
this._seriesLookupTimeout = setTimeout(() => {
|
|
||||||
this.props.lookupSeries({ term });
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearSeriesLookup = () => {
|
|
||||||
this.props.clearAddSeries();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
term,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AddNewSeries
|
|
||||||
term={term}
|
|
||||||
{...otherProps}
|
|
||||||
onSeriesLookupChange={this.onSeriesLookupChange}
|
|
||||||
onClearSeriesLookup={this.onClearSeriesLookup}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeriesConnector.propTypes = {
|
|
||||||
term: PropTypes.string,
|
|
||||||
lookupSeries: PropTypes.func.isRequired,
|
|
||||||
clearAddSeries: PropTypes.func.isRequired,
|
|
||||||
fetchRootFolders: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesConnector);
|
|
@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AddNewSeriesModalContentConnector from './AddNewSeriesModalContentConnector';
|
|
||||||
|
|
||||||
function AddNewSeriesModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AddNewSeriesModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeriesModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewSeriesModal;
|
|
@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AddNewSeriesModalContent, {
|
||||||
|
AddNewSeriesModalContentProps,
|
||||||
|
} from './AddNewSeriesModalContent';
|
||||||
|
|
||||||
|
interface AddNewSeriesModalProps extends AddNewSeriesModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewSeriesModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: AddNewSeriesModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<AddNewSeriesModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewSeriesModal;
|
@ -1,300 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
|
||||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
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 Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
|
||||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AddNewSeriesModalContent.css';
|
|
||||||
|
|
||||||
class AddNewSeriesModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
|
||||||
props.seriesType.value :
|
|
||||||
props.initialSeriesType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.seriesType.value !== prevProps.seriesType.value) {
|
|
||||||
this.setState({ seriesType: this.props.seriesType.value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQualityProfileIdChange = ({ value }) => {
|
|
||||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddSeriesPress = () => {
|
|
||||||
const {
|
|
||||||
seriesType
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
this.props.onAddSeriesPress(
|
|
||||||
seriesType
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
year,
|
|
||||||
overview,
|
|
||||||
images,
|
|
||||||
isAdding,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
seriesType,
|
|
||||||
seasonFolder,
|
|
||||||
searchForMissingEpisodes,
|
|
||||||
searchForCutoffUnmetEpisodes,
|
|
||||||
folder,
|
|
||||||
tags,
|
|
||||||
isSmallScreen,
|
|
||||||
isWindows,
|
|
||||||
onModalClose,
|
|
||||||
onInputChange,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{title}
|
|
||||||
|
|
||||||
{
|
|
||||||
!title.contains(year) && !!year &&
|
|
||||||
<span className={styles.year}>({year})</span>
|
|
||||||
}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.container}>
|
|
||||||
{
|
|
||||||
isSmallScreen ?
|
|
||||||
null :
|
|
||||||
<div className={styles.poster}>
|
|
||||||
<SeriesPoster
|
|
||||||
className={styles.poster}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
|
||||||
{
|
|
||||||
overview ?
|
|
||||||
<div className={styles.overview}>
|
|
||||||
{overview}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<Form {...otherProps}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
|
||||||
name="rootFolderPath"
|
|
||||||
valueOptions={{
|
|
||||||
seriesFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
selectedValueOptions={{
|
|
||||||
seriesFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
helpText={translate('AddNewSeriesRootFolderHelpText', { folder })}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...rootFolderPath}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Monitor')}
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
className={styles.labelIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('MonitoringOptions')}
|
|
||||||
body={<SeriesMonitoringOptionsPopoverContent />}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.MONITOR_EPISODES_SELECT}
|
|
||||||
name="monitor"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...monitor}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
onChange={this.onQualityProfileIdChange}
|
|
||||||
{...qualityProfileId}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('SeriesType')}
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
className={styles.labelIcon}
|
|
||||||
name={icons.INFO}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('SeriesTypes')}
|
|
||||||
body={<SeriesTypePopoverContent />}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SERIES_TYPE_SELECT}
|
|
||||||
name="seriesType"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...seriesType}
|
|
||||||
value={this.state.seriesType}
|
|
||||||
helpText={translate('SeriesTypesHelpText')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('SeasonFolder')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="seasonFolder"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...seasonFolder}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...tags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<div>
|
|
||||||
<label className={styles.searchLabelContainer}>
|
|
||||||
<span className={styles.searchLabel}>
|
|
||||||
{translate('AddNewSeriesSearchForMissingEpisodes')}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<CheckInput
|
|
||||||
containerClassName={styles.searchInputContainer}
|
|
||||||
className={styles.searchInput}
|
|
||||||
name="searchForMissingEpisodes"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...searchForMissingEpisodes}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className={styles.searchLabelContainer}>
|
|
||||||
<span className={styles.searchLabel}>
|
|
||||||
{translate('AddNewSeriesSearchForCutoffUnmetEpisodes')}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<CheckInput
|
|
||||||
containerClassName={styles.searchInputContainer}
|
|
||||||
className={styles.searchInput}
|
|
||||||
name="searchForCutoffUnmetEpisodes"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...searchForCutoffUnmetEpisodes}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.addButton}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
isSpinning={isAdding}
|
|
||||||
onPress={this.onAddSeriesPress}
|
|
||||||
>
|
|
||||||
{translate('AddSeriesWithTitle', { title })}
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeriesModalContent.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
year: PropTypes.number.isRequired,
|
|
||||||
overview: PropTypes.string,
|
|
||||||
initialSeriesType: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isAdding: PropTypes.bool.isRequired,
|
|
||||||
addError: PropTypes.object,
|
|
||||||
rootFolderPath: PropTypes.object,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
qualityProfileId: PropTypes.object,
|
|
||||||
seriesType: PropTypes.object.isRequired,
|
|
||||||
seasonFolder: PropTypes.object.isRequired,
|
|
||||||
searchForMissingEpisodes: PropTypes.object.isRequired,
|
|
||||||
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
|
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
isWindows: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onAddSeriesPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewSeriesModalContent;
|
|
@ -0,0 +1,301 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
|
import { AddSeries } from 'App/State/AddSeriesAppState';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
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 Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
|
import { addSeries, setAddSeriesDefault } from 'Store/Actions/addSeriesActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import useIsWindows from 'System/useIsWindows';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddNewSeriesModalContent.css';
|
||||||
|
|
||||||
|
export interface AddNewSeriesModalContentProps
|
||||||
|
extends Pick<
|
||||||
|
AddSeries,
|
||||||
|
'tvdbId' | 'title' | 'year' | 'overview' | 'images' | 'folder'
|
||||||
|
> {
|
||||||
|
initialSeriesType: string;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewSeriesModalContent({
|
||||||
|
tvdbId,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
folder,
|
||||||
|
initialSeriesType,
|
||||||
|
onModalClose,
|
||||||
|
}: AddNewSeriesModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isAdding, addError, defaults } = useSelector(
|
||||||
|
(state: AppState) => state.addSeries
|
||||||
|
);
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
const isWindows = useIsWindows();
|
||||||
|
|
||||||
|
const { settings, validationErrors, validationWarnings } = useMemo(() => {
|
||||||
|
return selectSettings(defaults, {}, addError);
|
||||||
|
}, [defaults, addError]);
|
||||||
|
|
||||||
|
const [seriesType, setSeriesType] = useState(
|
||||||
|
initialSeriesType === 'standard'
|
||||||
|
? settings.seriesType.value
|
||||||
|
: initialSeriesType
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
rootFolderPath,
|
||||||
|
searchForCutoffUnmetEpisodes,
|
||||||
|
searchForMissingEpisodes,
|
||||||
|
seasonFolder,
|
||||||
|
seriesType: seriesTypeSetting,
|
||||||
|
tags,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
dispatch(setAddSeriesDefault({ [name]: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleQualityProfileIdChange = useCallback(
|
||||||
|
({ value }: InputChanged<string | number>) => {
|
||||||
|
dispatch(setAddSeriesDefault({ qualityProfileId: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddSeriesPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
addSeries({
|
||||||
|
tvdbId,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
seriesType,
|
||||||
|
seasonFolder: seasonFolder.value,
|
||||||
|
searchForMissingEpisodes: searchForMissingEpisodes.value,
|
||||||
|
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
|
||||||
|
tags: tags.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
tvdbId,
|
||||||
|
seriesType,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
seasonFolder,
|
||||||
|
searchForMissingEpisodes,
|
||||||
|
searchForCutoffUnmetEpisodes,
|
||||||
|
tags,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSeriesType(seriesTypeSetting.value);
|
||||||
|
}, [seriesTypeSetting]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{!title.includes(String(year)) && year ? (
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
) : null}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<SeriesPoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
{overview ? (
|
||||||
|
<div className={styles.overview}>{overview}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Form
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
validationWarnings={validationWarnings}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
seriesFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
seriesFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
helpText={translate('AddNewSeriesRootFolderHelpText', {
|
||||||
|
folder,
|
||||||
|
})}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...rootFolderPath}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('Monitor')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon className={styles.labelIcon} name={icons.INFO} />
|
||||||
|
}
|
||||||
|
title={translate('MonitoringOptions')}
|
||||||
|
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...monitor}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
onChange={handleQualityProfileIdChange}
|
||||||
|
{...qualityProfileId}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('SeriesType')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon className={styles.labelIcon} name={icons.INFO} />
|
||||||
|
}
|
||||||
|
title={translate('SeriesTypes')}
|
||||||
|
body={<SeriesTypePopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SERIES_TYPE_SELECT}
|
||||||
|
name="seriesType"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...seriesTypeSetting}
|
||||||
|
value={seriesType}
|
||||||
|
helpText={translate('SeriesTypesHelpText')}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('SeasonFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="seasonFolder"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...seasonFolder}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...tags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<div>
|
||||||
|
<label className={styles.searchLabelContainer}>
|
||||||
|
<span className={styles.searchLabel}>
|
||||||
|
{translate('AddNewSeriesSearchForMissingEpisodes')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchInputContainer}
|
||||||
|
className={styles.searchInput}
|
||||||
|
name="searchForMissingEpisodes"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...searchForMissingEpisodes}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.searchLabelContainer}>
|
||||||
|
<span className={styles.searchLabel}>
|
||||||
|
{translate('AddNewSeriesSearchForCutoffUnmetEpisodes')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchInputContainer}
|
||||||
|
className={styles.searchInput}
|
||||||
|
name="searchForCutoffUnmetEpisodes"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...searchForCutoffUnmetEpisodes}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={handleAddSeriesPress}
|
||||||
|
>
|
||||||
|
{translate('AddSeriesWithTitle', { title })}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewSeriesModalContent;
|
@ -1,110 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { addSeries, setAddSeriesDefault } from 'Store/Actions/addSeriesActions';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.addSeries,
|
|
||||||
createDimensionsSelector(),
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(addSeriesState, dimensions, systemStatus) => {
|
|
||||||
const {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
defaults
|
|
||||||
} = addSeriesState;
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings
|
|
||||||
} = selectSettings(defaults, {}, addError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
isWindows: systemStatus.isWindows,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setAddSeriesDefault,
|
|
||||||
addSeries
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddNewSeriesModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setAddSeriesDefault({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddSeriesPress = (seriesType) => {
|
|
||||||
const {
|
|
||||||
tvdbId,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
seasonFolder,
|
|
||||||
searchForMissingEpisodes,
|
|
||||||
searchForCutoffUnmetEpisodes,
|
|
||||||
tags
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.addSeries({
|
|
||||||
tvdbId,
|
|
||||||
rootFolderPath: rootFolderPath.value,
|
|
||||||
monitor: monitor.value,
|
|
||||||
qualityProfileId: qualityProfileId.value,
|
|
||||||
seriesType,
|
|
||||||
seasonFolder: seasonFolder.value,
|
|
||||||
searchForMissingEpisodes: searchForMissingEpisodes.value,
|
|
||||||
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
|
|
||||||
tags: tags.value
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddNewSeriesModalContent
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onAddSeriesPress={this.onAddSeriesPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeriesModalContentConnector.propTypes = {
|
|
||||||
tvdbId: PropTypes.number.isRequired,
|
|
||||||
rootFolderPath: PropTypes.object,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
qualityProfileId: PropTypes.object,
|
|
||||||
seriesType: PropTypes.object.isRequired,
|
|
||||||
seasonFolder: PropTypes.object.isRequired,
|
|
||||||
searchForMissingEpisodes: PropTypes.object.isRequired,
|
|
||||||
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
setAddSeriesDefault: PropTypes.func.isRequired,
|
|
||||||
addSeries: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesModalContentConnector);
|
|
@ -1,276 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import HeartRating from 'Components/HeartRating';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import MetadataAttribution from 'Components/MetadataAttribution';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import SeriesGenres from 'Series/SeriesGenres';
|
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddNewSeriesModal from './AddNewSeriesModal';
|
|
||||||
import styles from './AddNewSeriesSearchResult.css';
|
|
||||||
|
|
||||||
class AddNewSeriesSearchResult extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isNewAddSeriesModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
|
||||||
this.onAddSeriesModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.setState({ isNewAddSeriesModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddSeriesModalClose = () => {
|
|
||||||
this.setState({ isNewAddSeriesModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTVDBLinkPress = (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
tvdbId,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
year,
|
|
||||||
network,
|
|
||||||
originalLanguage,
|
|
||||||
genres,
|
|
||||||
status,
|
|
||||||
overview,
|
|
||||||
statistics,
|
|
||||||
ratings,
|
|
||||||
folder,
|
|
||||||
seriesType,
|
|
||||||
images,
|
|
||||||
isExistingSeries,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const seasonCount = statistics.seasonCount;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isNewAddSeriesModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const linkProps = isExistingSeries ? { to: `/series/${titleSlug}` } : { onPress: this.onPress };
|
|
||||||
let seasons = translate('OneSeason');
|
|
||||||
|
|
||||||
if (seasonCount > 1) {
|
|
||||||
seasons = translate('CountSeasons', { count: seasonCount });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.searchResult}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
{...linkProps}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
{
|
|
||||||
isSmallScreen ?
|
|
||||||
null :
|
|
||||||
<SeriesPoster
|
|
||||||
className={styles.poster}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
overflow={true}
|
|
||||||
lazy={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.content}>
|
|
||||||
<div className={styles.titleRow}>
|
|
||||||
<div className={styles.titleContainer}>
|
|
||||||
<div className={styles.title}>
|
|
||||||
{title}
|
|
||||||
|
|
||||||
{
|
|
||||||
!title.contains(year) && year ?
|
|
||||||
<span className={styles.year}>
|
|
||||||
({year})
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.icons}>
|
|
||||||
{
|
|
||||||
isExistingSeries ?
|
|
||||||
<Icon
|
|
||||||
className={styles.alreadyExistsIcon}
|
|
||||||
name={icons.CHECK_CIRCLE}
|
|
||||||
size={36}
|
|
||||||
title={translate('AlreadyInYourLibrary')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={styles.tvdbLink}
|
|
||||||
to={`https://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
|
||||||
onPress={this.onTVDBLinkPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.tvdbLinkIcon}
|
|
||||||
name={icons.EXTERNAL_LINK}
|
|
||||||
size={28}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Label size={sizes.LARGE}>
|
|
||||||
<HeartRating
|
|
||||||
rating={ratings.value}
|
|
||||||
votes={ratings.votes}
|
|
||||||
iconSize={13}
|
|
||||||
/>
|
|
||||||
</Label>
|
|
||||||
|
|
||||||
{
|
|
||||||
originalLanguage?.name ?
|
|
||||||
<Label size={sizes.LARGE}>
|
|
||||||
<Icon
|
|
||||||
name={icons.LANGUAGE}
|
|
||||||
size={13}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.originalLanguageName}>
|
|
||||||
{originalLanguage.name}
|
|
||||||
</span>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
network ?
|
|
||||||
<Label size={sizes.LARGE}>
|
|
||||||
<Icon
|
|
||||||
name={icons.NETWORK}
|
|
||||||
size={13}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className={styles.network}>
|
|
||||||
{network}
|
|
||||||
</span>
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
genres.length > 0 ?
|
|
||||||
<Label size={sizes.LARGE}>
|
|
||||||
<Icon
|
|
||||||
name={icons.GENRE}
|
|
||||||
size={13}
|
|
||||||
/>
|
|
||||||
<SeriesGenres className={styles.genres} genres={genres} />
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
seasonCount ?
|
|
||||||
<Label size={sizes.LARGE}>
|
|
||||||
{seasons}
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
status === 'ended' ?
|
|
||||||
<Label
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('Ended')}
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
status === 'upcoming' ?
|
|
||||||
<Label
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{translate('Upcoming')}
|
|
||||||
</Label> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.overview}>
|
|
||||||
{overview}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MetadataAttribution />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddNewSeriesModal
|
|
||||||
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
|
||||||
tvdbId={tvdbId}
|
|
||||||
title={title}
|
|
||||||
year={year}
|
|
||||||
overview={overview}
|
|
||||||
folder={folder}
|
|
||||||
initialSeriesType={seriesType}
|
|
||||||
images={images}
|
|
||||||
onModalClose={this.onAddSeriesModalClose}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewSeriesSearchResult.propTypes = {
|
|
||||||
tvdbId: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
year: PropTypes.number.isRequired,
|
|
||||||
network: PropTypes.string,
|
|
||||||
originalLanguage: PropTypes.object,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
overview: PropTypes.string,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
|
||||||
ratings: PropTypes.object.isRequired,
|
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
seriesType: PropTypes.string.isRequired,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isExistingSeries: PropTypes.bool.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AddNewSeriesSearchResult.defaultProps = {
|
|
||||||
genres: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewSeriesSearchResult;
|
|
@ -0,0 +1,184 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AddSeries } from 'App/State/AddSeriesAppState';
|
||||||
|
import HeartRating from 'Components/HeartRating';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import MetadataAttribution from 'Components/MetadataAttribution';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { Statistics } from 'Series/Series';
|
||||||
|
import SeriesGenres from 'Series/SeriesGenres';
|
||||||
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AddNewSeriesModal from './AddNewSeriesModal';
|
||||||
|
import styles from './AddNewSeriesSearchResult.css';
|
||||||
|
|
||||||
|
type AddNewSeriesSearchResultProps = AddSeries;
|
||||||
|
|
||||||
|
function AddNewSeriesSearchResult({
|
||||||
|
tvdbId,
|
||||||
|
titleSlug,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
network,
|
||||||
|
originalLanguage,
|
||||||
|
genres = [],
|
||||||
|
status,
|
||||||
|
statistics = {} as Statistics,
|
||||||
|
ratings,
|
||||||
|
folder,
|
||||||
|
overview,
|
||||||
|
seriesType,
|
||||||
|
images,
|
||||||
|
}: AddNewSeriesSearchResultProps) {
|
||||||
|
const isExistingSeries = useSelector(createExistingSeriesSelector(tvdbId));
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
const [isNewAddSeriesModalOpen, setIsNewAddSeriesModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const seasonCount = statistics.seasonCount;
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
setIsNewAddSeriesModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleAddSeriesModalClose = useCallback(() => {
|
||||||
|
setIsNewAddSeriesModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTvdbLinkPress = useCallback((event: React.SyntheticEvent) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const linkProps = isExistingSeries
|
||||||
|
? { to: `/series/${titleSlug}` }
|
||||||
|
: { onPress: handlePress };
|
||||||
|
let seasons = translate('OneSeason');
|
||||||
|
|
||||||
|
if (seasonCount > 1) {
|
||||||
|
seasons = translate('CountSeasons', { count: seasonCount });
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.searchResult}>
|
||||||
|
<Link className={styles.underlay} {...linkProps} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<SeriesPoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
overflow={true}
|
||||||
|
lazy={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{!title.includes(String(year)) && year ? (
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.icons}>
|
||||||
|
{isExistingSeries ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.alreadyExistsIcon}
|
||||||
|
name={icons.CHECK_CIRCLE}
|
||||||
|
size={36}
|
||||||
|
title={translate('AlreadyInYourLibrary')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.tvdbLink}
|
||||||
|
to={`https://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||||
|
onPress={handleTvdbLinkPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.tvdbLinkIcon}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<HeartRating
|
||||||
|
rating={ratings.value}
|
||||||
|
votes={ratings.votes}
|
||||||
|
iconSize={13}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{originalLanguage?.name ? (
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon name={icons.LANGUAGE} size={13} />
|
||||||
|
|
||||||
|
<span className={styles.originalLanguageName}>
|
||||||
|
{originalLanguage.name}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{network ? (
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon name={icons.NETWORK} size={13} />
|
||||||
|
|
||||||
|
<span className={styles.network}>{network}</span>
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{genres.length > 0 ? (
|
||||||
|
<Label size={sizes.LARGE}>
|
||||||
|
<Icon name={icons.GENRE} size={13} />
|
||||||
|
<SeriesGenres className={styles.genres} genres={genres} />
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{seasonCount ? <Label size={sizes.LARGE}>{seasons}</Label> : null}
|
||||||
|
|
||||||
|
{status === 'ended' ? (
|
||||||
|
<Label kind={kinds.DANGER} size={sizes.LARGE}>
|
||||||
|
{translate('Ended')}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{status === 'upcoming' ? (
|
||||||
|
<Label kind={kinds.INFO} size={sizes.LARGE}>
|
||||||
|
{translate('Upcoming')}
|
||||||
|
</Label>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.overview}>{overview}</div>
|
||||||
|
|
||||||
|
<MetadataAttribution />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddNewSeriesModal
|
||||||
|
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
||||||
|
tvdbId={tvdbId}
|
||||||
|
title={title}
|
||||||
|
year={year}
|
||||||
|
overview={overview}
|
||||||
|
folder={folder}
|
||||||
|
initialSeriesType={seriesType}
|
||||||
|
images={images}
|
||||||
|
onModalClose={handleAddSeriesModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewSeriesSearchResult;
|
@ -1,20 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
|
||||||
import AddNewSeriesSearchResult from './AddNewSeriesSearchResult';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createExistingSeriesSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(isExistingSeries, dimensions) => {
|
|
||||||
return {
|
|
||||||
isExistingSeries,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(AddNewSeriesSearchResult);
|
|
@ -0,0 +1,27 @@
|
|||||||
|
import AppSectionState, { Error } from 'App/State/AppSectionState';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import Series, { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||||
|
|
||||||
|
export interface AddSeries extends Series {
|
||||||
|
folder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddSeriesAppState extends AppSectionState<AddSeries> {
|
||||||
|
isAdding: boolean;
|
||||||
|
isAdded: boolean;
|
||||||
|
addError: Error | undefined;
|
||||||
|
|
||||||
|
defaults: {
|
||||||
|
rootFolderPath: string;
|
||||||
|
monitor: SeriesMonitor;
|
||||||
|
qualityProfileId: number;
|
||||||
|
seriesType: SeriesType;
|
||||||
|
seasonFolder: boolean;
|
||||||
|
language: Language;
|
||||||
|
tags: number[];
|
||||||
|
searchForMissingEpisodes: boolean;
|
||||||
|
searchForCutoffUnmetEpisodes: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddSeriesAppState;
|
@ -0,0 +1,26 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
function useDebounce<T>(value: T, delay: number) {
|
||||||
|
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||||
|
const valueTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (delay === 0) {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
clearTimeout(valueTimeout.current);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
valueTimeout.current = setTimeout(() => {
|
||||||
|
setDebouncedValue(value);
|
||||||
|
}, delay);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(valueTimeout.current);
|
||||||
|
};
|
||||||
|
}, [value, delay]);
|
||||||
|
|
||||||
|
return debouncedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDebounce;
|
@ -0,0 +1,19 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router';
|
||||||
|
|
||||||
|
function useQueryParams<T>() {
|
||||||
|
const { search } = useLocation();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const searchParams = new URLSearchParams(search);
|
||||||
|
|
||||||
|
return searchParams.entries().reduce<T>((acc, [key, value]) => {
|
||||||
|
return {
|
||||||
|
...acc,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
|
}, {} as T);
|
||||||
|
}, [search]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useQueryParams;
|
@ -1,16 +1,11 @@
|
|||||||
import { some } from 'lodash';
|
import { some } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import createAllSeriesSelector from './createAllSeriesSelector';
|
import createAllSeriesSelector from './createAllSeriesSelector';
|
||||||
|
|
||||||
function createExistingSeriesSelector() {
|
function createExistingSeriesSelector(tvdbId: number) {
|
||||||
return createSelector(
|
return createSelector(createAllSeriesSelector(), (series) => {
|
||||||
(_: AppState, { tvdbId }: { tvdbId: number }) => tvdbId,
|
return some(series, { tvdbId });
|
||||||
createAllSeriesSelector(),
|
});
|
||||||
(tvdbId, series) => {
|
|
||||||
return some(series, { tvdbId });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createExistingSeriesSelector;
|
export default createExistingSeriesSelector;
|
||||||
|
Loading…
Reference in new issue