Convert Indexer settings to TypeScript

pull/7640/head
Mark McDowall 2 months ago
parent 27f81117ed
commit 6e008a8e85
No known key found for this signature in database

@ -14,7 +14,7 @@ import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSetting
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import MetadataSourceSettings from 'Settings/MetadataSource/MetadataSourceSettings';
@ -109,7 +109,7 @@ function AppRoutes() {
component={CustomFormatSettingsPage}
/>
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
<Route path="/settings/indexers" component={IndexerSettings} />
<Route
path="/settings/downloadclients"

@ -21,6 +21,7 @@ import Notification from 'typings/Notification';
import QualityDefinition from 'typings/QualityDefinition';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import IndexerOptions from 'typings/Settings/IndexerOptions';
import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
@ -28,6 +29,10 @@ import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
presets: T[];
};
export interface AutoTaggingAppState
extends AppSectionState<AutoTagging>,
AppSectionDeleteState,
@ -70,10 +75,15 @@ export interface ImportListAppState
AppSectionDeleteState,
AppSectionSaveState {}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
AppSectionSaveState,
AppSectionSchemaState<Presets<Indexer>> {
isTestingAll: boolean;
}
@ -134,6 +144,7 @@ interface SettingsAppState {
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
mediaManagement: MediaManagementAppState;

@ -1,5 +1,6 @@
import React, { FocusEvent, ReactNode } from 'react';
import Link from 'Components/Link/Link';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { inputTypes } from 'Helpers/Props';
import { InputType } from 'Helpers/Props/inputTypes';
import { Kind } from 'Helpers/Props/kinds';
@ -158,6 +159,7 @@ interface FormInputGroupProps<T> {
selectOptionsProviderAction?: string;
indexerFlags?: number;
pending?: boolean;
protocol?: DownloadProtocol;
canEdit?: boolean;
includeAny?: boolean;
delimiters?: string[];

@ -1,120 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import translate from 'Utilities/String/translate';
import IndexersConnector from './Indexers/IndexersConnector';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
class IndexerSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false,
isManageIndexersOpen: false
};
}
//
// Listeners
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
};
onChildStateChange = (payload) => {
this.setState(payload);
};
onManageIndexersPress = () => {
this.setState({ isManageIndexersOpen: true });
};
onManageIndexersModalClose = () => {
this.setState({ isManageIndexersOpen: false });
};
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
};
// Render
//
render() {
const {
isTestingAll,
dispatchTestAllIndexers
} = this.props;
const {
isSaving,
hasPendingChanges,
isManageIndexersOpen
} = this.state;
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllIndexers}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={this.onManageIndexersPress}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<IndexersConnector />
<IndexerOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersOpen}
onModalClose={this.onManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
IndexerSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllIndexers: PropTypes.func.isRequired
};
export default IndexerSettings;

@ -0,0 +1,104 @@
import React, { useCallback, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import SettingsToolbar from 'Settings/SettingsToolbar';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import {
SaveCallback,
SettingsStateChange,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
import Indexers from './Indexers/Indexers';
import ManageIndexersModal from './Indexers/Manage/ManageIndexersModal';
import IndexerOptions from './Options/IndexerOptions';
function IndexerSettings() {
const dispatch = useDispatch();
const isTestingAll = useSelector(
(state: AppState) => state.settings.indexers.isTestingAll
);
const saveOptions = useRef<() => void>();
const [isSaving, setIsSaving] = useState(false);
const [hasPendingChanges, setHasPendingChanges] = useState(false);
const [isManageIndexersModalOpen, setIsManageIndexersModalOpen] =
useState(false);
const handleSetChildSave = useCallback((saveCallback: SaveCallback) => {
saveOptions.current = saveCallback;
}, []);
const handleChildStateChange = useCallback(
({ isSaving, hasPendingChanges }: SettingsStateChange) => {
setIsSaving(isSaving);
setHasPendingChanges(hasPendingChanges);
},
[]
);
const handleManageIndexersPress = useCallback(() => {
setIsManageIndexersModalOpen(true);
}, []);
const handleManageIndexersModalClose = useCallback(() => {
setIsManageIndexersModalOpen(false);
}, []);
const handleSavePress = useCallback(() => {
saveOptions.current?.();
}, []);
const handleTestAllIndexersPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);
return (
<PageContent title={translate('IndexerSettings')}>
<SettingsToolbar
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllIndexers')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={handleTestAllIndexersPress}
/>
<PageToolbarButton
label={translate('ManageIndexers')}
iconName={icons.MANAGE}
onPress={handleManageIndexersPress}
/>
</>
}
onSavePress={handleSavePress}
/>
<PageContentBody>
<Indexers />
<IndexerOptions
setChildSave={handleSetChildSave}
onChildStateChange={handleChildStateChange}
/>
<ManageIndexersModal
isOpen={isManageIndexersModalOpen}
onModalClose={handleManageIndexersModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default IndexerSettings;

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { testAllIndexers } from 'Store/Actions/settingsActions';
import IndexerSettings from './IndexerSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers.isTestingAll,
(isTestingAll) => {
return {
isTestingAll
};
}
);
}
const mapDispatchToProps = {
dispatchTestAllIndexers: testAllIndexers
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSettings);

@ -1,113 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
class AddIndexerItem extends Component {
//
// Listeners
onIndexerSelect = () => {
const {
implementation,
implementationName
} = this.props;
this.props.onIndexerSelect({ implementation, implementationName });
};
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.indexer}
>
<Link
className={styles.underlay}
onPress={this.onIndexerSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onIndexerSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddIndexerItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onIndexerSelect: PropTypes.func.isRequired
};
export default AddIndexerItem;

@ -0,0 +1,88 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent';
import { sizes } from 'Helpers/Props';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
interface AddIndexerItemProps {
implementation: string;
implementationName: string;
infoLink: string;
presets?: Indexer[];
onIndexerSelect: () => void;
}
function AddIndexerItem({
implementation,
implementationName,
infoLink,
presets,
onIndexerSelect,
}: AddIndexerItemProps) {
const dispatch = useDispatch();
const hasPresets = !!presets && !!presets.length;
const handleIndexerSelect = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
})
);
onIndexerSelect();
}, [implementation, implementationName, dispatch, onIndexerSelect]);
return (
<div className={styles.indexer}>
<Link className={styles.underlay} onPress={handleIndexerSelect} />
<div className={styles.overlay}>
<div className={styles.name}>{implementationName}</div>
<div className={styles.actions}>
{hasPresets && (
<span>
<Button size={sizes.SMALL} onPress={handleIndexerSelect}>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button className={styles.presetsMenuButton} size={sizes.SMALL}>
{translate('Presets')}
</Button>
<MenuContent>
{presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerSelect}
/>
);
})}
</MenuContent>
</Menu>
</span>
)}
<Button to={infoLink} size={sizes.SMALL}>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
export default AddIndexerItem;

@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModal;

@ -0,0 +1,26 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContent from './AddIndexerModalContent';
interface AddIndexerModalProps {
isOpen: boolean;
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModal({
isOpen,
onIndexerSelect,
onModalClose,
}: AddIndexerModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<AddIndexerModalContent
onIndexerSelect={onIndexerSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default AddIndexerModal;

@ -1,122 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
class AddIndexerModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers,
onIndexerSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddIndexer')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('SupportedIndexers')}
</div>
<div>
{translate('SupportedIndexersMoreInfo')}
</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{
usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{
torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
usenetIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentIndexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModalContent;

@ -0,0 +1,116 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { kinds } from 'Helpers/Props';
import { fetchIndexerSchema } from 'Store/Actions/settingsActions';
import Indexer from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import AddIndexerItem from './AddIndexerItem';
import styles from './AddIndexerModalContent.css';
interface AddIndexerModalContentProps {
onIndexerSelect: () => void;
onModalClose: () => void;
}
function AddIndexerModalContent({
onIndexerSelect,
onModalClose,
}: AddIndexerModalContentProps) {
const dispatch = useDispatch();
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
useSelector((state: AppState) => state.settings.indexers);
const { usenetIndexers, torrentIndexers } = useMemo(() => {
return schema.reduce<{
usenetIndexers: Indexer[];
torrentIndexers: Indexer[];
}>(
(acc, item) => {
if (item.protocol === 'usenet') {
acc.usenetIndexers.push(item);
} else if (item.protocol === 'torrent') {
acc.torrentIndexers.push(item);
}
return acc;
},
{
usenetIndexers: [],
torrentIndexers: [],
}
);
}, [schema]);
useEffect(() => {
dispatch(fetchIndexerSchema());
}, [dispatch]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('AddIndexer')}</ModalHeader>
<ModalBody>
{isSchemaFetching ? <LoadingIndicator /> : null}
{!isSchemaFetching && !!schemaError ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{isSchemaPopulated && !schemaError ? (
<div>
<Alert kind={kinds.INFO}>
<div>{translate('SupportedIndexers')}</div>
<div>{translate('SupportedIndexersMoreInfo')}</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.indexers}>
{usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.indexers}>
{torrentIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
{...indexer}
implementation={indexer.implementation}
onIndexerSelect={onIndexerSelect}
/>
);
})}
</div>
</FieldSet>
</div>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default AddIndexerModalContent;

@ -1,75 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema } from 'Store/Actions/settingsActions';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(indexers) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = indexers;
const usenetIndexers = _.filter(schema, { protocol: 'usenet' });
const torrentIndexers = _.filter(schema, { protocol: 'torrent' });
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetIndexers,
torrentIndexers
};
}
);
}
const mapDispatchToProps = {
fetchIndexerSchema,
selectIndexerSchema
};
class AddIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerSchema();
}
//
// Listeners
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, presetName: name });
this.props.onModalClose({ indexerSelected: true });
};
//
// Render
render() {
return (
<AddIndexerModalContent
{...this.props}
onIndexerSelect={this.onIndexerSelect}
/>
);
}
}
AddIndexerModalContentConnector.propTypes = {
fetchIndexerSchema: PropTypes.func.isRequired,
selectIndexerSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);

@ -1,53 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation,
implementationName
});
};
//
// Render
render() {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerPresetMenuItem;

@ -0,0 +1,42 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import MenuItem, { MenuItemProps } from 'Components/Menu/MenuItem';
import { selectIndexerSchema } from 'Store/Actions/settingsActions';
interface AddIndexerPresetMenuItemProps
extends Omit<MenuItemProps, 'children'> {
name: string;
implementation: string;
implementationName: string;
onPress: () => void;
}
function AddIndexerPresetMenuItem({
name,
implementation,
implementationName,
onPress,
...otherProps
}: AddIndexerPresetMenuItemProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
dispatch(
selectIndexerSchema({
implementation,
implementationName,
presetName: name,
})
);
onPress();
}, [name, implementation, implementationName, dispatch, onPress]);
return (
<MenuItem {...otherProps} onPress={handlePress}>
{name}
</MenuItem>
);
}
export default AddIndexerPresetMenuItem;

@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerModal;

@ -0,0 +1,45 @@
import React, { useCallback } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
cancelSaveIndexer,
cancelTestIndexer,
} from 'Store/Actions/settingsActions';
import EditIndexerModalContent, {
EditIndexerModalContentProps,
} from './EditIndexerModalContent';
const section = 'settings.indexers';
interface EditIndexerModalProps extends EditIndexerModalContentProps {
isOpen: boolean;
}
function EditIndexerModal({
isOpen,
onModalClose,
...otherProps
}: EditIndexerModalProps) {
const dispatch = useDispatch();
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section }));
dispatch(cancelTestIndexer({ section }));
dispatch(cancelSaveIndexer({ section }));
onModalClose();
}, [dispatch, onModalClose]);
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditIndexerModalContent
{...otherProps}
onModalClose={handleModalClose}
/>
</Modal>
);
}
export default EditIndexerModal;

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/settingsActions';
import EditIndexerModal from './EditIndexerModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.indexers';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestIndexer() {
dispatch(cancelTestIndexer({ section }));
},
dispatchCancelSaveIndexer() {
dispatch(cancelSaveIndexer({ section }));
}
};
}
class EditIndexerModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestIndexer();
this.props.dispatchCancelSaveIndexer();
this.props.onModalClose();
};
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestIndexer,
dispatchCancelSaveIndexer,
...otherProps
} = this.props;
return (
<EditIndexerModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditIndexerModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestIndexer: PropTypes.func.isRequired,
dispatchCancelSaveIndexer: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector);

@ -1,269 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
function EditIndexerModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
seasonSearchMaximumSingleEpisodeAge,
protocol,
downloadClientId
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? translate('EditIndexerImplementation', { implementationName }) : translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('AddIndexerError')}
</Alert>
}
{
!isFetching && !error &&
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={supportsRss.value ? translate('EnableRssHelpText') : undefined}
helpTextWarning={supportsRss.value ? undefined : translate('RssIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={supportsSearch.value ? translate('EnableAutomaticSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={supportsSearch.value ? translate('EnableInteractiveSearchHelpText') : undefined}
helpTextWarning={supportsSearch.value ? undefined : translate('SearchIsNotSupportedWithThisIndexer')}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('MaximumSingleEpisodeAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="seasonSearchMaximumSingleEpisodeAge"
helpText={translate('MaximumSingleEpisodeAgeHelpText')}
min={0}
unit="days"
{...seasonSearchMaximumSingleEpisodeAge}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagSeriesHelpText')}
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
}
<AdvancedSettingsButton
showLabel={false}
/>
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};
export default EditIndexerModalContent;

@ -0,0 +1,317 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Alert from 'Components/Alert';
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 usePrevious from 'Helpers/Hooks/usePrevious';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import {
saveIndexer,
setIndexerFieldValue,
setIndexerValue,
testIndexer,
} from 'Store/Actions/settingsActions';
import { createProviderSettingsSelectorHook } from 'Store/Selectors/createProviderSettingsSelector';
import Indexer from 'typings/Indexer';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
export interface EditIndexerModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteIndexerPress?: () => void;
}
function EditIndexerModalContent({
id,
onModalClose,
onDeleteIndexerPress,
}: EditIndexerModalContentProps) {
const dispatch = useDispatch();
const showAdvancedSettings = useShowAdvancedSettings();
const {
isFetching,
error,
isSaving,
isTesting = false,
saveError,
item,
validationErrors,
validationWarnings,
} = useSelector(
createProviderSettingsSelectorHook<Indexer, IndexerAppState>('indexers', id)
);
const wasSaving = usePrevious(isSaving);
const {
implementationName = '',
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
supportsRss,
supportsSearch,
tags,
fields,
priority,
seasonSearchMaximumSingleEpisodeAge,
protocol,
downloadClientId,
} = item;
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerValue(change));
},
[dispatch]
);
const handleFieldChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions are not typed
dispatch(setIndexerFieldValue(change));
},
[dispatch]
);
const handleSavePress = useCallback(() => {
dispatch(saveIndexer({ id }));
}, [id, dispatch]);
const handleTestPress = useCallback(() => {
dispatch(testIndexer({ id }));
}, [id, dispatch]);
useEffect(() => {
if (!isSaving && wasSaving && !saveError) {
onModalClose();
}
}, [isSaving, wasSaving, saveError, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id
? translate('EditIndexerImplementation', { implementationName })
: translate('AddIndexerImplementation', { implementationName })}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>{translate('AddIndexerError')}</Alert>
) : null}
{!isFetching && !error ? (
<Form
validationErrors={validationErrors}
validationWarnings={validationWarnings}
>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableRss')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpText={
supportsRss.value ? translate('EnableRssHelpText') : undefined
}
helpTextWarning={
supportsRss.value
? undefined
: translate('RssIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableAutomaticSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableAutomaticSearch"
helpText={
supportsSearch.value
? translate('EnableAutomaticSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableAutomaticSearch}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableInteractiveSearch')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableInteractiveSearch"
helpText={
supportsSearch.value
? translate('EnableInteractiveSearchHelpText')
: undefined
}
helpTextWarning={
supportsSearch.value
? undefined
: translate('SearchIsNotSupportedWithThisIndexer')
}
isDisabled={!supportsSearch.value}
{...enableInteractiveSearch}
onChange={handleInputChange}
/>
</FormGroup>
{fields?.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={showAdvancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={handleFieldChange}
/>
);
})}
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('MaximumSingleEpisodeAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="seasonSearchMaximumSingleEpisodeAge"
helpText={translate('MaximumSingleEpisodeAgeHelpText')}
min={0}
unit="days"
{...seasonSearchMaximumSingleEpisodeAge}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup
advancedSettings={showAdvancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('DownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.DOWNLOAD_CLIENT_SELECT}
name="downloadClientId"
helpText={translate('IndexerDownloadClientHelpText')}
{...downloadClientId}
includeAny={true}
protocol={protocol.value}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText={translate('IndexerTagSeriesHelpText')}
{...tags}
onChange={handleInputChange}
/>
</FormGroup>
</Form>
) : null}
</ModalBody>
<ModalFooter>
{id ? (
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
) : null}
<AdvancedSettingsButton showLabel={false} />
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={handleTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={handleSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
export default EditIndexerModalContent;

@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/settingsActions';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createProviderSettingsSelector('indexers'),
(advancedSettings, indexer) => {
return {
advancedSettings,
...indexer
};
}
);
}
const mapDispatchToProps = {
setIndexerValue,
setIndexerFieldValue,
saveIndexer,
testIndexer
};
class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerValue({ name, value });
};
onFieldChange = ({ name, value }) => {
this.props.setIndexerFieldValue({ name, value });
};
onSavePress = () => {
this.props.saveIndexer({ id: this.props.id });
};
onTestPress = () => {
this.props.testIndexer({ id: this.props.id });
};
//
// Render
render() {
return (
<EditIndexerModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

@ -1,181 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import styles from './Indexer.css';
class Indexer extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: false
};
}
//
// Listeners
onEditIndexerPress = () => {
this.setState({ isEditIndexerModalOpen: true });
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
onDeleteIndexerPress = () => {
this.setState({
isEditIndexerModalOpen: false,
isDeleteIndexerModalOpen: true
});
};
onDeleteIndexerModalClose = () => {
this.setState({ isDeleteIndexerModalOpen: false });
};
onConfirmDeleteIndexer = () => {
this.props.onConfirmDeleteIndexer(this.props.id);
};
onCloneIndexerPress = () => {
const {
id,
onCloneIndexerPress
} = this.props;
onCloneIndexerPress(id);
};
//
// Render
render() {
const {
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
tagList,
supportsRss,
supportsSearch,
priority,
showPriority
} = this.props;
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>
{name}
</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={this.onCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{
supportsRss && enableRss &&
<Label kind={kinds.SUCCESS}>
{translate('Rss')}
</Label>
}
{
supportsSearch && enableAutomaticSearch &&
<Label kind={kinds.SUCCESS}>
{translate('AutomaticSearch')}
</Label>
}
{
supportsSearch && enableInteractiveSearch &&
<Label kind={kinds.SUCCESS}>
{translate('InteractiveSearch')}
</Label>
}
{
showPriority &&
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
}
{
!enableRss && !enableAutomaticSearch && !enableInteractiveSearch &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditIndexerModalConnector
id={id}
isOpen={this.state.isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteIndexerPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose}
/>
</Card>
);
}
}
Indexer.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
priority: PropTypes.number.isRequired,
enableRss: PropTypes.bool.isRequired,
enableAutomaticSearch: PropTypes.bool.isRequired,
enableInteractiveSearch: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
showPriority: PropTypes.bool.isRequired,
onCloneIndexerPress: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexer;

@ -0,0 +1,131 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Card from 'Components/Card';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { icons, kinds } from 'Helpers/Props';
import { deleteIndexer } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import IndexerModel from 'typings/Indexer';
import translate from 'Utilities/String/translate';
import EditIndexerModal from './EditIndexerModal';
import styles from './Indexer.css';
interface IndexerProps extends IndexerModel {
showPriority: boolean;
onCloneIndexerPress: (id: number) => void;
}
function Indexer({
id,
name,
enableRss,
enableAutomaticSearch,
enableInteractiveSearch,
tags,
supportsRss,
supportsSearch,
priority,
showPriority,
onCloneIndexerPress,
}: IndexerProps) {
const dispatch = useDispatch();
const tagList = useSelector(createTagsSelector());
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const handleEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
const handleDeleteIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(false);
setIsDeleteIndexerModalOpen(true);
}, []);
const handleDeleteIndexerModalClose = useCallback(() => {
setIsDeleteIndexerModalOpen(false);
}, []);
const handleConfirmDeleteIndexer = useCallback(() => {
dispatch(deleteIndexer({ id }));
}, [id, dispatch]);
const handleCloneIndexerPress = useCallback(() => {
onCloneIndexerPress(id);
}, [id, onCloneIndexerPress]);
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={handleEditIndexerPress}
>
<div className={styles.nameContainer}>
<div className={styles.name}>{name}</div>
<IconButton
className={styles.cloneButton}
title={translate('CloneIndexer')}
name={icons.CLONE}
onPress={handleCloneIndexerPress}
/>
</div>
<div className={styles.enabled}>
{supportsRss && enableRss ? (
<Label kind={kinds.SUCCESS}>{translate('Rss')}</Label>
) : null}
{supportsSearch && enableAutomaticSearch ? (
<Label kind={kinds.SUCCESS}>{translate('AutomaticSearch')}</Label>
) : null}
{supportsSearch && enableInteractiveSearch ? (
<Label kind={kinds.SUCCESS}>{translate('InteractiveSearch')}</Label>
) : null}
{showPriority ? (
<Label kind={kinds.DEFAULT}>
{translate('Priority')}: {priority}
</Label>
) : null}
{!enableRss && !enableAutomaticSearch && !enableInteractiveSearch ? (
<Label kind={kinds.DISABLED} outline={true}>
{translate('Disabled')}
</Label>
) : null}
</div>
<TagList tags={tags} tagList={tagList} />
<EditIndexerModal
id={id}
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
onDeleteIndexerPress={handleDeleteIndexerPress}
/>
<ConfirmModal
isOpen={isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexer')}
message={translate('DeleteIndexerMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={handleConfirmDeleteIndexer}
onCancel={handleDeleteIndexerModalClose}
/>
</Card>
);
}
export default Indexer;

@ -1,129 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModalConnector from './EditIndexerModalConnector';
import Indexer from './Indexer';
import styles from './Indexers.css';
class Indexers extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: false
};
}
//
// Listeners
onAddIndexerPress = () => {
this.setState({ isAddIndexerModalOpen: true });
};
onCloneIndexerPress = (id) => {
this.props.dispatchCloneIndexer({ id });
this.setState({ isEditIndexerModalOpen: true });
};
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
};
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
};
//
// Render
render() {
const {
items,
tagList,
dispatchCloneIndexer,
onConfirmDeleteIndexer,
...otherProps
} = this.props;
const {
isAddIndexerModalOpen,
isEditIndexerModalOpen
} = this.state;
const showPriority = items.some((index) => index.priority !== 25);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
{...otherProps}
>
<div className={styles.indexers}>
{
items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
tagList={tagList}
showPriority={showPriority}
onCloneIndexerPress={this.onCloneIndexerPress}
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
/>
);
})
}
<Card
className={styles.addIndexer}
onPress={this.onAddIndexerPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onModalClose={this.onAddIndexerModalClose}
/>
<EditIndexerModalConnector
isOpen={isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
Indexers.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexers;

@ -0,0 +1,105 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { IndexerAppState } from 'App/State/SettingsAppState';
import Card from 'Components/Card';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import { cloneIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import IndexerModel from 'typings/Indexer';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModal from './EditIndexerModal';
import Indexer from './Indexer';
import styles from './Indexers.css';
function Indexers() {
const dispatch = useDispatch();
const { isFetching, isPopulated, items, error } = useSelector(
createSortedSectionSelector<IndexerModel, IndexerAppState>(
'settings.indexers',
sortByProp('name')
)
);
const [isAddIndexerModalOpen, setIsAddIndexerModalOpen] = useState(false);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const showPriority = items.some((index) => index.priority !== 25);
const handleAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, []);
const handleCloneIndexerPress = useCallback(
(id: number) => {
dispatch(cloneIndexer({ id }));
setIsEditIndexerModalOpen(true);
},
[dispatch]
);
const handleIndexerSelect = useCallback(() => {
setIsAddIndexerModalOpen(false);
setIsEditIndexerModalOpen(true);
}, []);
const handleAddIndexerModalClose = useCallback(() => {
setIsAddIndexerModalOpen(false);
}, []);
const handleEditIndexerModalClose = useCallback(() => {
setIsEditIndexerModalOpen(false);
}, []);
useEffect(() => {
dispatch(fetchIndexers());
}, [dispatch]);
return (
<FieldSet legend={translate('Indexers')}>
<PageSectionContent
errorMessage={translate('IndexersLoadError')}
error={error}
isFetching={isFetching}
isPopulated={isPopulated}
>
<div className={styles.indexers}>
{items.map((item) => {
return (
<Indexer
key={item.id}
{...item}
showPriority={showPriority}
onCloneIndexerPress={handleCloneIndexerPress}
/>
);
})}
<Card className={styles.addIndexer} onPress={handleAddIndexerPress}>
<div className={styles.center}>
<Icon name={icons.ADD} size={45} />
</div>
</Card>
</div>
<AddIndexerModal
isOpen={isAddIndexerModalOpen}
onIndexerSelect={handleIndexerSelect}
onModalClose={handleAddIndexerModalClose}
/>
<EditIndexerModal
isOpen={isEditIndexerModalOpen}
onModalClose={handleEditIndexerModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
export default Indexers;

@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.indexers', sortByProp('name')),
createTagsSelector(),
(indexers, tagList) => {
return {
...indexers,
tagList
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers,
dispatchDeleteIndexer: deleteIndexer,
dispatchCloneIndexer: cloneIndexer
};
class IndexersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchIndexers();
}
//
// Listeners
onConfirmDeleteIndexer = (id) => {
this.props.dispatchDeleteIndexer({ id });
};
//
// Render
render() {
return (
<Indexers
{...this.props}
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
/>
);
}
}
IndexersConnector.propTypes = {
dispatchFetchIndexers: PropTypes.func.isRequired,
dispatchDeleteIndexer: PropTypes.func.isRequired,
dispatchCloneIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);

@ -1,116 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function IndexerOptions(props) {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
onInputChange
} = props;
return (
<FieldSet legend={translate('Options')}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={onInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={onInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={onInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/sonarr/faq#how-does-sonarr-find-episodes"
onChange={onInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
IndexerOptions.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default IndexerOptions;

@ -0,0 +1,152 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useShowAdvancedSettings from 'Helpers/Hooks/useShowAdvancedSettings';
import { inputTypes, kinds } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchIndexerOptions,
saveIndexerOptions,
setIndexerOptionsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { InputChanged } from 'typings/inputs';
import {
OnChildStateChange,
SetChildSave,
} from 'typings/Settings/SettingsState';
import translate from 'Utilities/String/translate';
const SECTION = 'indexerOptions';
interface IndexerOptionsProps {
setChildSave: SetChildSave;
onChildStateChange: OnChildStateChange;
}
function IndexerOptions({
setChildSave,
onChildStateChange,
}: IndexerOptionsProps) {
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
isSaving,
error,
settings,
hasSettings,
hasPendingChanges,
} = useSelector(createSettingsSectionSelector(SECTION));
const showAdvancedSettings = useShowAdvancedSettings();
const handleInputChange = useCallback(
(change: InputChanged) => {
// @ts-expect-error - actions aren't typed
dispatch(setIndexerOptionsValue(change));
},
[dispatch]
);
useEffect(() => {
dispatch(fetchIndexerOptions());
setChildSave(() => dispatch(saveIndexerOptions()));
}, [dispatch, setChildSave]);
useEffect(() => {
onChildStateChange({
isSaving,
hasPendingChanges,
});
}, [hasPendingChanges, isSaving, onChildStateChange]);
useEffect(() => {
return () => {
dispatch(clearPendingChanges({ section: `settings.${SECTION}` }));
};
}, [dispatch]);
return (
<FieldSet legend={translate('Options')}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('IndexerOptionsLoadError')}
</Alert>
) : null}
{hasSettings && isPopulated && !error ? (
<Form>
<FormGroup>
<FormLabel>{translate('MinimumAge')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText={translate('MinimumAgeHelpText')}
onChange={handleInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Retention')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText={translate('RetentionHelpText')}
onChange={handleInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MaximumSize')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText={translate('MaximumSizeHelpText')}
onChange={handleInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
<FormLabel>{translate('RssSyncInterval')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText={translate('RssSyncIntervalHelpText')}
helpTextWarning={translate('RssSyncIntervalHelpTextWarning')}
helpLink="https://wiki.servarr.com/sonarr/faq#how-does-sonarr-find-episodes"
onChange={handleInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
</Form>
) : null}
</FieldSet>
);
}
export default IndexerOptions;

@ -1,101 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchIndexerOptions, saveIndexerOptions, setIndexerOptionsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import IndexerOptions from './IndexerOptions';
const SECTION = 'indexerOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createSettingsSectionSelector(SECTION),
(advancedSettings, sectionSettings) => {
return {
advancedSettings,
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexerOptions: fetchIndexerOptions,
dispatchSetIndexerOptionsValue: setIndexerOptionsValue,
dispatchSaveIndexerOptions: saveIndexerOptions,
dispatchClearPendingChanges: clearPendingChanges
};
class IndexerOptionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
dispatchFetchIndexerOptions,
dispatchSaveIndexerOptions,
onChildMounted
} = this.props;
dispatchFetchIndexerOptions();
onChildMounted(dispatchSaveIndexerOptions);
}
componentDidUpdate(prevProps) {
const {
hasPendingChanges,
isSaving,
onChildStateChange
} = this.props;
if (
prevProps.isSaving !== isSaving ||
prevProps.hasPendingChanges !== hasPendingChanges
) {
onChildStateChange({
isSaving,
hasPendingChanges
});
}
}
componentWillUnmount() {
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value });
};
//
// Render
render() {
return (
<IndexerOptions
onInputChange={this.onInputChange}
{...this.props}
/>
);
}
}
IndexerOptionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);

@ -9,6 +9,7 @@ import AppState from 'App/State/AppState';
import selectSettings, {
ModelBaseSetting,
} from 'Store/Selectors/selectSettings';
import { PendingSection } from 'typings/pending';
import getSectionState from 'Utilities/State/getSectionState';
type SchemaState<T> = AppSectionSchemaState<T> | AppSectionItemSchemaState<T>;
@ -73,7 +74,7 @@ function selector<
isTesting,
...settings,
pendingChanges,
item: settings.settings,
item: settings.settings as PendingSection<T>,
};
}

@ -1,11 +1,16 @@
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import Provider from './Provider';
interface Indexer extends Provider {
enableRss: boolean;
enableAutomaticSearch: boolean;
enableInteractiveSearch: boolean;
protocol: string;
supportsRss: boolean;
supportsSearch: boolean;
seasonSearchMaximumSingleEpisodeAge: number;
protocol: DownloadProtocol;
priority: number;
downloadClientId: number;
tags: number[];
}

@ -0,0 +1,6 @@
export default interface IndexerOptions {
minimumAge: number;
retention: number;
maximumSize: number;
rssSyncInterval: number;
}
Loading…
Cancel
Save