mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-17 15:02:34 +02:00
New: Import and export custom formats
This commit is contained in:
parent
e014826b17
commit
c3a89c46f5
@ -55,6 +55,7 @@ import {
|
|||||||
faEye as fasEye,
|
faEye as fasEye,
|
||||||
faFastBackward as fasFastBackward,
|
faFastBackward as fasFastBackward,
|
||||||
faFastForward as fasFastForward,
|
faFastForward as fasFastForward,
|
||||||
|
faFileExport as fasFileExport,
|
||||||
faFileInvoice as farFileInvoice,
|
faFileInvoice as farFileInvoice,
|
||||||
faFilm as fasFilm,
|
faFilm as fasFilm,
|
||||||
faFilter as fasFilter,
|
faFilter as fasFilter,
|
||||||
@ -145,6 +146,7 @@ export const EDIT = fasWrench;
|
|||||||
export const MOVIE_FILE = farFileVideo;
|
export const MOVIE_FILE = farFileVideo;
|
||||||
export const EXPAND = fasChevronCircleDown;
|
export const EXPAND = fasChevronCircleDown;
|
||||||
export const EXPAND_INDETERMINATE = fasChevronCircleRight;
|
export const EXPAND_INDETERMINATE = fasChevronCircleRight;
|
||||||
|
export const EXPORT = fasFileExport;
|
||||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||||
export const FATAL = fasTimesCircle;
|
export const FATAL = fasTimesCircle;
|
||||||
export const FILE = farFile;
|
export const FILE = farFile;
|
||||||
|
@ -7,6 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||||
|
import ExportCustomFormatModal from './ExportCustomFormatModal';
|
||||||
import styles from './CustomFormat.css';
|
import styles from './CustomFormat.css';
|
||||||
|
|
||||||
class CustomFormat extends Component {
|
class CustomFormat extends Component {
|
||||||
@ -19,6 +20,7 @@ class CustomFormat extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isEditCustomFormatModalOpen: false,
|
isEditCustomFormatModalOpen: false,
|
||||||
|
isExportCustomFormatModalOpen: false,
|
||||||
isDeleteCustomFormatModalOpen: false
|
isDeleteCustomFormatModalOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -34,6 +36,14 @@ class CustomFormat extends Component {
|
|||||||
this.setState({ isEditCustomFormatModalOpen: false });
|
this.setState({ isEditCustomFormatModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onExportCustomFormatPress = () => {
|
||||||
|
this.setState({ isExportCustomFormatModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onExportCustomFormatModalClose = () => {
|
||||||
|
this.setState({ isExportCustomFormatModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
onDeleteCustomFormatPress = () => {
|
onDeleteCustomFormatPress = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditCustomFormatModalOpen: false,
|
isEditCustomFormatModalOpen: false,
|
||||||
@ -80,12 +90,21 @@ class CustomFormat extends Component {
|
|||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<IconButton
|
<div>
|
||||||
className={styles.cloneButton}
|
<IconButton
|
||||||
title={translate('CloneProfile')}
|
className={styles.cloneButton}
|
||||||
name={icons.CLONE}
|
title={translate('CloneProfile')}
|
||||||
onPress={this.onCloneCustomFormatPress}
|
name={icons.CLONE}
|
||||||
/>
|
onPress={this.onCloneCustomFormatPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.cloneButton}
|
||||||
|
title={translate('CloneProfile')}
|
||||||
|
name={icons.EXPORT}
|
||||||
|
onPress={this.onExportCustomFormatPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@ -122,6 +141,12 @@ class CustomFormat extends Component {
|
|||||||
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExportCustomFormatModal
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isExportCustomFormatModalOpen}
|
||||||
|
onModalClose={this.onExportCustomFormatModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
|
@ -16,6 +16,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import ImportCustomFormatModal from './ImportCustomFormatModal';
|
||||||
import AddSpecificationModal from './Specifications/AddSpecificationModal';
|
import AddSpecificationModal from './Specifications/AddSpecificationModal';
|
||||||
import EditSpecificationModalConnector from './Specifications/EditSpecificationModalConnector';
|
import EditSpecificationModalConnector from './Specifications/EditSpecificationModalConnector';
|
||||||
import Specification from './Specifications/Specification';
|
import Specification from './Specifications/Specification';
|
||||||
@ -31,7 +32,8 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isAddSpecificationModalOpen: false,
|
isAddSpecificationModalOpen: false,
|
||||||
isEditSpecificationModalOpen: false
|
isEditSpecificationModalOpen: false,
|
||||||
|
isImportCustomFormatModalOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,6 +55,14 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
this.setState({ isEditSpecificationModalOpen: false });
|
this.setState({ isEditSpecificationModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onImportPress = () => {
|
||||||
|
this.setState({ isImportCustomFormatModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportCustomFormatModalClose = () => {
|
||||||
|
this.setState({ isImportCustomFormatModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@ -76,7 +86,8 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
isAddSpecificationModalOpen,
|
isAddSpecificationModalOpen,
|
||||||
isEditSpecificationModalOpen
|
isEditSpecificationModalOpen,
|
||||||
|
isImportCustomFormatModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -176,6 +187,12 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
isOpen={isEditSpecificationModalOpen}
|
isOpen={isEditSpecificationModalOpen}
|
||||||
onModalClose={this.onEditSpecificationModalClose}
|
onModalClose={this.onEditSpecificationModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ImportCustomFormatModal
|
||||||
|
isOpen={isImportCustomFormatModalOpen}
|
||||||
|
onModalClose={this.onImportCustomFormatModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@ -192,6 +209,16 @@ class EditCustomFormatModalContent extends Component {
|
|||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!id &&
|
||||||
|
<Button
|
||||||
|
className={styles.deleteButton}
|
||||||
|
onPress={this.onImportPress}
|
||||||
|
>
|
||||||
|
{translate('Import')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={onModalClose}
|
onPress={onModalClose}
|
||||||
>
|
>
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import ExportCustomFormatModalContentConnector from './ExportCustomFormatModalContentConnector';
|
||||||
|
|
||||||
|
class ExportCustomFormatModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
height: 'auto'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onContentHeightChange = (height) => {
|
||||||
|
if (this.state.height === 'auto' || height > this.state.height) {
|
||||||
|
this.setState({ height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
style={{ height: `${this.state.height}px` }}
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ExportCustomFormatModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onContentHeightChange={this.onContentHeightChange}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportCustomFormatModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExportCustomFormatModal;
|
@ -0,0 +1,5 @@
|
|||||||
|
.button {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||||
|
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 styles from './ExportCustomFormatModalContent.css';
|
||||||
|
|
||||||
|
class ExportCustomFormatModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
json,
|
||||||
|
specificationsPopulated,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('ExportCustomFormat')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>
|
||||||
|
{translate('UnableToLoadCustomFormats')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error && specificationsPopulated &&
|
||||||
|
<div>
|
||||||
|
<pre>
|
||||||
|
{json}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<ClipboardButton
|
||||||
|
className={styles.button}
|
||||||
|
value={json}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Close')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportCustomFormatModalContent.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
json: PropTypes.string.isRequired,
|
||||||
|
specificationsPopulated: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExportCustomFormatModalContent;
|
@ -0,0 +1,83 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchCustomFormatSpecifications } from 'Store/Actions/settingsActions';
|
||||||
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import ExportCustomFormatModalContent from './ExportCustomFormatModalContent';
|
||||||
|
|
||||||
|
const blacklistedProperties = ['id', 'implementationName', 'infoLink'];
|
||||||
|
|
||||||
|
function replacer(key, value) {
|
||||||
|
if (blacklistedProperties.includes(key)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// provider fields
|
||||||
|
if (key === 'fields') {
|
||||||
|
return value.reduce((acc, cur) => {
|
||||||
|
acc[cur.name] = cur.value;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
// regular setting values
|
||||||
|
if (value.hasOwnProperty('value')) {
|
||||||
|
return value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
createProviderSettingsSelector('customFormats'),
|
||||||
|
(state) => state.settings.customFormatSpecifications,
|
||||||
|
(advancedSettings, customFormat, specifications) => {
|
||||||
|
const json = customFormat.item ? JSON.stringify(customFormat.item, replacer, 2) : '';
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
...customFormat,
|
||||||
|
json,
|
||||||
|
specificationsPopulated: specifications.isPopulated,
|
||||||
|
specifications: specifications.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchCustomFormatSpecifications
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExportCustomFormatModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
id
|
||||||
|
} = this.props;
|
||||||
|
this.props.fetchCustomFormatSpecifications({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ExportCustomFormatModalContent
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExportCustomFormatModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
fetchCustomFormatSpecifications: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ExportCustomFormatModalContentConnector);
|
@ -0,0 +1,61 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import ImportCustomFormatModalContentConnector from './ImportCustomFormatModalContentConnector';
|
||||||
|
|
||||||
|
class ImportCustomFormatModal extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
height: 'auto'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onContentHeightChange = (height) => {
|
||||||
|
if (this.state.height === 'auto' || height > this.state.height) {
|
||||||
|
this.setState({ height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
style={{ height: `${this.state.height}px` }}
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ImportCustomFormatModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onContentHeightChange={this.onContentHeightChange}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportCustomFormatModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportCustomFormatModal;
|
@ -0,0 +1,5 @@
|
|||||||
|
.input {
|
||||||
|
composes: input from '~Components/Form/TextArea.css';
|
||||||
|
|
||||||
|
font-family: $monoSpaceFontFamily;
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
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 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, sizes } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './ImportCustomFormatModalContent.css';
|
||||||
|
|
||||||
|
class ImportCustomFormatModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this._importTimeout = null;
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
json: '',
|
||||||
|
isSpinning: false,
|
||||||
|
parseError: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._importTimeout) {
|
||||||
|
clearTimeout(this._importTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
onChange = (event) => {
|
||||||
|
this.setState({ json: event.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportPress = () => {
|
||||||
|
this.setState({ isSpinning: true });
|
||||||
|
// this is a bodge as we need to register a isSpinning: true to get the spinner button to update
|
||||||
|
this._importTimeout = setTimeout(this.doImport, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
doImport = () => {
|
||||||
|
const parseError = this.props.onImportPress(this.state.json);
|
||||||
|
this.setState({
|
||||||
|
parseError,
|
||||||
|
isSpinning: false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parseError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
specificationsPopulated,
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
json,
|
||||||
|
isSpinning,
|
||||||
|
parseError
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('ImportCustomFormat')}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>
|
||||||
|
{translate('UnableToLoadCustomFormats')}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error && specificationsPopulated &&
|
||||||
|
<Form>
|
||||||
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('CustomFormatJSON')}
|
||||||
|
</FormLabel>
|
||||||
|
<FormInputGroup
|
||||||
|
key={0}
|
||||||
|
inputClassName={styles.input}
|
||||||
|
type={inputTypes.TEXT_AREA}
|
||||||
|
name="customFormatJson"
|
||||||
|
value={json}
|
||||||
|
onChange={this.onChange}
|
||||||
|
placeholder={'{\n "name": "Custom Format"\n}'}
|
||||||
|
errors={parseError ? [parseError] : []}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
{translate('Cancel')}
|
||||||
|
</Button>
|
||||||
|
<SpinnerErrorButton
|
||||||
|
onPress={this.onImportPress}
|
||||||
|
isSpinning={isSpinning}
|
||||||
|
error={parseError}
|
||||||
|
>
|
||||||
|
{translate('Import')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportCustomFormatModalContent.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
specificationsPopulated: PropTypes.bool.isRequired,
|
||||||
|
onImportPress: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportCustomFormatModalContent;
|
@ -0,0 +1,146 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
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 { clearCustomFormatSpecificationPending, deleteAllCustomFormatSpecification, fetchCustomFormatSpecificationSchema, saveCustomFormatSpecification, selectCustomFormatSpecificationSchema, setCustomFormatSpecificationFieldValue, setCustomFormatSpecificationValue, setCustomFormatValue } from 'Store/Actions/settingsActions';
|
||||||
|
import createProviderSettingsSelector from 'Store/Selectors/createProviderSettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import ImportCustomFormatModalContent from './ImportCustomFormatModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
createProviderSettingsSelector('customFormats'),
|
||||||
|
(state) => state.settings.customFormatSpecifications,
|
||||||
|
(advancedSettings, customFormat, specifications) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
...customFormat,
|
||||||
|
specificationsPopulated: specifications.isPopulated,
|
||||||
|
specificationSchema: specifications.schema
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
deleteAllCustomFormatSpecification,
|
||||||
|
clearCustomFormatSpecificationPending,
|
||||||
|
clearPendingChanges,
|
||||||
|
saveCustomFormatSpecification,
|
||||||
|
selectCustomFormatSpecificationSchema,
|
||||||
|
setCustomFormatSpecificationFieldValue,
|
||||||
|
setCustomFormatSpecificationValue,
|
||||||
|
setCustomFormatValue,
|
||||||
|
fetchCustomFormatSpecificationSchema
|
||||||
|
};
|
||||||
|
|
||||||
|
class ImportCustomFormatModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchCustomFormatSpecificationSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
clearPending = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'settings.customFormats' });
|
||||||
|
this.props.clearCustomFormatSpecificationPending();
|
||||||
|
this.props.deleteAllCustomFormatSpecification();
|
||||||
|
}
|
||||||
|
|
||||||
|
onImportPress = (payload) => {
|
||||||
|
|
||||||
|
this.clearPending();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const cf = JSON.parse(payload);
|
||||||
|
this.parseCf(cf);
|
||||||
|
} catch (err) {
|
||||||
|
this.clearPending();
|
||||||
|
return {
|
||||||
|
message: err.message,
|
||||||
|
detailedMessage: err.stack
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseCf = (cf) => {
|
||||||
|
for (const [key, value] of Object.entries(cf)) {
|
||||||
|
if (key === 'specifications') {
|
||||||
|
for (const spec of value) {
|
||||||
|
this.parseSpecification(spec);
|
||||||
|
}
|
||||||
|
} else if (key !== 'id') {
|
||||||
|
this.props.setCustomFormatValue({ name: key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSpecification = (spec) => {
|
||||||
|
const selectedImplementation = _.find(this.props.specificationSchema, { implementation: spec.implementation });
|
||||||
|
|
||||||
|
if (!selectedImplementation) {
|
||||||
|
throw new Error(translate('CustomFormatUnknownCondition', [spec.implementation]));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.selectCustomFormatSpecificationSchema({ implementation: spec.implementation });
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(spec)) {
|
||||||
|
if (key === 'fields') {
|
||||||
|
this.parseFields(value, selectedImplementation);
|
||||||
|
} else if (key !== 'id') {
|
||||||
|
this.props.setCustomFormatSpecificationValue({ name: key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.saveCustomFormatSpecification();
|
||||||
|
}
|
||||||
|
|
||||||
|
parseFields = (fields, schema) => {
|
||||||
|
for (const [key, value] of Object.entries(fields)) {
|
||||||
|
const field = _.find(schema.fields, { name: key });
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(translate('CustomFormatUnknownConditionOption', [key, schema.implementationName]));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.setCustomFormatSpecificationFieldValue({ name: key, value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<ImportCustomFormatModalContent
|
||||||
|
{...this.props}
|
||||||
|
onImportPress={this.onImportPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImportCustomFormatModalContentConnector.propTypes = {
|
||||||
|
specificationSchema: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired,
|
||||||
|
deleteAllCustomFormatSpecification: PropTypes.func.isRequired,
|
||||||
|
clearCustomFormatSpecificationPending: PropTypes.func.isRequired,
|
||||||
|
saveCustomFormatSpecification: PropTypes.func.isRequired,
|
||||||
|
fetchCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||||
|
selectCustomFormatSpecificationSchema: PropTypes.func.isRequired,
|
||||||
|
setCustomFormatSpecificationValue: PropTypes.func.isRequired,
|
||||||
|
setCustomFormatSpecificationFieldValue: PropTypes.func.isRequired,
|
||||||
|
setCustomFormatValue: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ImportCustomFormatModalContentConnector);
|
@ -27,6 +27,7 @@ export const SET_CUSTOM_FORMAT_SPECIFICATION_VALUE = 'settings/customFormatSpeci
|
|||||||
export const SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE = 'settings/customFormatSpecifications/setCustomFormatSpecificationFieldValue';
|
export const SET_CUSTOM_FORMAT_SPECIFICATION_FIELD_VALUE = 'settings/customFormatSpecifications/setCustomFormatSpecificationFieldValue';
|
||||||
export const SAVE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/saveCustomFormatSpecification';
|
export const SAVE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/saveCustomFormatSpecification';
|
||||||
export const DELETE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/deleteCustomFormatSpecification';
|
export const DELETE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/deleteCustomFormatSpecification';
|
||||||
|
export const DELETE_ALL_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/deleteCustomFormatSpecification';
|
||||||
export const CLONE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/cloneCustomFormatSpecification';
|
export const CLONE_CUSTOM_FORMAT_SPECIFICATION = 'settings/customFormatSpecifications/cloneCustomFormatSpecification';
|
||||||
export const CLEAR_CUSTOM_FORMAT_SPECIFICATIONS = 'settings/customFormatSpecifications/clearCustomFormatSpecifications';
|
export const CLEAR_CUSTOM_FORMAT_SPECIFICATIONS = 'settings/customFormatSpecifications/clearCustomFormatSpecifications';
|
||||||
export const CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING = 'settings/customFormatSpecifications/clearCustomFormatSpecificationPending';
|
export const CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING = 'settings/customFormatSpecifications/clearCustomFormatSpecificationPending';
|
||||||
@ -39,6 +40,7 @@ export const selectCustomFormatSpecificationSchema = createAction(SELECT_CUSTOM_
|
|||||||
|
|
||||||
export const saveCustomFormatSpecification = createThunk(SAVE_CUSTOM_FORMAT_SPECIFICATION);
|
export const saveCustomFormatSpecification = createThunk(SAVE_CUSTOM_FORMAT_SPECIFICATION);
|
||||||
export const deleteCustomFormatSpecification = createThunk(DELETE_CUSTOM_FORMAT_SPECIFICATION);
|
export const deleteCustomFormatSpecification = createThunk(DELETE_CUSTOM_FORMAT_SPECIFICATION);
|
||||||
|
export const deleteAllCustomFormatSpecification = createThunk(DELETE_ALL_CUSTOM_FORMAT_SPECIFICATION);
|
||||||
|
|
||||||
export const setCustomFormatSpecificationValue = createAction(SET_CUSTOM_FORMAT_SPECIFICATION_VALUE, (payload) => {
|
export const setCustomFormatSpecificationValue = createAction(SET_CUSTOM_FORMAT_SPECIFICATION_VALUE, (payload) => {
|
||||||
return {
|
return {
|
||||||
@ -137,6 +139,13 @@ export default {
|
|||||||
return dispatch(removeItem({ section, id }));
|
return dispatch(removeItem({ section, id }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[DELETE_ALL_CUSTOM_FORMAT_SPECIFICATION]: (getState, payload, dispatch) => {
|
||||||
|
return dispatch(set({
|
||||||
|
section,
|
||||||
|
items: []
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
[CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
|
[CLEAR_CUSTOM_FORMAT_SPECIFICATION_PENDING]: (getState, payload, dispatch) => {
|
||||||
return dispatch(set({
|
return dispatch(set({
|
||||||
section,
|
section,
|
||||||
|
@ -115,6 +115,9 @@
|
|||||||
"CreateGroup": "Create group",
|
"CreateGroup": "Create group",
|
||||||
"Crew": "Crew",
|
"Crew": "Crew",
|
||||||
"CustomFilters": "Custom Filters",
|
"CustomFilters": "Custom Filters",
|
||||||
|
"CustomFormatJSON": "Custom Format JSON",
|
||||||
|
"CustomFormatUnknownCondition": "Unknown Custom Format condition '{0}'",
|
||||||
|
"CustomFormatUnknownConditionOption": "Unknown option '{0}' for condition '{1}'",
|
||||||
"CustomFormats": "Custom Formats",
|
"CustomFormats": "Custom Formats",
|
||||||
"CustomFormatScore": "Custom Format score",
|
"CustomFormatScore": "Custom Format score",
|
||||||
"CustomFormatsSettings": "Custom Formats Settings",
|
"CustomFormatsSettings": "Custom Formats Settings",
|
||||||
@ -219,6 +222,7 @@
|
|||||||
"ExcludeMovie": "Exclude Movie",
|
"ExcludeMovie": "Exclude Movie",
|
||||||
"ExistingMovies": "Existing Movie(s)",
|
"ExistingMovies": "Existing Movie(s)",
|
||||||
"ExistingTag": "Existing tag",
|
"ExistingTag": "Existing tag",
|
||||||
|
"ExportCustomFormat": "Export Custom Format",
|
||||||
"Extension": "Extension",
|
"Extension": "Extension",
|
||||||
"ExtraFileExtensionsHelpTexts1": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)",
|
"ExtraFileExtensionsHelpTexts1": "Comma separated list of extra files to import (.nfo will be imported as .nfo-orig)",
|
||||||
"ExtraFileExtensionsHelpTexts2": "Examples: '.sub, .nfo' or 'sub,nfo'",
|
"ExtraFileExtensionsHelpTexts2": "Examples: '.sub, .nfo' or 'sub,nfo'",
|
||||||
@ -281,6 +285,7 @@
|
|||||||
"IgnoredPlaceHolder": "Add new restriction",
|
"IgnoredPlaceHolder": "Add new restriction",
|
||||||
"IllRestartLater": "I'll restart later",
|
"IllRestartLater": "I'll restart later",
|
||||||
"Import": "Import",
|
"Import": "Import",
|
||||||
|
"ImportCustomFormat": "Import Custom Format",
|
||||||
"Imported": "Imported",
|
"Imported": "Imported",
|
||||||
"ImportedTo": "Imported To",
|
"ImportedTo": "Imported To",
|
||||||
"ImportExistingMovies": "Import Existing Movies",
|
"ImportExistingMovies": "Import Existing Movies",
|
||||||
|
Loading…
Reference in New Issue
Block a user