mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Convert Indexer settings to TypeScript
(cherry picked from commit 6e008a8e855e67bb14b0e04bdb9042eebcacb59f)
This commit is contained in:
@@ -16,7 +16,7 @@ import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSetting
|
|||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
@@ -113,7 +113,7 @@ function AppRoutes() {
|
|||||||
component={CustomFormatSettingsPage}
|
component={CustomFormatSettingsPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/downloadclients"
|
path="/settings/downloadclients"
|
||||||
|
|||||||
@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
|
|||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
schemaError: Error;
|
schemaError: Error;
|
||||||
schema: {
|
schema: T[];
|
||||||
items: T[];
|
selectedSchema?: T;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface AppSectionItemSchemaState<T> {
|
||||||
|
isSchemaFetching: boolean;
|
||||||
|
isSchemaPopulated: boolean;
|
||||||
|
schemaError: Error;
|
||||||
|
schema: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionItemState<T> {
|
export interface AppSectionItemState<T> {
|
||||||
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
|
|||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
isTesting?: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
items: T[];
|
items: T[];
|
||||||
pendingChanges: Partial<T>;
|
pendingChanges?: Partial<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionItemSchemaState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
@@ -16,12 +17,17 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import General from 'typings/Settings/General';
|
||||||
|
import IndexerOptions from 'typings/Settings/IndexerOptions';
|
||||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
import NamingExample from 'typings/Settings/NamingExample';
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
import MetadataAppState from './MetadataAppState';
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
|
type Presets<T> = T & {
|
||||||
|
presets: T[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -44,10 +50,15 @@ export interface ImportListAppState
|
|||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface IndexerOptionsAppState
|
||||||
|
extends AppSectionItemState<IndexerOptions>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface IndexerAppState
|
export interface IndexerAppState
|
||||||
extends AppSectionState<Indexer>,
|
extends AppSectionState<Indexer>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState,
|
||||||
|
AppSectionSchemaState<Presets<Indexer>> {
|
||||||
isTestingAll: boolean;
|
isTestingAll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +68,7 @@ export interface NotificationAppState
|
|||||||
|
|
||||||
export interface QualityProfilesAppState
|
export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionItemSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
export interface ReleaseProfilesAppState
|
export interface ReleaseProfilesAppState
|
||||||
extends AppSectionState<ReleaseProfile>,
|
extends AppSectionState<ReleaseProfile>,
|
||||||
@@ -95,6 +106,7 @@ interface SettingsAppState {
|
|||||||
importListOptions: ImportListOptionsSettingsAppState;
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
|
indexerOptions: IndexerOptionsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
metadata: MetadataAppState;
|
metadata: MetadataAppState;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
|
|||||||
includeMixed: boolean
|
includeMixed: boolean
|
||||||
) {
|
) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector(
|
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
|
||||||
'settings.qualityProfiles',
|
'settings.qualityProfiles',
|
||||||
sortByProp<QualityProfile, 'name'>('name')
|
sortByProp<QualityProfile, 'name'>('name')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function useShowAdvancedSettings() {
|
||||||
|
return useSelector((state: AppState) => state.settings.advancedSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useShowAdvancedSettings;
|
||||||
@@ -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,251 +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,
|
|
||||||
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('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('IndexerTagMovieHelpText')}
|
|
||||||
{...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,299 @@
|
|||||||
|
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,
|
||||||
|
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('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('IndexerTagMovieHelpText')}
|
||||||
|
{...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,
|
|
||||||
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,
|
|
||||||
onCloneIndexerPress: PropTypes.func.isRequired,
|
|
||||||
onConfirmDeleteIndexer: PropTypes.func.isRequired,
|
|
||||||
priority: PropTypes.number.isRequired,
|
|
||||||
showPriority: PropTypes.bool.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,174 +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,
|
|
||||||
onWhitelistedSubtitleChange
|
|
||||||
} = 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>
|
|
||||||
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="preferIndexerFlags"
|
|
||||||
helpText={translate('PreferIndexerFlagsHelpText')}
|
|
||||||
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.preferIndexerFlags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.NUMBER}
|
|
||||||
name="availabilityDelay"
|
|
||||||
unit="days"
|
|
||||||
helpText={translate('AvailabilityDelayHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.availabilityDelay}
|
|
||||||
/>
|
|
||||||
</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/radarr/faq#how-does-radarr-work"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.rssSyncInterval}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
>
|
|
||||||
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT_TAG}
|
|
||||||
name="whitelistedHardcodedSubs"
|
|
||||||
helpText={translate('WhitelistedHardcodedSubsHelpText')}
|
|
||||||
onChange={onWhitelistedSubtitleChange}
|
|
||||||
{...settings.whitelistedHardcodedSubs}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
isAdvanced={true}
|
|
||||||
>
|
|
||||||
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="allowHardcodedSubs"
|
|
||||||
helpText={translate('AllowHardcodedSubsHelpText')}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...settings.allowHardcodedSubs}
|
|
||||||
/>
|
|
||||||
</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,
|
|
||||||
onWhitelistedSubtitleChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default IndexerOptions;
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
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]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleWhitelistedSubtitleChange = useCallback(
|
||||||
|
({ name, value }: InputChanged<string[] | null>) => {
|
||||||
|
// @ts-expect-error - actions aren't typed
|
||||||
|
dispatch(setIndexerOptionsValue({ name, value: value?.join(',') }));
|
||||||
|
},
|
||||||
|
[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>
|
||||||
|
<FormLabel>{translate('PreferIndexerFlags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="preferIndexerFlags"
|
||||||
|
helpText={translate('PreferIndexerFlagsHelpText')}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/settings#indexer-flags"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.preferIndexerFlags}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('AvailabilityDelay')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="availabilityDelay"
|
||||||
|
unit="days"
|
||||||
|
helpText={translate('AvailabilityDelayHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.availabilityDelay}
|
||||||
|
/>
|
||||||
|
</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/radarr/faq#how-does-radarr-work"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.rssSyncInterval}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||||
|
<FormLabel>{translate('WhitelistedSubtitleTags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT_TAG}
|
||||||
|
name="whitelistedHardcodedSubs"
|
||||||
|
helpText={translate('WhitelistedHardcodedSubsHelpText')}
|
||||||
|
onChange={handleWhitelistedSubtitleChange}
|
||||||
|
{...settings.whitelistedHardcodedSubs}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup advancedSettings={showAdvancedSettings} isAdvanced={true}>
|
||||||
|
<FormLabel>{translate('AllowHardcodedSubs')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="allowHardcodedSubs"
|
||||||
|
helpText={translate('AllowHardcodedSubsHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
{...settings.allowHardcodedSubs}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerOptions;
|
||||||
@@ -1,106 +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 });
|
|
||||||
};
|
|
||||||
|
|
||||||
onWhitelistedSubtitleChange = ({ name, value }) => {
|
|
||||||
this.props.dispatchSetIndexerOptionsValue({ name, value: value.join(',') });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<IndexerOptions
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onWhitelistedSubtitleChange={this.onWhitelistedSubtitleChange}
|
|
||||||
{...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);
|
|
||||||
@@ -14,7 +14,7 @@ import styles from './Metadatas.css';
|
|||||||
|
|
||||||
function createMetadatasSelector() {
|
function createMetadatasSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector<MetadataType>(
|
createSortedSectionSelector<MetadataType, MetadataAppState>(
|
||||||
'settings.metadata',
|
'settings.metadata',
|
||||||
sortByProp('name')
|
sortByProp('name')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function createEnabledDownloadClientsSelector(
|
|||||||
protocol: DownloadProtocol
|
protocol: DownloadProtocol
|
||||||
) {
|
) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector<DownloadClient>(
|
createSortedSectionSelector<DownloadClient, DownloadClientAppState>(
|
||||||
'settings.downloadClients',
|
'settings.downloadClients',
|
||||||
sortByProp('name')
|
sortByProp('name')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
|
|
||||||
function selector(id, section) {
|
|
||||||
if (!id) {
|
|
||||||
const item = _.isArray(section.schema) ? section.selectedSchema : section.schema;
|
|
||||||
const settings = selectSettings(Object.assign({ name: '' }, item), section.pendingChanges, section.saveError);
|
|
||||||
|
|
||||||
const {
|
|
||||||
isSchemaFetching: isFetching,
|
|
||||||
isSchemaPopulated: isPopulated,
|
|
||||||
schemaError: error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges
|
|
||||||
} = section;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges,
|
|
||||||
...settings,
|
|
||||||
item: settings.settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
pendingChanges
|
|
||||||
} = section;
|
|
||||||
|
|
||||||
const settings = selectSettings(_.find(section.items, { id }), pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isTesting,
|
|
||||||
...settings,
|
|
||||||
item: settings.settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function createProviderSettingsSelector(sectionName) {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings[sectionName],
|
|
||||||
(id, section) => selector(id, section)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createProviderSettingsSelectorHook(sectionName, id) {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings[sectionName],
|
|
||||||
(section) => selector(id, section)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import {
|
||||||
|
AppSectionItemSchemaState,
|
||||||
|
AppSectionProviderState,
|
||||||
|
AppSectionSchemaState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
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>;
|
||||||
|
|
||||||
|
function selector<
|
||||||
|
T extends ModelBaseSetting,
|
||||||
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
|
>(id: number | undefined, section: S) {
|
||||||
|
if (id) {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
pendingChanges,
|
||||||
|
} = section;
|
||||||
|
|
||||||
|
const item = section.items.find((i) => i.id === id)!;
|
||||||
|
const settings = selectSettings<T>(item, pendingChanges, saveError);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
...settings,
|
||||||
|
item: settings.settings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const item =
|
||||||
|
'selectedSchema' in section
|
||||||
|
? section.selectedSchema
|
||||||
|
: (section.schema as T);
|
||||||
|
|
||||||
|
const settings = selectSettings(
|
||||||
|
Object.assign({ name: '' }, item),
|
||||||
|
section.pendingChanges ?? {},
|
||||||
|
section.saveError
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isSchemaFetching: isFetching,
|
||||||
|
isSchemaPopulated: isPopulated,
|
||||||
|
schemaError: error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
pendingChanges,
|
||||||
|
} = section;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
isTesting,
|
||||||
|
...settings,
|
||||||
|
pendingChanges,
|
||||||
|
item: settings.settings as PendingSection<T>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function createProviderSettingsSelector<
|
||||||
|
T extends ModelBase,
|
||||||
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
|
>(sectionName: string) {
|
||||||
|
// @ts-expect-error - This isn't fully typed
|
||||||
|
return createSelector(
|
||||||
|
(_state: AppState, { id }: { id: number }) => id,
|
||||||
|
(state) => state.settings[sectionName] as S,
|
||||||
|
(id: number, section: S) => selector(id, section)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProviderSettingsSelectorHook<
|
||||||
|
T extends ModelBaseSetting,
|
||||||
|
S extends AppSectionProviderState<T> & SchemaState<T>
|
||||||
|
>(sectionName: string, id: number | undefined) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings,
|
||||||
|
(state) => {
|
||||||
|
const sectionState = getSectionState(state, sectionName, false) as S;
|
||||||
|
|
||||||
|
return selector<T, S>(id, sectionState);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import sortByProp from 'Utilities/Array/sortByProp';
|
|||||||
|
|
||||||
export default function createRootFoldersSelector() {
|
export default function createRootFoldersSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector<RootFolder>('rootFolders', sortByProp('path')),
|
createSortedSectionSelector<RootFolder, RootFolderAppState>(
|
||||||
|
'rootFolders',
|
||||||
|
sortByProp('path')
|
||||||
|
),
|
||||||
(rootFolders: RootFolderAppState) => rootFolders
|
(rootFolders: RootFolderAppState) => rootFolders
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionProviderState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
function createSortedSectionSelector<T>(
|
function createSortedSectionSelector<
|
||||||
section: string,
|
T,
|
||||||
comparer: (a: T, b: T) => number
|
S extends AppSectionState<T> | AppSectionProviderState<T>
|
||||||
) {
|
>(section: string, comparer: (a: T, b: T) => number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state,
|
(state: AppState) => state,
|
||||||
(state) => {
|
(state) => {
|
||||||
const sectionState = getSectionState(state, section, true);
|
const sectionState = getSectionState(state, section, true) as S;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...sectionState,
|
...sectionState,
|
||||||
|
|||||||
@@ -68,14 +68,14 @@ function mapFailure(failure: ValidationFailure): Failure {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModelBaseSetting {
|
export interface ModelBaseSetting {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
[id: string]: any;
|
[id: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSettings<T extends ModelBaseSetting>(
|
function selectSettings<T extends ModelBaseSetting>(
|
||||||
item: T,
|
item: T,
|
||||||
pendingChanges: Partial<ModelBaseSetting>,
|
pendingChanges?: Partial<ModelBaseSetting>,
|
||||||
saveError?: Error
|
saveError?: Error
|
||||||
) {
|
) {
|
||||||
const { errors, warnings } = getValidationFailures(saveError);
|
const { errors, warnings } = getValidationFailures(saveError);
|
||||||
@@ -105,7 +105,7 @@ function selectSettings<T extends ModelBaseSetting>(
|
|||||||
warnings: getFailures(warnings, key),
|
warnings: getFailures(warnings, key),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (pendingChanges.hasOwnProperty(key)) {
|
if (pendingChanges?.hasOwnProperty(key)) {
|
||||||
setting.previousValue = setting.value;
|
setting.previousValue = setting.value;
|
||||||
setting.value = pendingChanges[key];
|
setting.value = pendingChanges[key];
|
||||||
setting.pending = true;
|
setting.pending = true;
|
||||||
@@ -126,7 +126,7 @@ function selectSettings<T extends ModelBaseSetting>(
|
|||||||
f
|
f
|
||||||
);
|
);
|
||||||
|
|
||||||
if ('fields' in pendingChanges) {
|
if (pendingChanges && 'fields' in pendingChanges) {
|
||||||
const pendingChangesFields = pendingChanges.fields as Record<
|
const pendingChangesFields = pendingChanges.fields as Record<
|
||||||
string,
|
string,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import Provider from './Provider';
|
import Provider from './Provider';
|
||||||
|
|
||||||
interface Indexer extends Provider {
|
interface Indexer extends Provider {
|
||||||
enableRss: boolean;
|
enableRss: boolean;
|
||||||
enableAutomaticSearch: boolean;
|
enableAutomaticSearch: boolean;
|
||||||
enableInteractiveSearch: boolean;
|
enableInteractiveSearch: boolean;
|
||||||
protocol: string;
|
supportsRss: boolean;
|
||||||
|
supportsSearch: boolean;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
priority: number;
|
priority: number;
|
||||||
|
downloadClientId: number;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export default interface IndexerOptions {
|
||||||
|
minimumAge: number;
|
||||||
|
retention: number;
|
||||||
|
maximumSize: number;
|
||||||
|
rssSyncInterval: number;
|
||||||
|
preferIndexerFlags: boolean;
|
||||||
|
availabilityDelay: number;
|
||||||
|
whitelistedHardcodedSubs: string[];
|
||||||
|
allowHardcodedSubs: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export type SaveCallback = () => void;
|
||||||
|
|
||||||
|
export interface SettingsStateChange {
|
||||||
|
isSaving: boolean;
|
||||||
|
hasPendingChanges: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SetChildSave = (SaveCallback: SaveCallback) => void;
|
||||||
|
|
||||||
|
export type OnChildStateChange = (change: SettingsStateChange) => void;
|
||||||
Reference in New Issue
Block a user