New: Project Aphrodite

This commit is contained in:
Qstick
2018-11-23 02:04:42 -05:00
parent 65efa15551
commit 8430cb40ab
1080 changed files with 73015 additions and 0 deletions
@@ -0,0 +1,100 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import { icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import IndexersConnector from './Indexers/IndexersConnector';
import IndexerOptionsConnector from './Options/IndexerOptionsConnector';
import RestrictionsConnector from './Restrictions/RestrictionsConnector';
class IndexerSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._saveCallback = null;
this.state = {
isSaving: false,
hasPendingChanges: false
};
}
//
// Listeners
onChildMounted = (saveCallback) => {
this._saveCallback = saveCallback;
}
onChildStateChange = (payload) => {
this.setState(payload);
}
onSavePress = () => {
if (this._saveCallback) {
this._saveCallback();
}
}
// Render
//
render() {
const {
isTestingAll,
dispatchTestAllIndexers
} = this.props;
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title="Indexer Settings">
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label="Test All Indexers"
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllIndexers}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBodyConnector>
<IndexersConnector />
<IndexerOptionsConnector
onChildMounted={this.onChildMounted}
onChildStateChange={this.onChildStateChange}
/>
<RestrictionsConnector />
</PageContentBodyConnector>
</PageContent>
);
}
}
IndexerSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllIndexers: PropTypes.func.isRequired
};
export default IndexerSettings;
@@ -0,0 +1,21 @@
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);
@@ -0,0 +1,44 @@
.indexer {
composes: card from 'Components/Card.css';
position: relative;
width: 300px;
height: 100px;
}
.underlay {
@add-mixin cover;
}
.overlay {
@add-mixin linkOverlay;
padding: 10px;
}
.name {
text-align: center;
font-weight: lighter;
font-size: 24px;
}
.actions {
margin-top: 20px;
text-align: right;
}
.presetsMenu {
composes: menu from 'Components/Menu/Menu.css';
display: inline-block;
margin: 0 5px;
}
.presetsMenuButton {
composes: button from 'Components/Link/Button.css';
&::after {
margin-left: 5px;
content: '\25BE';
}
}
@@ -0,0 +1,110 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { sizes } from 'Helpers/Props';
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 AddIndexerPresetMenuItem from './AddIndexerPresetMenuItem';
import styles from './AddIndexerItem.css';
class AddIndexerItem extends Component {
//
// Listeners
onIndexerSelect = () => {
const {
implementation
} = this.props;
this.props.onIndexerSelect({ implementation });
}
//
// 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}
>
Custom
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
Presets
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddIndexerPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
onPress={onIndexerSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
More info
</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,25 @@
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,5 @@
.indexers {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
@@ -0,0 +1,115 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import Alert from 'Components/Alert';
import Button from 'Components/Link/Button';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
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>
Add Indexer
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<div>Unable to add a new indexer, please try again.</div>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>Radarr supports any indexer that uses the Newznab standard, as well as other indexers listed below.</div>
<div>For more information on the individual indexers, clink on the info buttons.</div>
</Alert>
<FieldSet legend="Usenet">
<div className={styles.indexers}>
{
usenetIndexers.map((indexer) => {
return (
<AddIndexerItem
key={indexer.implementation}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
);
})
}
</div>
</FieldSet>
<FieldSet legend="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}
>
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,75 @@
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, name }) => {
this.props.selectIndexerSchema({ implementation, 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);
@@ -0,0 +1,49 @@
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
} = this.props;
this.props.onPress({
name,
implementation
});
}
//
// Render
render() {
const {
name,
implementation,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerPresetMenuItem;
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal';
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,65 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelTestIndexer, cancelSaveIndexer } 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);
@@ -0,0 +1,5 @@
.deleteButton {
composes: button from 'Components/Link/Button.css';
margin-right: auto;
}
@@ -0,0 +1,179 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
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,
enableSearch,
supportsRss,
supportsSearch,
fields
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Indexer - ${implementationName}`}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to add a new indexer, please try again.</div>
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Name</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Enable RSS</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableRss"
helpTextWarning={supportsRss.value ? undefined : 'RSS is not supported with this indexer'}
isDisabled={!supportsRss.value}
{...enableRss}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Enable Search</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableSearch"
helpText={supportsSearch.value ? 'Will be used when automatic searches are performed via the UI or by Radarr' : undefined}
helpTextWarning={supportsSearch.value ? undefined : 'Search is not supported with this indexer'}
isDisabled={!supportsSearch.value}
{...enableSearch}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
Delete
</Button>
}
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
Test
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
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,88 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
import { setIndexerValue, setIndexerFieldValue, saveIndexer, testIndexer } from 'Store/Actions/settingsActions';
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);
@@ -0,0 +1,19 @@
.indexer {
composes: card from 'Components/Card.css';
width: 290px;
}
.name {
@add-mixin truncate;
margin-bottom: 20px;
font-weight: 300;
font-size: 24px;
}
.enabled {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}
@@ -0,0 +1,131 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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);
}
//
// Render
render() {
const {
id,
name,
enableRss,
enableSearch,
supportsRss,
supportsSearch
} = this.props;
return (
<Card
className={styles.indexer}
overlayContent={true}
onPress={this.onEditIndexerPress}
>
<div className={styles.name}>
{name}
</div>
<div className={styles.enabled}>
{
supportsRss && enableRss &&
<Label kind={kinds.SUCCESS}>
RSS
</Label>
}
{
supportsSearch && enableSearch &&
<Label kind={kinds.SUCCESS}>
Search
</Label>
}
{
!enableRss && !enableSearch &&
<Label
kind={kinds.DISABLED}
outline={true}
>
Disabled
</Label>
}
</div>
<EditIndexerModalConnector
id={id}
isOpen={this.state.isEditIndexerModalOpen}
onModalClose={this.onEditIndexerModalClose}
onDeleteIndexerPress={this.onDeleteIndexerPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteIndexerModalOpen}
kind={kinds.DANGER}
title="Delete Indexer"
message={`Are you sure you want to delete the indexer '${name}'?`}
confirmLabel="Delete"
onConfirm={this.onConfirmDeleteIndexer}
onCancel={this.onDeleteIndexerModalClose}
/>
</Card>
);
}
}
Indexer.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enableRss: PropTypes.bool.isRequired,
enableSearch: PropTypes.bool.isRequired,
supportsRss: PropTypes.bool.isRequired,
supportsSearch: PropTypes.bool.isRequired,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexer;
@@ -0,0 +1,20 @@
.indexers {
display: flex;
flex-wrap: wrap;
}
.addIndexer {
composes: indexer from './Indexer.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}
@@ -0,0 +1,115 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import sortByName from 'Utilities/Array/sortByName';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Card from 'Components/Card';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import Indexer from './Indexer';
import AddIndexerModal from './AddIndexerModal';
import EditIndexerModalConnector from './EditIndexerModalConnector';
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 });
}
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
this.setState({
isAddIndexerModalOpen: false,
isEditIndexerModalOpen: indexerSelected
});
}
onEditIndexerModalClose = () => {
this.setState({ isEditIndexerModalOpen: false });
}
//
// Render
render() {
const {
items,
onConfirmDeleteIndexer,
...otherProps
} = this.props;
const {
isAddIndexerModalOpen,
isEditIndexerModalOpen
} = this.state;
return (
<FieldSet legend="Indexers">
<PageSectionContent
errorMessage="Unable to load Indexers"
{...otherProps}
>
<div className={styles.indexers}>
{
items.sort(sortByName).map((item) => {
return (
<Indexer
key={item.id}
{...item}
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,
onConfirmDeleteIndexer: PropTypes.func.isRequired
};
export default Indexers;
@@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexers, deleteIndexer } from 'Store/Actions/settingsActions';
import Indexers from './Indexers';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(indexers) => {
return {
...indexers
};
}
);
}
const mapDispatchToProps = {
fetchIndexers,
deleteIndexer
};
class IndexersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexers();
}
//
// Listeners
onConfirmDeleteIndexer = (id) => {
this.props.deleteIndexer({ id });
}
//
// Render
render() {
return (
<Indexers
{...this.props}
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
/>
);
}
}
IndexersConnector.propTypes = {
fetchIndexers: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);
@@ -0,0 +1,112 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
function IndexerOptions(props) {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
onInputChange
} = props;
return (
<FieldSet legend="Options">
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<div>Unable to load indexer options</div>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup>
<FormLabel>Minimum Age</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="minimumAge"
min={0}
unit="minutes"
helpText="Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider."
onChange={onInputChange}
{...settings.minimumAge}
/>
</FormGroup>
<FormGroup>
<FormLabel>Retention</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="retention"
min={0}
unit="days"
helpText="Usenet only: Set to zero to set for unlimited retention"
onChange={onInputChange}
{...settings.retention}
/>
</FormGroup>
<FormGroup>
<FormLabel>Maximum Size</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="maximumSize"
min={0}
unit="MB"
helpText="Maximum size for a release to be grabbed in MB. Set to zero to set to unlimited"
onChange={onInputChange}
{...settings.maximumSize}
/>
</FormGroup>
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>RSS Sync Interval</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="rssSyncInterval"
min={0}
max={120}
unit="minutes"
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
helpLink="https://github.com/Radarr/Radarr/wiki/RSS-Sync"
onChange={onInputChange}
{...settings.rssSyncInterval}
/>
</FormGroup>
</Form>
}
</FieldSet>
);
}
IndexerOptions.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default IndexerOptions;
@@ -0,0 +1,101 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import { fetchIndexerOptions, setIndexerOptionsValue, saveIndexerOptions } from 'Store/Actions/settingsActions';
import { clearPendingChanges } from 'Store/Actions/baseActions';
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: SECTION });
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetIndexerOptionsValue({ name, value });
}
//
// Render
render() {
return (
<IndexerOptions
onInputChange={this.onInputChange}
{...this.props}
/>
);
}
}
IndexerOptionsConnector.propTypes = {
isSaving: PropTypes.bool.isRequired,
hasPendingChanges: PropTypes.bool.isRequired,
dispatchFetchIndexerOptions: PropTypes.func.isRequired,
dispatchSetIndexerOptionsValue: PropTypes.func.isRequired,
dispatchSaveIndexerOptions: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
onChildMounted: PropTypes.func.isRequired,
onChildStateChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerOptionsConnector);
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import { sizes } from 'Helpers/Props';
import Modal from 'Components/Modal/Modal';
import EditRestrictionModalContentConnector from './EditRestrictionModalContentConnector';
function EditRestrictionModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditRestrictionModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditRestrictionModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditRestrictionModal;
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditRestrictionModal from './EditRestrictionModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditRestrictionModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'settings.restrictions' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditRestrictionModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditRestrictionModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(EditRestrictionModalConnector);
@@ -0,0 +1,5 @@
.deleteButton {
composes: button from 'Components/Link/Button.css';
margin-right: auto;
}
@@ -0,0 +1,126 @@
import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes, kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import styles from './EditRestrictionModalContent.css';
function EditRestrictionModalContent(props) {
const {
isSaving,
saveError,
item,
onInputChange,
onModalClose,
onSavePress,
onDeleteRestrictionPress,
...otherProps
} = props;
const {
id,
required,
ignored,
tags
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? 'Edit Restriction' : 'Add Restriction'}
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Must Contain</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="required"
helpText="The release must contain at least one of these terms (case insensitive)"
kind={kinds.SUCCESS}
placeholder="Add new restriction"
{...required}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Must Not Contain</FormLabel>
<FormInputGroup
type={inputTypes.TEXT_TAG}
name="ignored"
helpText="The release will be rejected if it contains one or more of terms (case insensitive)"
kind={kinds.DANGER}
placeholder="Add new restriction"
{...ignored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
helpText="Restrictions will apply to series at least one matching tag. Leave blank to apply to all series"
{...tags}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteRestrictionPress}
>
Delete
</Button>
}
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
Save
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditRestrictionModalContent.propTypes = {
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onDeleteRestrictionPress: PropTypes.func
};
export default EditRestrictionModalContent;
@@ -0,0 +1,111 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import selectSettings from 'Store/Selectors/selectSettings';
import { setRestrictionValue, saveRestriction } from 'Store/Actions/settingsActions';
import EditRestrictionModalContent from './EditRestrictionModalContent';
const newRestriction = {
required: '',
ignored: '',
tags: []
};
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
(state) => state.settings.restrictions,
(id, restrictions) => {
const {
isFetching,
error,
isSaving,
saveError,
pendingChanges,
items
} = restrictions;
const profile = id ? _.find(items, { id }) : newRestriction;
const settings = selectSettings(profile, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
setRestrictionValue,
saveRestriction
};
class EditRestrictionModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.id) {
Object.keys(newRestriction).forEach((name) => {
this.props.setRestrictionValue({
name,
value: newRestriction[name]
});
});
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setRestrictionValue({ name, value });
}
onSavePress = () => {
this.props.saveRestriction({ id: this.props.id });
}
//
// Render
render() {
return (
<EditRestrictionModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditRestrictionModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setRestrictionValue: PropTypes.func.isRequired,
saveRestriction: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditRestrictionModalContentConnector);
@@ -0,0 +1,11 @@
.restriction {
composes: card from 'Components/Card.css';
width: 290px;
}
.enabled {
display: flex;
flex-wrap: wrap;
margin-top: 5px;
}
@@ -0,0 +1,148 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import split from 'Utilities/String/split';
import { kinds } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import TagList from 'Components/TagList';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import EditRestrictionModalConnector from './EditRestrictionModalConnector';
import styles from './Restriction.css';
class Restriction extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditRestrictionModalOpen: false,
isDeleteRestrictionModalOpen: false
};
}
//
// Listeners
onEditRestrictionPress = () => {
this.setState({ isEditRestrictionModalOpen: true });
}
onEditRestrictionModalClose = () => {
this.setState({ isEditRestrictionModalOpen: false });
}
onDeleteRestrictionPress = () => {
this.setState({
isEditRestrictionModalOpen: false,
isDeleteRestrictionModalOpen: true
});
}
onDeleteRestrictionModalClose= () => {
this.setState({ isDeleteRestrictionModalOpen: false });
}
onConfirmDeleteRestriction = () => {
this.props.onConfirmDeleteRestriction(this.props.id);
}
//
// Render
render() {
const {
id,
required,
ignored,
tags,
tagList
} = this.props;
return (
<Card
className={styles.restriction}
overlayContent={true}
onPress={this.onEditRestrictionPress}
>
<div>
{
split(required).map((item) => {
if (!item) {
return null;
}
return (
<Label
key={item}
kind={kinds.SUCCESS}
>
{item}
</Label>
);
})
}
</div>
<div>
{
split(ignored).map((item) => {
if (!item) {
return null;
}
return (
<Label
key={item}
kind={kinds.DANGER}
>
{item}
</Label>
);
})
}
</div>
<TagList
tags={tags}
tagList={tagList}
/>
<EditRestrictionModalConnector
id={id}
isOpen={this.state.isEditRestrictionModalOpen}
onModalClose={this.onEditRestrictionModalClose}
onDeleteRestrictionPress={this.onDeleteRestrictionPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteRestrictionModalOpen}
kind={kinds.DANGER}
title="Delete Restriction"
message={'Are you sure you want to delete this restriction?'}
confirmLabel="Delete"
onConfirm={this.onConfirmDeleteRestriction}
onCancel={this.onDeleteRestrictionModalClose}
/>
</Card>
);
}
}
Restriction.propTypes = {
id: PropTypes.number.isRequired,
required: PropTypes.string.isRequired,
ignored: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteRestriction: PropTypes.func.isRequired
};
Restriction.defaultProps = {
required: '',
ignored: ''
};
export default Restriction;
@@ -0,0 +1,20 @@
.restrictions {
display: flex;
flex-wrap: wrap;
}
.addRestriction {
composes: restriction from './Restriction.css';
background-color: $cardAlternateBackgroundColor;
color: $gray;
text-align: center;
}
.center {
display: inline-block;
padding: 5px 20px 0;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: $white;
}
@@ -0,0 +1,98 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet';
import Card from 'Components/Card';
import Icon from 'Components/Icon';
import PageSectionContent from 'Components/Page/PageSectionContent';
import Restriction from './Restriction';
import EditRestrictionModalConnector from './EditRestrictionModalConnector';
import styles from './Restrictions.css';
class Restrictions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddRestrictionModalOpen: false
};
}
//
// Listeners
onAddRestrictionPress = () => {
this.setState({ isAddRestrictionModalOpen: true });
}
onAddRestrictionModalClose = () => {
this.setState({ isAddRestrictionModalOpen: false });
}
//
// Render
render() {
const {
items,
tagList,
onConfirmDeleteRestriction,
...otherProps
} = this.props;
return (
<FieldSet legend="Restrictions">
<PageSectionContent
errorMessage="Unable to load Restrictions"
{...otherProps}
>
<div className={styles.restrictions}>
<Card
className={styles.addRestriction}
onPress={this.onAddRestrictionPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
{
items.map((item) => {
return (
<Restriction
key={item.id}
tagList={tagList}
{...item}
onConfirmDeleteRestriction={onConfirmDeleteRestriction}
/>
);
})
}
</div>
<EditRestrictionModalConnector
isOpen={this.state.isAddRestrictionModalOpen}
onModalClose={this.onAddRestrictionModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
Restrictions.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteRestriction: PropTypes.func.isRequired
};
export default Restrictions;
@@ -0,0 +1,61 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchRestrictions, deleteRestriction } from 'Store/Actions/settingsActions';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import Restrictions from './Restrictions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.restrictions,
createTagsSelector(),
(restrictions, tagList) => {
return {
...restrictions,
tagList
};
}
);
}
const mapDispatchToProps = {
fetchRestrictions,
deleteRestriction
};
class RestrictionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchRestrictions();
}
//
// Listeners
onConfirmDeleteRestriction = (id) => {
this.props.deleteRestriction({ id });
}
//
// Render
render() {
return (
<Restrictions
{...this.props}
onConfirmDeleteRestriction={this.onConfirmDeleteRestriction}
/>
);
}
}
RestrictionsConnector.propTypes = {
fetchRestrictions: PropTypes.func.isRequired,
deleteRestriction: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(RestrictionsConnector);