New: Download Clients for Manual Grabs

This commit is contained in:
Qstick
2021-03-18 00:07:25 -04:00
parent fd27018caa
commit 881313ef2b
209 changed files with 12229 additions and 127 deletions

View File

@@ -9,6 +9,7 @@ import StatsConnector from 'Indexer/Stats/StatsConnector';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Settings from 'Settings/Settings';
@@ -94,6 +95,11 @@ function AppRoutes(props) {
component={ApplicationSettingsConnector}
/>
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/connect"
component={NotificationSettings}

View File

@@ -52,6 +52,10 @@ const links = [
title: translate('Apps'),
to: '/settings/applications'
},
{
title: translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: translate('Connect'),
to: '/settings/connect'

View File

@@ -266,7 +266,6 @@ IndexerIndexRow.propTypes = {
added: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSearchingMovie: PropTypes.bool.isRequired,
isMovieEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,

View File

@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
@@ -15,8 +16,59 @@ import Peers from './Peers';
import ProtocolLabel from './ProtocolLabel';
import styles from './SearchIndexRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
return icons.DOWNLOADING;
} else if (grabError) {
return icons.DOWNLOADING;
}
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
return translate('AddedToDownloadClient');
} else if (grabError) {
return grabError;
}
return translate('AddToDownloadClient');
}
class SearchIndexRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isConfirmGrabModalOpen: false
};
}
//
// Listeners
onGrabPress = () => {
const {
guid,
indexerId,
onGrabPress
} = this.props;
onGrabPress({
guid,
indexerId
});
}
//
// Render
@@ -39,6 +91,9 @@ class SearchIndexRow extends Component {
leechers,
indexerFlags,
columns,
isGrabbing,
isGrabbed,
grabError,
longDateFormat,
timeFormat
} = this.props;
@@ -214,11 +269,13 @@ class SearchIndexRow extends Component {
key={column.name}
className={styles[column.name]}
>
<IconButton
className={styles.downloadLink}
name={icons.DOWNLOAD}
title={'Grab'}
to={downloadUrl}
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
<IconButton
@@ -259,8 +316,17 @@ SearchIndexRow.propTypes = {
leechers: PropTypes.number,
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onGrabPress: PropTypes.func.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
SearchIndexRow.defaultProps = {
isGrabbing: false,
isGrabbed: false
};
export default SearchIndexRow;

View File

@@ -48,7 +48,8 @@ class SearchIndexTable extends Component {
items,
columns,
longDateFormat,
timeFormat
timeFormat,
onGrabPress
} = this.props;
const release = items[rowIndex];
@@ -65,6 +66,7 @@ class SearchIndexTable extends Component {
guid={release.guid}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
onGrabPress={onGrabPress}
/>
</VirtualTableRow>
);
@@ -118,7 +120,8 @@ SearchIndexTable.propTypes = {
scroller: PropTypes.instanceOf(Element).isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired
onSortPress: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
export default SearchIndexTable;

View File

@@ -1,20 +1,20 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setReleasesSort } from 'Store/Actions/releaseActions';
import { grabRelease, setReleasesSort } from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import SearchIndexTable from './SearchIndexTable';
function createMapStateToProps() {
return createSelector(
(state) => state.app.dimensions,
(state) => state.releases.columns,
(state) => state.releases,
createUISettingsSelector(),
(dimensions, columns, uiSettings) => {
(dimensions, releases, uiSettings) => {
return {
isSmallScreen: dimensions.isSmallScreen,
columns,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
timeFormat: uiSettings.timeFormat,
...releases
};
}
);
@@ -24,6 +24,9 @@ function createMapDispatchToProps(dispatch, props) {
return {
onSortPress(sortKey) {
dispatch(setReleasesSort({ sortKey }));
},
onGrabPress(payload) {
dispatch(grabRelease(payload));
}
};
}

View File

@@ -0,0 +1,92 @@
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 SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import DownloadClientsConnector from './DownloadClients/DownloadClientsConnector';
class DownloadClientSettings 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,
dispatchTestAllDownloadClients
} = this.props;
const {
isSaving,
hasPendingChanges
} = this.state;
return (
<PageContent title={translate('DownloadClientSettings')}>
<SettingsToolbarConnector
isSaving={isSaving}
hasPendingChanges={hasPendingChanges}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('TestAllClients')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={dispatchTestAllDownloadClients}
/>
</Fragment>
}
onSavePress={this.onSavePress}
/>
<PageContentBody>
<DownloadClientsConnector />
</PageContentBody>
</PageContent>
);
}
}
DownloadClientSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
dispatchTestAllDownloadClients: PropTypes.func.isRequired
};
export default DownloadClientSettings;

View File

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

View File

@@ -0,0 +1,44 @@
.downloadClient {
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';
}
}

View File

@@ -0,0 +1,111 @@
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 AddDownloadClientPresetMenuItem from './AddDownloadClientPresetMenuItem';
import styles from './AddDownloadClientItem.css';
class AddDownloadClientItem extends Component {
//
// Listeners
onDownloadClientSelect = () => {
const {
implementation
} = this.props;
this.props.onDownloadClientSelect({ implementation });
}
//
// Render
render() {
const {
implementation,
implementationName,
infoLink,
presets,
onDownloadClientSelect
} = this.props;
const hasPresets = !!presets && !!presets.length;
return (
<div
className={styles.downloadClient}
>
<Link
className={styles.underlay}
onPress={this.onDownloadClientSelect}
/>
<div className={styles.overlay}>
<div className={styles.name}>
{implementationName}
</div>
<div className={styles.actions}>
{
hasPresets &&
<span>
<Button
size={sizes.SMALL}
onPress={this.onDownloadClientSelect}
>
{translate('Custom')}
</Button>
<Menu className={styles.presetsMenu}>
<Button
className={styles.presetsMenuButton}
size={sizes.SMALL}
>
{translate('Presets')}
</Button>
<MenuContent>
{
presets.map((preset) => {
return (
<AddDownloadClientPresetMenuItem
key={preset.name}
name={preset.name}
implementation={implementation}
onPress={onDownloadClientSelect}
/>
);
})
}
</MenuContent>
</Menu>
</span>
}
<Button
to={infoLink}
size={sizes.SMALL}
>
{translate('MoreInfo')}
</Button>
</div>
</div>
</div>
);
}
}
AddDownloadClientItem.propTypes = {
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
infoLink: PropTypes.string.isRequired,
presets: PropTypes.arrayOf(PropTypes.object),
onDownloadClientSelect: PropTypes.func.isRequired
};
export default AddDownloadClientItem;

View File

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

View File

@@ -0,0 +1,5 @@
.downloadClients {
display: flex;
justify-content: center;
flex-wrap: wrap;
}

View File

@@ -0,0 +1,122 @@
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 AddDownloadClientItem from './AddDownloadClientItem';
import styles from './AddDownloadClientModalContent.css';
class AddDownloadClientModalContent extends Component {
//
// Render
render() {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetDownloadClients,
torrentDownloadClients,
onDownloadClientSelect,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AddDownloadClient')}
</ModalHeader>
<ModalBody>
{
isSchemaFetching &&
<LoadingIndicator />
}
{
!isSchemaFetching && !!schemaError &&
<div>
{translate('UnableToAddANewDownloadClientPleaseTryAgain')}
</div>
}
{
isSchemaPopulated && !schemaError &&
<div>
<Alert kind={kinds.INFO}>
<div>
{translate('ProwlarrSupportsAnyDownloadClient')}
</div>
<div>
{translate('ForMoreInformationOnTheIndividualDownloadClients')}
</div>
</Alert>
<FieldSet legend={translate('Usenet')}>
<div className={styles.downloadClients}>
{
usenetDownloadClients.map((downloadClient) => {
return (
<AddDownloadClientItem
key={downloadClient.implementation}
implementation={downloadClient.implementation}
{...downloadClient}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})
}
</div>
</FieldSet>
<FieldSet legend={translate('Torrents')}>
<div className={styles.downloadClients}>
{
torrentDownloadClients.map((downloadClient) => {
return (
<AddDownloadClientItem
key={downloadClient.implementation}
implementation={downloadClient.implementation}
{...downloadClient}
onDownloadClientSelect={onDownloadClientSelect}
/>
);
})
}
</div>
</FieldSet>
</div>
}
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddDownloadClientModalContent.propTypes = {
isSchemaFetching: PropTypes.bool.isRequired,
isSchemaPopulated: PropTypes.bool.isRequired,
schemaError: PropTypes.object,
usenetDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
torrentDownloadClients: PropTypes.arrayOf(PropTypes.object).isRequired,
onDownloadClientSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddDownloadClientModalContent;

View File

@@ -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 { fetchDownloadClientSchema, selectDownloadClientSchema } from 'Store/Actions/settingsActions';
import AddDownloadClientModalContent from './AddDownloadClientModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(downloadClients) => {
const {
isSchemaFetching,
isSchemaPopulated,
schemaError,
schema
} = downloadClients;
const usenetDownloadClients = _.filter(schema, { protocol: 'usenet' });
const torrentDownloadClients = _.filter(schema, { protocol: 'torrent' });
return {
isSchemaFetching,
isSchemaPopulated,
schemaError,
usenetDownloadClients,
torrentDownloadClients
};
}
);
}
const mapDispatchToProps = {
fetchDownloadClientSchema,
selectDownloadClientSchema
};
class AddDownloadClientModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchDownloadClientSchema();
}
//
// Listeners
onDownloadClientSelect = ({ implementation }) => {
this.props.selectDownloadClientSchema({ implementation });
this.props.onModalClose({ downloadClientSelected: true });
}
//
// Render
render() {
return (
<AddDownloadClientModalContent
{...this.props}
onDownloadClientSelect={this.onDownloadClientSelect}
/>
);
}
}
AddDownloadClientModalContentConnector.propTypes = {
fetchDownloadClientSchema: PropTypes.func.isRequired,
selectDownloadClientSchema: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddDownloadClientModalContentConnector);

View File

@@ -0,0 +1,49 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddDownloadClientPresetMenuItem 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>
);
}
}
AddDownloadClientPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddDownloadClientPresetMenuItem;

View File

@@ -0,0 +1,19 @@
.downloadClient {
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;
}

View File

@@ -0,0 +1,126 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
import styles from './DownloadClient.css';
class DownloadClient extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditDownloadClientModalOpen: false,
isDeleteDownloadClientModalOpen: false
};
}
//
// Listeners
onEditDownloadClientPress = () => {
this.setState({ isEditDownloadClientModalOpen: true });
}
onEditDownloadClientModalClose = () => {
this.setState({ isEditDownloadClientModalOpen: false });
}
onDeleteDownloadClientPress = () => {
this.setState({
isEditDownloadClientModalOpen: false,
isDeleteDownloadClientModalOpen: true
});
}
onDeleteDownloadClientModalClose= () => {
this.setState({ isDeleteDownloadClientModalOpen: false });
}
onConfirmDeleteDownloadClient = () => {
this.props.onConfirmDeleteDownloadClient(this.props.id);
}
//
// Render
render() {
const {
id,
name,
enable,
priority
} = this.props;
return (
<Card
className={styles.downloadClient}
overlayContent={true}
onPress={this.onEditDownloadClientPress}
>
<div className={styles.name}>
{name}
</div>
<div className={styles.enabled}>
{
enable ?
<Label kind={kinds.SUCCESS}>
{translate('Enabled')}
</Label> :
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('Disabled')}
</Label>
}
{
priority > 1 &&
<Label
kind={kinds.DISABLED}
outline={true}
>
{translate('PrioritySettings', [priority])}
</Label>
}
</div>
<EditDownloadClientModalConnector
id={id}
isOpen={this.state.isEditDownloadClientModalOpen}
onModalClose={this.onEditDownloadClientModalClose}
onDeleteDownloadClientPress={this.onDeleteDownloadClientPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER}
title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose}
/>
</Card>
);
}
}
DownloadClient.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
enable: PropTypes.bool.isRequired,
priority: PropTypes.number.isRequired,
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
};
export default DownloadClient;

View File

@@ -0,0 +1,20 @@
.downloadClients {
display: flex;
flex-wrap: wrap;
}
.addDownloadClient {
composes: downloadClient from '~./DownloadClient.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;
}

View File

@@ -0,0 +1,115 @@
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 AddDownloadClientModal from './AddDownloadClientModal';
import DownloadClient from './DownloadClient';
import EditDownloadClientModalConnector from './EditDownloadClientModalConnector';
import styles from './DownloadClients.css';
class DownloadClients extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddDownloadClientModalOpen: false,
isEditDownloadClientModalOpen: false
};
}
//
// Listeners
onAddDownloadClientPress = () => {
this.setState({ isAddDownloadClientModalOpen: true });
}
onAddDownloadClientModalClose = ({ downloadClientSelected = false } = {}) => {
this.setState({
isAddDownloadClientModalOpen: false,
isEditDownloadClientModalOpen: downloadClientSelected
});
}
onEditDownloadClientModalClose = () => {
this.setState({ isEditDownloadClientModalOpen: false });
}
//
// Render
render() {
const {
items,
onConfirmDeleteDownloadClient,
...otherProps
} = this.props;
const {
isAddDownloadClientModalOpen,
isEditDownloadClientModalOpen
} = this.state;
return (
<FieldSet legend={translate('DownloadClients')}>
<PageSectionContent
errorMessage={translate('UnableToLoadDownloadClients')}
{...otherProps}
>
<div className={styles.downloadClients}>
{
items.map((item) => {
return (
<DownloadClient
key={item.id}
{...item}
onConfirmDeleteDownloadClient={onConfirmDeleteDownloadClient}
/>
);
})
}
<Card
className={styles.addDownloadClient}
onPress={this.onAddDownloadClientPress}
>
<div className={styles.center}>
<Icon
name={icons.ADD}
size={45}
/>
</div>
</Card>
</div>
<AddDownloadClientModal
isOpen={isAddDownloadClientModalOpen}
onModalClose={this.onAddDownloadClientModalClose}
/>
<EditDownloadClientModalConnector
isOpen={isEditDownloadClientModalOpen}
onModalClose={this.onEditDownloadClientModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
DownloadClients.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteDownloadClient: PropTypes.func.isRequired
};
export default DownloadClients;

View File

@@ -0,0 +1,56 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteDownloadClient, fetchDownloadClients } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import DownloadClients from './DownloadClients';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName),
(downloadClients) => downloadClients
);
}
const mapDispatchToProps = {
fetchDownloadClients,
deleteDownloadClient
};
class DownloadClientsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchDownloadClients();
}
//
// Listeners
onConfirmDeleteDownloadClient = (id) => {
this.props.deleteDownloadClient({ id });
}
//
// Render
render() {
return (
<DownloadClients
{...this.props}
onConfirmDeleteDownloadClient={this.onConfirmDeleteDownloadClient}
/>
);
}
}
DownloadClientsConnector.propTypes = {
fetchDownloadClients: PropTypes.func.isRequired,
deleteDownloadClient: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientsConnector);

View File

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

View File

@@ -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 { cancelSaveDownloadClient, cancelTestDownloadClient } from 'Store/Actions/settingsActions';
import EditDownloadClientModal from './EditDownloadClientModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'settings.downloadClients';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestDownloadClient() {
dispatch(cancelTestDownloadClient({ section }));
},
dispatchCancelSaveDownloadClient() {
dispatch(cancelSaveDownloadClient({ section }));
}
};
}
class EditDownloadClientModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestDownloadClient();
this.props.dispatchCancelSaveDownloadClient();
this.props.onModalClose();
}
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestDownloadClient,
dispatchCancelSaveDownloadClient,
...otherProps
} = this.props;
return (
<EditDownloadClientModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditDownloadClientModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestDownloadClient: PropTypes.func.isRequired,
dispatchCancelSaveDownloadClient: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditDownloadClientModalConnector);

View File

@@ -0,0 +1,11 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
.message {
composes: alert from '~Components/Alert.css';
margin-bottom: 30px;
}

View File

@@ -0,0 +1,197 @@
import PropTypes from 'prop-types';
import React, { Component } 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 translate from 'Utilities/String/translate';
import styles from './EditDownloadClientModalContent.css';
class EditDownloadClientModalContent extends Component {
//
// Render
render() {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteDownloadClientPress,
...otherProps
} = this.props;
const {
id,
implementationName,
name,
enable,
priority,
fields,
message
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('Edit') : translate('Add')} ${translate('DownloadClient')} - ${implementationName}`}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToAddANewDownloadClientPleaseTryAgain')}
</div>
}
{
!isFetching && !error &&
<Form {...otherProps}>
{
!!message &&
<Alert
className={styles.message}
kind={message.value.type}
>
{message.value.message}
</Alert>
}
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enable"
{...enable}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="downloadClient"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('ClientPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('PriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteDownloadClientPress}
>
{translate('Delete')}
</Button>
}
<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>
);
}
}
EditDownloadClientModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isTesting: PropTypes.bool.isRequired,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteDownloadClientPress: PropTypes.func
};
export default EditDownloadClientModalContent;

View File

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

View File

@@ -25,6 +25,17 @@ function Settings() {
Applications and settings to configure how prowlarr interacts with your PVR programs
</div>
<Link
className={styles.link}
to="/settings/downloadclients"
>
{translate('DownloadClients')}
</Link>
<div className={styles.summary}>
{translate('DownloadClientsSettingsSummary')}
</div>
<Link
className={styles.link}
to="/settings/connect"

View File

@@ -0,0 +1,117 @@
import { createAction } from 'redux-actions';
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import { createThunk } from 'Store/thunks';
import selectProviderSchema from 'Utilities/State/selectProviderSchema';
//
// Variables
const section = 'settings.downloadClients';
//
// Actions Types
export const FETCH_DOWNLOAD_CLIENTS = 'settings/downloadClients/fetchDownloadClients';
export const FETCH_DOWNLOAD_CLIENT_SCHEMA = 'settings/downloadClients/fetchDownloadClientSchema';
export const SELECT_DOWNLOAD_CLIENT_SCHEMA = 'settings/downloadClients/selectDownloadClientSchema';
export const SET_DOWNLOAD_CLIENT_VALUE = 'settings/downloadClients/setDownloadClientValue';
export const SET_DOWNLOAD_CLIENT_FIELD_VALUE = 'settings/downloadClients/setDownloadClientFieldValue';
export const SAVE_DOWNLOAD_CLIENT = 'settings/downloadClients/saveDownloadClient';
export const CANCEL_SAVE_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelSaveDownloadClient';
export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadClient';
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
//
// Action Creators
export const fetchDownloadClients = createThunk(FETCH_DOWNLOAD_CLIENTS);
export const fetchDownloadClientSchema = createThunk(FETCH_DOWNLOAD_CLIENT_SCHEMA);
export const selectDownloadClientSchema = createAction(SELECT_DOWNLOAD_CLIENT_SCHEMA);
export const saveDownloadClient = createThunk(SAVE_DOWNLOAD_CLIENT);
export const cancelSaveDownloadClient = createThunk(CANCEL_SAVE_DOWNLOAD_CLIENT);
export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
return {
section,
...payload
};
});
export const setDownloadClientFieldValue = createAction(SET_DOWNLOAD_CLIENT_FIELD_VALUE, (payload) => {
return {
section,
...payload
};
});
//
// Details
export default {
//
// State
defaultState: {
isFetching: false,
isPopulated: false,
error: null,
isSchemaFetching: false,
isSchemaPopulated: false,
schemaError: null,
schema: [],
selectedSchema: {},
isSaving: false,
saveError: null,
isTesting: false,
isTestingAll: false,
items: [],
pendingChanges: {}
},
//
// Action Handlers
actionHandlers: {
[FETCH_DOWNLOAD_CLIENTS]: createFetchHandler(section, '/downloadclient'),
[FETCH_DOWNLOAD_CLIENT_SCHEMA]: createFetchSchemaHandler(section, '/downloadclient/schema'),
[SAVE_DOWNLOAD_CLIENT]: createSaveProviderHandler(section, '/downloadclient'),
[CANCEL_SAVE_DOWNLOAD_CLIENT]: createCancelSaveProviderHandler(section),
[DELETE_DOWNLOAD_CLIENT]: createRemoveItemHandler(section, '/downloadclient'),
[TEST_DOWNLOAD_CLIENT]: createTestProviderHandler(section, '/downloadclient'),
[CANCEL_TEST_DOWNLOAD_CLIENT]: createCancelTestProviderHandler(section),
[TEST_ALL_DOWNLOAD_CLIENTS]: createTestAllProvidersHandler(section, '/downloadclient')
},
//
// Reducers
reducers: {
[SET_DOWNLOAD_CLIENT_VALUE]: createSetSettingValueReducer(section),
[SET_DOWNLOAD_CLIENT_FIELD_VALUE]: createSetProviderFieldValueReducer(section),
[SELECT_DOWNLOAD_CLIENT_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.enable = true;
return selectedSchema;
});
}
}
};

View File

@@ -247,7 +247,7 @@ export const actionHandlers = handleThunks({
dispatch(updateRelease({ guid, isGrabbing: true }));
const promise = createAjaxRequest({
url: '/release',
url: '/search',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload)

View File

@@ -3,6 +3,7 @@ import { handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
import applications from './Settings/applications';
import development from './Settings/development';
import downloadClients from './Settings/downloadClients';
import general from './Settings/general';
import indexerCategories from './Settings/indexerCategories';
import indexerFlags from './Settings/indexerFlags';
@@ -10,6 +11,7 @@ import languages from './Settings/languages';
import notifications from './Settings/notifications';
import ui from './Settings/ui';
export * from './Settings/downloadClients';
export * from './Settings/general';
export * from './Settings/indexerCategories';
export * from './Settings/indexerFlags';
@@ -30,6 +32,7 @@ export const section = 'settings';
export const defaultState = {
advancedSettings: false,
downloadClients: downloadClients.defaultState,
general: general.defaultState,
indexerCategories: indexerCategories.defaultState,
indexerFlags: indexerFlags.defaultState,
@@ -58,6 +61,7 @@ export const toggleAdvancedSettings = createAction(TOGGLE_ADVANCED_SETTINGS);
// Action Handlers
export const actionHandlers = handleThunks({
...downloadClients.actionHandlers,
...general.actionHandlers,
...indexerCategories.actionHandlers,
...indexerFlags.actionHandlers,
@@ -77,6 +81,7 @@ export const reducers = createHandleActions({
return Object.assign({}, state, { advancedSettings: !state.advancedSettings });
},
...downloadClients.reducers,
...general.reducers,
...indexerCategories.reducers,
...indexerFlags.reducers,