mirror of
https://github.com/Radarr/Radarr.git
synced 2024-09-11 03:52:33 +02:00
New: Get Custom Formats Working in Aphrodite
This commit is contained in:
parent
86dde88fe6
commit
b2268c7452
@ -1,3 +1,4 @@
|
|||||||
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
||||||
|
export const QUALITY_PROFILE_FORMAT_ITEM = 'qualityProfileFormatItem';
|
||||||
export const DELAY_PROFILE = 'delayProfile';
|
export const DELAY_PROFILE = 'delayProfile';
|
||||||
export const TABLE_COLUMN = 'tableColumn';
|
export const TABLE_COLUMN = 'tableColumn';
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import split from 'Utilities/String/split';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import Card from 'Components/Card';
|
import Card from 'Components/Card';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||||
import styles from './CustomFormat.css';
|
import styles from './CustomFormat.css';
|
||||||
|
|
||||||
class CustomFormat extends Component {
|
class CustomFormat extends Component {
|
||||||
@ -62,15 +63,15 @@ class CustomFormat extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
// id,
|
id,
|
||||||
name,
|
name,
|
||||||
items,
|
formatTags,
|
||||||
isDeleting
|
isDeleting
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
className={styles.CustomFormat}
|
className={styles.customFormat}
|
||||||
overlayContent={true}
|
overlayContent={true}
|
||||||
onPress={this.onEditCustomFormatPress}
|
onPress={this.onEditCustomFormatPress}
|
||||||
>
|
>
|
||||||
@ -87,32 +88,31 @@ class CustomFormat extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.formats}>
|
<div>
|
||||||
{
|
{
|
||||||
items.map((item) => {
|
split(formatTags).map((item) => {
|
||||||
if (!item.allowed) {
|
if (!item) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
key={item.quality.id}
|
key={item}
|
||||||
kind={kinds.default}
|
kind={kinds.DEFAULT}
|
||||||
title={null}
|
|
||||||
>
|
>
|
||||||
{item.quality.name}
|
{item}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <EditCustomFormatModalConnector
|
<EditCustomFormatModalConnector
|
||||||
id={id}
|
id={id}
|
||||||
isOpen={this.state.isEditCustomFormatModalOpen}
|
isOpen={this.state.isEditCustomFormatModalOpen}
|
||||||
onModalClose={this.onEditCustomFormatModalClose}
|
onModalClose={this.onEditCustomFormatModalClose}
|
||||||
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||||
/> */}
|
/>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
@ -132,7 +132,7 @@ class CustomFormat extends Component {
|
|||||||
CustomFormat.propTypes = {
|
CustomFormat.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
formatTags: PropTypes.string.isRequired,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||||
|
@ -7,7 +7,7 @@ import Card from 'Components/Card';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
import CustomFormat from './CustomFormat';
|
import CustomFormat from './CustomFormat';
|
||||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||||
import styles from './CustomFormats.css';
|
import styles from './CustomFormats.css';
|
||||||
|
|
||||||
class CustomFormats extends Component {
|
class CustomFormats extends Component {
|
||||||
@ -57,7 +57,7 @@ class CustomFormats extends Component {
|
|||||||
errorMessage="Unable to load Custom Formats"
|
errorMessage="Unable to load Custom Formats"
|
||||||
{...otherProps}c={true}
|
{...otherProps}c={true}
|
||||||
>
|
>
|
||||||
<div className={styles.CustomFormats}>
|
<div className={styles.customFormats}>
|
||||||
{
|
{
|
||||||
items.sort(sortByName).map((item) => {
|
items.sort(sortByName).map((item) => {
|
||||||
return (
|
return (
|
||||||
@ -85,11 +85,10 @@ class CustomFormats extends Component {
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*
|
|
||||||
<EditCustomFormatModalConnector
|
<EditCustomFormatModalConnector
|
||||||
isOpen={this.state.isCustomFormatModalOpen}
|
isOpen={this.state.isCustomFormatModalOpen}
|
||||||
onModalClose={this.onModalClose}
|
onModalClose={this.onModalClose}
|
||||||
/> */}
|
/>
|
||||||
|
|
||||||
</PageSectionContent>
|
</PageSectionContent>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||||
|
|
||||||
|
class EditCustomFormatModal 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.EXTRA_LARGE}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<EditCustomFormatModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onContentHeightChange={this.onContentHeightChange}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCustomFormatModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCustomFormatModal;
|
@ -0,0 +1,43 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||||
|
|
||||||
|
function mapStateToProps() {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
clearPendingChanges
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCustomFormatModalConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.props.clearPendingChanges({ section: 'settings.customFormats' });
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCustomFormatModal
|
||||||
|
{...this.props}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCustomFormatModalConnector.propTypes = {
|
||||||
|
onModalClose: PropTypes.func.isRequired,
|
||||||
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(EditCustomFormatModalConnector);
|
@ -0,0 +1,18 @@
|
|||||||
|
.formGroupsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroupWrapper {
|
||||||
|
flex: 0 0 calc($formGroupSmallWidth - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteButtonContainer {
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
.formGroupsContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes, sizes } 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 styles from './EditCustomFormatModalContent.css';
|
||||||
|
|
||||||
|
class EditCustomFormatModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
item,
|
||||||
|
onInputChange,
|
||||||
|
onSavePress,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
formatTags
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit Custom Format' : 'Add Custom Format'}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to add a new custom format, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error &&
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.formGroupsContainer}>
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
Name
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
Format Tags
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT_TAG}
|
||||||
|
name="formatTags"
|
||||||
|
{...formatTags}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCustomFormatModalContent.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onContentHeightChange: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditCustomFormatModalContent;
|
@ -0,0 +1,74 @@
|
|||||||
|
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 { setCustomFormatValue, saveCustomFormat } from 'Store/Actions/settingsActions';
|
||||||
|
import EditCustomFormatModalContent from './EditCustomFormatModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.advancedSettings,
|
||||||
|
createProviderSettingsSelector('customFormats'),
|
||||||
|
(advancedSettings, customFormat) => {
|
||||||
|
return {
|
||||||
|
advancedSettings,
|
||||||
|
...customFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
setCustomFormatValue,
|
||||||
|
saveCustomFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
class EditCustomFormatModalContentConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||||
|
this.props.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.props.setCustomFormatValue({ name, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSavePress = () => {
|
||||||
|
this.props.saveCustomFormat({ id: this.props.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EditCustomFormatModalContent
|
||||||
|
{...this.props}
|
||||||
|
onSavePress={this.onSavePress}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EditCustomFormatModalContentConnector.propTypes = {
|
||||||
|
id: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
saveError: PropTypes.object,
|
||||||
|
item: PropTypes.object.isRequired,
|
||||||
|
setCustomFormatValue: PropTypes.func.isRequired,
|
||||||
|
saveCustomFormat: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EditCustomFormatModalContentConnector);
|
@ -15,6 +15,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import QualityProfileItems from './QualityProfileItems';
|
import QualityProfileItems from './QualityProfileItems';
|
||||||
|
import QualityProfileFormatItems from './QualityProfileFormatItems';
|
||||||
import styles from './EditQualityProfileModalContent.css';
|
import styles from './EditQualityProfileModalContent.css';
|
||||||
|
|
||||||
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||||
@ -92,6 +93,7 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
qualities,
|
qualities,
|
||||||
|
customFormats,
|
||||||
languages,
|
languages,
|
||||||
item,
|
item,
|
||||||
isInUse,
|
isInUse,
|
||||||
@ -109,11 +111,13 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
name,
|
name,
|
||||||
upgradeAllowed,
|
upgradeAllowed,
|
||||||
cutoff,
|
cutoff,
|
||||||
|
formatCutoff,
|
||||||
language,
|
language,
|
||||||
items
|
items,
|
||||||
|
formatItems
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
const languageId = language.value.id;
|
const languageId = language ? language.value.id : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
@ -181,7 +185,7 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
upgradeAllowed.value &&
|
upgradeAllowed.value &&
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
<FormLabel size={sizes.SMALL}>
|
<FormLabel size={sizes.SMALL}>
|
||||||
Upgrade Until
|
Upgrade Until Quality
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
@ -195,6 +199,24 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
upgradeAllowed.value &&
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
Upgrade Until Format
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="formatCutoff"
|
||||||
|
{...formatCutoff}
|
||||||
|
values={customFormats}
|
||||||
|
helpText="Once this custom format is reached Radarr will no longer download movies"
|
||||||
|
onChange={onCutoffChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
}
|
||||||
|
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
<FormLabel size={sizes.SMALL}>
|
<FormLabel size={sizes.SMALL}>
|
||||||
Language
|
Language
|
||||||
@ -220,6 +242,15 @@ class EditQualityProfileModalContent extends Component {
|
|||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<QualityProfileFormatItems
|
||||||
|
profileFormatItems={formatItems.value}
|
||||||
|
errors={formatItems.errors}
|
||||||
|
warnings={formatItems.warnings}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
@ -282,6 +313,7 @@ EditQualityProfileModalContent.propTypes = {
|
|||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
saveError: PropTypes.object,
|
saveError: PropTypes.object,
|
||||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
isInUse: PropTypes.bool.isRequired,
|
isInUse: PropTypes.bool.isRequired,
|
||||||
|
@ -61,6 +61,36 @@ function createQualitiesSelector() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createFormatsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createProviderSettingsSelector('qualityProfiles'),
|
||||||
|
(customFormat) => {
|
||||||
|
const items = customFormat.item.formatItems;
|
||||||
|
if (!items || !items.value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.reduceRight(items.value, (result, { allowed, id, name, format }) => {
|
||||||
|
if (allowed) {
|
||||||
|
if (id) {
|
||||||
|
result.push({
|
||||||
|
key: id,
|
||||||
|
value: name
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
key: format.id,
|
||||||
|
value: format.name
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createLanguagesSelector() {
|
function createLanguagesSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.languages,
|
(state) => state.settings.languages,
|
||||||
@ -87,11 +117,13 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
createProviderSettingsSelector('qualityProfiles'),
|
createProviderSettingsSelector('qualityProfiles'),
|
||||||
createQualitiesSelector(),
|
createQualitiesSelector(),
|
||||||
|
createFormatsSelector(),
|
||||||
createLanguagesSelector(),
|
createLanguagesSelector(),
|
||||||
createProfileInUseSelector('qualityProfileId'),
|
createProfileInUseSelector('qualityProfileId'),
|
||||||
(qualityProfile, qualities, languages, isInUse) => {
|
(qualityProfile, qualities, customFormats, languages, isInUse) => {
|
||||||
return {
|
return {
|
||||||
qualities,
|
qualities,
|
||||||
|
customFormats,
|
||||||
languages,
|
languages,
|
||||||
...qualityProfile,
|
...qualityProfile,
|
||||||
isInUse
|
isInUse
|
||||||
@ -161,6 +193,30 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ensureFormatCutoff = (qualityProfile) => {
|
||||||
|
const cutoff = qualityProfile.formatCutoff.value;
|
||||||
|
|
||||||
|
const cutoffItem = _.find(qualityProfile.formatItems.value, (i) => {
|
||||||
|
if (!cutoff) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.id === cutoff || (i.format && i.format.id === cutoff);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
||||||
|
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
|
||||||
|
const firstAllowed = _.find(qualityProfile.formatItems.value, { allowed: true });
|
||||||
|
let cutoffId = null;
|
||||||
|
|
||||||
|
if (firstAllowed) {
|
||||||
|
cutoffId = firstAllowed.format ? firstAllowed.format.id : firstAllowed.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({ name: 'formatCutoff', value: cutoffId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
@ -211,6 +267,21 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||||||
this.ensureCutoff(qualityProfile);
|
this.ensureCutoff(qualityProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onQualityProfileFormatItemAllowedChange = (id, allowed) => {
|
||||||
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const formatItems = qualityProfile.formatItems.value;
|
||||||
|
const item = _.find(qualityProfile.formatItems.value, (i) => i.format && i.format.id === id);
|
||||||
|
|
||||||
|
item.allowed = allowed;
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'formatItems',
|
||||||
|
value: formatItems
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ensureFormatCutoff(qualityProfile);
|
||||||
|
}
|
||||||
|
|
||||||
onItemGroupAllowedChange = (id, allowed) => {
|
onItemGroupAllowedChange = (id, allowed) => {
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
const items = qualityProfile.items.value;
|
const items = qualityProfile.items.value;
|
||||||
@ -427,6 +498,39 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onQualityProfileFormatItemDragMove = (dragIndex, dropIndex) => {
|
||||||
|
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
|
||||||
|
this.setState({
|
||||||
|
dragIndex,
|
||||||
|
dropIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onQualityProfileFormatItemDragEnd = ({ id }, didDrop) => {
|
||||||
|
const {
|
||||||
|
dragIndex,
|
||||||
|
dropIndex
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (didDrop && dropIndex !== null) {
|
||||||
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
|
||||||
|
const formats = qualityProfile.formatItems.value.splice(dragIndex, 1);
|
||||||
|
qualityProfile.formatItems.value.splice(dropIndex, 0, formats[0]);
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'formatItems',
|
||||||
|
value: qualityProfile.formatItems.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
dragIndex: null,
|
||||||
|
dropIndex: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onToggleEditGroupsMode = () => {
|
onToggleEditGroupsMode = () => {
|
||||||
this.setState({ editGroups: !this.state.editGroups });
|
this.setState({ editGroups: !this.state.editGroups });
|
||||||
}
|
}
|
||||||
@ -450,10 +554,13 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||||||
onCreateGroupPress={this.onCreateGroupPress}
|
onCreateGroupPress={this.onCreateGroupPress}
|
||||||
onDeleteGroupPress={this.onDeleteGroupPress}
|
onDeleteGroupPress={this.onDeleteGroupPress}
|
||||||
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
||||||
|
onQualityProfileFormatItemAllowedChange={this.onQualityProfileFormatItemAllowedChange}
|
||||||
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
|
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
|
||||||
onItemGroupNameChange={this.onItemGroupNameChange}
|
onItemGroupNameChange={this.onItemGroupNameChange}
|
||||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||||
|
onQualityProfileFormatItemDragMove={this.onQualityProfileFormatItemDragMove}
|
||||||
|
onQualityProfileFormatItemDragEnd={this.onQualityProfileFormatItemDragEnd}
|
||||||
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
.qualityProfileFormatItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formatName {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 36px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
width: $dragHandleWidth;
|
||||||
|
text-align: center;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragIcon {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDragging {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import styles from './QualityProfileFormatItem.css';
|
||||||
|
|
||||||
|
class QualityProfileFormatItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAllowedChange = ({ value }) => {
|
||||||
|
const {
|
||||||
|
formatId,
|
||||||
|
onQualityProfileFormatItemAllowedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onQualityProfileFormatItemAllowedChange(formatId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
isDragging,
|
||||||
|
connectDragSource
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileFormatItem,
|
||||||
|
isDragging && styles.isDragging
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className={styles.formatName}
|
||||||
|
>
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.checkContainer}
|
||||||
|
name={name}
|
||||||
|
value={allowed}
|
||||||
|
onChange={this.onAllowedChange}
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<div className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
name={icons.REORDER}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileFormatItem.propTypes = {
|
||||||
|
formatId: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
sortIndex: PropTypes.number.isRequired,
|
||||||
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
onQualityProfileFormatItemAllowedChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
QualityProfileFormatItem.defaultProps = {
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QualityProfileFormatItem;
|
@ -0,0 +1,4 @@
|
|||||||
|
.dragPreview {
|
||||||
|
width: 380px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
@ -0,0 +1,88 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { DragLayer } from 'react-dnd';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions.js';
|
||||||
|
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||||
|
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||||
|
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||||
|
import styles from './QualityProfileFormatItemDragPreview.css';
|
||||||
|
|
||||||
|
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||||
|
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
||||||
|
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||||
|
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||||
|
|
||||||
|
function collectDragLayer(monitor) {
|
||||||
|
return {
|
||||||
|
item: monitor.getItem(),
|
||||||
|
itemType: monitor.getItemType(),
|
||||||
|
currentOffset: monitor.getSourceClientOffset()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class QualityProfileFormatItemDragPreview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
item,
|
||||||
|
itemType,
|
||||||
|
currentOffset
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!currentOffset || itemType !== QUALITY_PROFILE_FORMAT_ITEM) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The offset is shifted because the drag handle is on the right edge of the
|
||||||
|
// list item and the preview is wider than the drag handle.
|
||||||
|
|
||||||
|
const { x, y } = currentOffset;
|
||||||
|
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||||
|
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
WebkitTransform: transform,
|
||||||
|
msTransform: transform,
|
||||||
|
transform
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
formatId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
sortIndex
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragPreviewLayer>
|
||||||
|
<div
|
||||||
|
className={styles.dragPreview}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<QualityProfileFormatItem
|
||||||
|
formatId={formatId}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
sortIndex={sortIndex}
|
||||||
|
isDragging={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DragPreviewLayer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileFormatItemDragPreview.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
itemType: PropTypes.string,
|
||||||
|
currentOffset: PropTypes.shape({
|
||||||
|
x: PropTypes.number.isRequired,
|
||||||
|
y: PropTypes.number.isRequired
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragLayer(collectDragLayer)(QualityProfileFormatItemDragPreview);
|
@ -0,0 +1,18 @@
|
|||||||
|
.qualityProfileFormatItemDragSource {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfileFormatItemPlaceholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 1px dotted #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfileFormatItemPlaceholderBefore {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfileFormatItemPlaceholderAfter {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
@ -0,0 +1,157 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { findDOMNode } from 'react-dom';
|
||||||
|
import { DragSource, DropTarget } from 'react-dnd';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { QUALITY_PROFILE_FORMAT_ITEM } from 'Helpers/dragTypes';
|
||||||
|
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||||
|
import styles from './QualityProfileFormatItemDragSource.css';
|
||||||
|
|
||||||
|
const qualityProfileFormatItemDragSource = {
|
||||||
|
beginDrag({ formatId, name, allowed, sortIndex }) {
|
||||||
|
return {
|
||||||
|
formatId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
sortIndex
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag(props, monitor, component) {
|
||||||
|
props.onQualityProfileFormatItemDragEnd(monitor.getItem(), monitor.didDrop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const qualityProfileFormatItemDropTarget = {
|
||||||
|
hover(props, monitor, component) {
|
||||||
|
const dragIndex = monitor.getItem().sortIndex;
|
||||||
|
const hoverIndex = props.sortIndex;
|
||||||
|
|
||||||
|
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// Moving up, only trigger if drag position is above 50%
|
||||||
|
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moving down, only trigger if drag position is below 50%
|
||||||
|
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onQualityProfileFormatItemDragMove(dragIndex, hoverIndex);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function collectDragSource(connect, monitor) {
|
||||||
|
return {
|
||||||
|
connectDragSource: connect.dragSource(),
|
||||||
|
isDragging: monitor.isDragging()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDropTarget(connect, monitor) {
|
||||||
|
return {
|
||||||
|
connectDropTarget: connect.dropTarget(),
|
||||||
|
isOver: monitor.isOver()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class QualityProfileFormatItemDragSource extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
formatId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
sortIndex,
|
||||||
|
isDragging,
|
||||||
|
isDraggingUp,
|
||||||
|
isDraggingDown,
|
||||||
|
isOver,
|
||||||
|
connectDragSource,
|
||||||
|
connectDropTarget,
|
||||||
|
onQualityProfileFormatItemAllowedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const isBefore = !isDragging && isDraggingUp && isOver;
|
||||||
|
const isAfter = !isDragging && isDraggingDown && isOver;
|
||||||
|
|
||||||
|
// if (isDragging && !isOver) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
return connectDropTarget(
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileFormatItemDragSource,
|
||||||
|
isBefore && styles.isDraggingUp,
|
||||||
|
isAfter && styles.isDraggingDown
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isBefore &&
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileFormatItemPlaceholder,
|
||||||
|
styles.qualityProfileFormatItemPlaceholderBefore
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<QualityProfileFormatItem
|
||||||
|
formatId={formatId}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
sortIndex={sortIndex}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isOver={isOver}
|
||||||
|
connectDragSource={connectDragSource}
|
||||||
|
onQualityProfileFormatItemAllowedChange={onQualityProfileFormatItemAllowedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
isAfter &&
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileFormatItemPlaceholder,
|
||||||
|
styles.qualityProfileFormatItemPlaceholderAfter
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileFormatItemDragSource.propTypes = {
|
||||||
|
formatId: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
sortIndex: PropTypes.number.isRequired,
|
||||||
|
isDragging: PropTypes.bool,
|
||||||
|
isDraggingUp: PropTypes.bool,
|
||||||
|
isDraggingDown: PropTypes.bool,
|
||||||
|
isOver: PropTypes.bool,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
connectDropTarget: PropTypes.func,
|
||||||
|
onQualityProfileFormatItemAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileFormatItemDragMove: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileFormatItemDragEnd: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropTarget(
|
||||||
|
QUALITY_PROFILE_FORMAT_ITEM,
|
||||||
|
qualityProfileFormatItemDropTarget,
|
||||||
|
collectDropTarget
|
||||||
|
)(DragSource(
|
||||||
|
QUALITY_PROFILE_FORMAT_ITEM,
|
||||||
|
qualityProfileFormatItemDragSource,
|
||||||
|
collectDragSource
|
||||||
|
)(QualityProfileFormatItemDragSource));
|
@ -0,0 +1,6 @@
|
|||||||
|
.formats {
|
||||||
|
margin-top: 10px;
|
||||||
|
/* TODO: This should consider the number of languages in the list */
|
||||||
|
min-height: 550px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import QualityProfileFormatItemDragSource from './QualityProfileFormatItemDragSource';
|
||||||
|
import QualityProfileFormatItemDragPreview from './QualityProfileFormatItemDragPreview';
|
||||||
|
import styles from './QualityProfileFormatItems.css';
|
||||||
|
|
||||||
|
class QualityProfileFormatItems extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dragIndex,
|
||||||
|
dropIndex,
|
||||||
|
profileFormatItems,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const isDragging = dropIndex !== null;
|
||||||
|
const isDraggingUp = isDragging && dropIndex > dragIndex;
|
||||||
|
const isDraggingDown = isDragging && dropIndex < dragIndex;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Custom Formats</FormLabel>
|
||||||
|
<div>
|
||||||
|
<FormInputHelpText
|
||||||
|
text="Custom Formats higher in the list are more preferred. Only checked custom formats are wanted"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
errors.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.message}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
warnings.map((warning, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.message}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.formats}>
|
||||||
|
{
|
||||||
|
profileFormatItems.map(({ allowed, format }, index) => {
|
||||||
|
return (
|
||||||
|
<QualityProfileFormatItemDragSource
|
||||||
|
key={format.id}
|
||||||
|
formatId={format.id}
|
||||||
|
name={format.name}
|
||||||
|
allowed={allowed}
|
||||||
|
sortIndex={index}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
<QualityProfileFormatItemDragPreview />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileFormatItems.propTypes = {
|
||||||
|
dragIndex: PropTypes.number,
|
||||||
|
dropIndex: PropTypes.number,
|
||||||
|
profileFormatItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
warnings: PropTypes.arrayOf(PropTypes.object)
|
||||||
|
};
|
||||||
|
|
||||||
|
QualityProfileFormatItems.defaultProps = {
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QualityProfileFormatItems;
|
@ -6,6 +6,7 @@
|
|||||||
"build": "gulp build",
|
"build": "gulp build",
|
||||||
"start": "gulp watch",
|
"start": "gulp watch",
|
||||||
"watch": "gulp watch",
|
"watch": "gulp watch",
|
||||||
|
"clean": "git clean -fXd",
|
||||||
"lint": "esprint check",
|
"lint": "esprint check",
|
||||||
"lint-fix": "eslint start --fix",
|
"lint-fix": "eslint start --fix",
|
||||||
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
||||||
@ -53,7 +54,7 @@
|
|||||||
"eslint": "6.0.1",
|
"eslint": "6.0.1",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-react": "7.14.2",
|
"eslint-plugin-react": "7.14.2",
|
||||||
"esprint": "0.4.0",
|
"esprint": "0.5.0",
|
||||||
"file-loader": "4.0.0",
|
"file-loader": "4.0.0",
|
||||||
"filesize": "4.1.2",
|
"filesize": "4.1.2",
|
||||||
"fuse.js": "3.4.5",
|
"fuse.js": "3.4.5",
|
||||||
|
@ -51,6 +51,17 @@ public static ProfileResource ToResource(this Profile model)
|
|||||||
? cutoffItem.Quality
|
? cutoffItem.Quality
|
||||||
: cutoffItem.Items.First().Quality;
|
: cutoffItem.Items.First().Quality;
|
||||||
|
|
||||||
|
var formatCutoffItem = model.FormatItems.First(q =>
|
||||||
|
{
|
||||||
|
if (q.Id == model.FormatCutoff) return true;
|
||||||
|
|
||||||
|
if (q.Format == null) return false;
|
||||||
|
|
||||||
|
return q.Format.Id == model.FormatCutoff;
|
||||||
|
});
|
||||||
|
|
||||||
|
var formatCutoff = formatCutoffItem.Format;
|
||||||
|
|
||||||
return new ProfileResource
|
return new ProfileResource
|
||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
@ -74,7 +85,7 @@ public static ProfileResource ToResource(this Profile model)
|
|||||||
|
|
||||||
return new List<ProfileQualityItemResource> { ToResource(i) };
|
return new List<ProfileQualityItemResource> { ToResource(i) };
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
FormatCutoff = model.FormatCutoff.ToResource(),
|
FormatCutoff = formatCutoff.ToResource(),
|
||||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||||
Language = model.Language
|
Language = model.Language
|
||||||
};
|
};
|
||||||
@ -112,7 +123,7 @@ public static Profile ToModel(this ProfileResource resource)
|
|||||||
Cutoff = resource.Cutoff.Id,
|
Cutoff = resource.Cutoff.Id,
|
||||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||||
Items = resource.Items.ConvertAll(ToModel),
|
Items = resource.Items.ConvertAll(ToModel),
|
||||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
FormatCutoff = resource.FormatCutoff.ToModel().Id,
|
||||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||||
Language = resource.Language
|
Language = resource.Language
|
||||||
};
|
};
|
||||||
|
@ -45,7 +45,7 @@ private List<ProfileResource> GetAll()
|
|||||||
var profile = new Profile();
|
var profile = new Profile();
|
||||||
profile.Cutoff = Quality.Unknown.Id;
|
profile.Cutoff = Quality.Unknown.Id;
|
||||||
profile.Items = items;
|
profile.Items = items;
|
||||||
profile.FormatCutoff = CustomFormat.None;
|
profile.FormatCutoff = CustomFormat.None.Id;
|
||||||
profile.FormatItems = formatItems;
|
profile.FormatItems = formatItems;
|
||||||
profile.Language = Language.English;
|
profile.Language = Language.English;
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ public void should_return_false_if_custom_formats_is_met_and_quality_and_format_
|
|||||||
{
|
{
|
||||||
Cutoff = Quality.HDTV720p.Id,
|
Cutoff = Quality.HDTV720p.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
||||||
}, old, newQ).Should().BeFalse();
|
}, old, newQ).Should().BeFalse();
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ public void should_lazyload_quality_profile()
|
|||||||
{
|
{
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||||
Cutoff = Quality.Bluray1080p.Id,
|
Cutoff = Quality.Bluray1080p.Id,
|
||||||
Name = "TestProfile"
|
Name = "TestProfile"
|
||||||
};
|
};
|
||||||
|
@ -20,7 +20,7 @@ public void should_be_able_to_read_and_write()
|
|||||||
var profile = new Profile
|
var profile = new Profile
|
||||||
{
|
{
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||||
Cutoff = Quality.Bluray1080p.Id,
|
Cutoff = Quality.Bluray1080p.Id,
|
||||||
Name = "TestProfile"
|
Name = "TestProfile"
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Movies;
|
using NzbDrone.Core.Movies;
|
||||||
using NzbDrone.Core.NetImport;
|
using NzbDrone.Core.NetImport;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.Profiles
|
namespace NzbDrone.Core.Test.Profiles
|
||||||
{
|
{
|
||||||
@ -17,6 +19,10 @@ public class ProfileServiceFixture : CoreTest<ProfileService>
|
|||||||
[Test]
|
[Test]
|
||||||
public void init_should_add_default_profiles()
|
public void init_should_add_default_profiles()
|
||||||
{
|
{
|
||||||
|
Mocker.GetMock<ICustomFormatService>()
|
||||||
|
.Setup(s => s.All())
|
||||||
|
.Returns(new List<CustomFormats.CustomFormat>());
|
||||||
|
|
||||||
Subject.Handle(new ApplicationStartedEvent());
|
Subject.Handle(new ApplicationStartedEvent());
|
||||||
|
|
||||||
Mocker.GetMock<IProfileRepository>()
|
Mocker.GetMock<IProfileRepository>()
|
||||||
|
@ -17,7 +17,7 @@ public Profile()
|
|||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public int Cutoff { get; set; }
|
public int Cutoff { get; set; }
|
||||||
public List<ProfileQualityItem> Items { get; set; }
|
public List<ProfileQualityItem> Items { get; set; }
|
||||||
public CustomFormat FormatCutoff { get; set; }
|
public int FormatCutoff { get; set; }
|
||||||
public List<ProfileFormatItem> FormatItems { get; set; }
|
public List<ProfileFormatItem> FormatItems { get; set; }
|
||||||
public List<string> PreferredTags { get; set; }
|
public List<string> PreferredTags { get; set; }
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using NzbDrone.Core.CustomFormats;
|
using Newtonsoft.Json;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Profiles
|
namespace NzbDrone.Core.Profiles
|
||||||
{
|
{
|
||||||
public class ProfileFormatItem : IEmbeddedDocument
|
public class ProfileFormatItem : IEmbeddedDocument
|
||||||
{
|
{
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
public int Id { get; set; }
|
||||||
public CustomFormat Format { get; set; }
|
public CustomFormat Format { get; set; }
|
||||||
public bool Allowed { get; set; }
|
public bool Allowed { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -74,9 +74,9 @@ public void DeleteCustomFormat(int formatId)
|
|||||||
foreach (var profile in all)
|
foreach (var profile in all)
|
||||||
{
|
{
|
||||||
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != formatId).ToList();
|
profile.FormatItems = profile.FormatItems.Where(c => c.Format.Id != formatId).ToList();
|
||||||
if (profile.FormatCutoff.Id == formatId)
|
if (profile.FormatCutoff == formatId)
|
||||||
{
|
{
|
||||||
profile.FormatCutoff = CustomFormat.None;
|
profile.FormatCutoff = CustomFormat.None.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
Update(profile);
|
Update(profile);
|
||||||
@ -187,7 +187,9 @@ public void Handle(ApplicationStartedEvent message)
|
|||||||
public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed)
|
public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed)
|
||||||
{
|
{
|
||||||
var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight);
|
var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight);
|
||||||
|
var formats = _formatService.All();
|
||||||
var items = new List<ProfileQualityItem>();
|
var items = new List<ProfileQualityItem>();
|
||||||
|
var formatItems = new List<ProfileFormatItem>();
|
||||||
var groupId = 1000;
|
var groupId = 1000;
|
||||||
var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id;
|
var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id;
|
||||||
|
|
||||||
@ -223,23 +225,36 @@ public Profile GetDefaultProfile(string name, Quality cutoff = null, params Qual
|
|||||||
groupId++;
|
groupId++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var format in formats)
|
||||||
|
{
|
||||||
|
formatItems.Add(new ProfileFormatItem
|
||||||
|
{
|
||||||
|
Id = format.Id,
|
||||||
|
Format = format,
|
||||||
|
Allowed = false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var qualityProfile = new Profile
|
var qualityProfile = new Profile
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
Cutoff = profileCutoff,
|
Cutoff = profileCutoff,
|
||||||
Items = items,
|
Items = items,
|
||||||
Language = Language.English,
|
Language = Language.English,
|
||||||
FormatCutoff = CustomFormat.None,
|
FormatCutoff = CustomFormat.None.Id,
|
||||||
FormatItems = new List<ProfileFormatItem>
|
FormatItems = new List<ProfileFormatItem>
|
||||||
{
|
{
|
||||||
new ProfileFormatItem
|
new ProfileFormatItem
|
||||||
{
|
{
|
||||||
|
Id = 0,
|
||||||
Allowed = true,
|
Allowed = true,
|
||||||
Format = CustomFormat.None
|
Format = CustomFormat.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
qualityProfile.FormatItems.AddRange(formatItems);
|
||||||
|
|
||||||
return qualityProfile;
|
return qualityProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,5 +94,15 @@ public int Compare(List<CustomFormat> left, CustomFormat right)
|
|||||||
|
|
||||||
return leftIndicies.Select(i => i.CompareTo(rightIndex)).Sum();
|
return leftIndicies.Select(i => i.CompareTo(rightIndex)).Sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int Compare(List<CustomFormat> left, int right)
|
||||||
|
{
|
||||||
|
left = left.WithNone();
|
||||||
|
|
||||||
|
var leftIndicies = GetIndicies(left, _profile);
|
||||||
|
var rightIndex = _profile.FormatItems.FindIndex(v => Equals(v.Format, right));
|
||||||
|
|
||||||
|
return leftIndicies.Select(i => i.CompareTo(rightIndex)).Sum();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ public QualityProfileModule(IProfileService profileService, ICustomFormatService
|
|||||||
return all.Except(ids).Empty();
|
return all.Except(ids).Empty();
|
||||||
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
||||||
SharedValidator.RuleFor(c => c.FormatCutoff)
|
SharedValidator.RuleFor(c => c.FormatCutoff)
|
||||||
.Must(c => _formatService.All().Select(f => f.Id).Contains(c.Id) || c.Id == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
|
.Must(c => _formatService.All().Select(f => f.Id).Contains(c) || c == CustomFormat.None.Id).WithMessage("The Custom Format Cutoff must be a valid Custom Format! Try refreshing your browser.");
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
GetResourceAll = GetAll;
|
||||||
GetResourceById = GetById;
|
GetResourceById = GetById;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
|
||||||
namespace Radarr.Api.V2.Profiles.Quality
|
namespace Radarr.Api.V2.Profiles.Quality
|
||||||
{
|
{
|
||||||
@ -16,7 +17,7 @@ public class QualityProfileResource : RestResource
|
|||||||
public int Cutoff { get; set; }
|
public int Cutoff { get; set; }
|
||||||
public string PreferredTags { get; set; }
|
public string PreferredTags { get; set; }
|
||||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||||
public CustomFormatResource FormatCutoff { get; set; }
|
public int FormatCutoff { get; set; }
|
||||||
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
public List<ProfileFormatItemResource> FormatItems { get; set; }
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
}
|
}
|
||||||
@ -36,7 +37,7 @@ public QualityProfileQualityItemResource()
|
|||||||
|
|
||||||
public class ProfileFormatItemResource : RestResource
|
public class ProfileFormatItemResource : RestResource
|
||||||
{
|
{
|
||||||
public CustomFormatResource Format { get; set; }
|
public CustomFormat Format { get; set; }
|
||||||
public bool Allowed { get; set; }
|
public bool Allowed { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,7 @@ public static QualityProfileResource ToResource(this Profile model)
|
|||||||
Cutoff = model.Cutoff,
|
Cutoff = model.Cutoff,
|
||||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||||
Items = model.Items.ConvertAll(ToResource),
|
Items = model.Items.ConvertAll(ToResource),
|
||||||
FormatCutoff = model.FormatCutoff.ToResource(),
|
FormatCutoff = model.FormatCutoff,
|
||||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||||
Language = model.Language
|
Language = model.Language
|
||||||
};
|
};
|
||||||
@ -78,7 +79,7 @@ public static ProfileFormatItemResource ToResource(this ProfileFormatItem model)
|
|||||||
{
|
{
|
||||||
return new ProfileFormatItemResource
|
return new ProfileFormatItemResource
|
||||||
{
|
{
|
||||||
Format = model.Format.ToResource(),
|
Format = model.Format,
|
||||||
Allowed = model.Allowed
|
Allowed = model.Allowed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -95,7 +96,7 @@ public static Profile ToModel(this QualityProfileResource resource)
|
|||||||
Cutoff = resource.Cutoff,
|
Cutoff = resource.Cutoff,
|
||||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||||
Items = resource.Items.ConvertAll(ToModel),
|
Items = resource.Items.ConvertAll(ToModel),
|
||||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
FormatCutoff = resource.FormatCutoff,
|
||||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||||
Language = resource.Language
|
Language = resource.Language
|
||||||
};
|
};
|
||||||
@ -119,7 +120,7 @@ public static ProfileFormatItem ToModel(this ProfileFormatItemResource resource)
|
|||||||
{
|
{
|
||||||
return new ProfileFormatItem
|
return new ProfileFormatItem
|
||||||
{
|
{
|
||||||
Format = resource.Format.ToModel(),
|
Format = resource.Format,
|
||||||
Allowed = resource.Allowed
|
Allowed = resource.Allowed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ public CustomFormatModule(ICustomFormatService formatService, IParsingService pa
|
|||||||
return !allFormats.Any(f =>
|
return !allFormats.Any(f =>
|
||||||
{
|
{
|
||||||
var allTags = f.FormatTags.Select(t => t.Raw.ToLower());
|
var allTags = f.FormatTags.Select(t => t.Raw.ToLower());
|
||||||
var allNewTags = c.Select(t => t.ToLower());
|
var allNewTags = c.Split(',').Select(t => t.ToLower());
|
||||||
var enumerable = allTags.ToList();
|
var enumerable = allTags.ToList();
|
||||||
var newTags = allNewTags.ToList();
|
var newTags = allNewTags.ToList();
|
||||||
return (enumerable.All(newTags.Contains) && f.Id != v.Id && enumerable.Count() == newTags.Count());
|
return (enumerable.All(newTags.Contains) && f.Id != v.Id && enumerable.Count() == newTags.Count());
|
||||||
|
@ -8,7 +8,7 @@ namespace Radarr.Api.V2.Qualities
|
|||||||
public class CustomFormatResource : RestResource
|
public class CustomFormatResource : RestResource
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public List<string> FormatTags { get; set; }
|
public string FormatTags { get; set; }
|
||||||
public string Simplicity { get; set; }
|
public string Simplicity { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ public static CustomFormatResource ToResource(this CustomFormat model)
|
|||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
Name = model.Name,
|
Name = model.Name,
|
||||||
FormatTags = model.FormatTags.Select(t => t.Raw.ToUpper()).ToList(),
|
FormatTags = string.Join(",", model.FormatTags.Select(t => t.Raw.ToUpper()).ToList()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ public static CustomFormat ToModel(this CustomFormatResource resource)
|
|||||||
{
|
{
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
Name = resource.Name,
|
Name = resource.Name,
|
||||||
FormatTags = resource.FormatTags.Select(s => new FormatTag(s)).ToList(),
|
FormatTags = resource.FormatTags.Split(',').Select(s => new FormatTag(s)).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ protected override bool IsValid(PropertyValidatorContext context)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tags = (IEnumerable<string>) context.PropertyValue;
|
var tags = (IEnumerable<string>) context.PropertyValue.ToString().Split(',');
|
||||||
|
|
||||||
var invalidTags = tags.Where(t => !FormatTag.QualityTagRegex.IsMatch(t));
|
var invalidTags = tags.Where(t => !FormatTag.QualityTagRegex.IsMatch(t));
|
||||||
|
|
||||||
|
@ -3295,10 +3295,10 @@ esprima@^4.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||||
|
|
||||||
esprint@0.4.0:
|
esprint@0.5.0:
|
||||||
version "0.4.0"
|
version "0.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.4.0.tgz#f89c9bace36d90407968a8f9ceb0800ff786aab0"
|
resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.5.0.tgz#25975b855b9df625ce2e32655db6dff1a84bbe36"
|
||||||
integrity sha1-+JybrONtkEB5aKj5zrCAD/eGqrA=
|
integrity sha512-TpaXKPy6g1saDqMYwqppZC6C0wQpYQAnhms6829oVvP6XieUbGjQdcNgatGQMihin2bMgE90tmX+1OOPc5tuiw==
|
||||||
dependencies:
|
dependencies:
|
||||||
dnode "^1.2.2"
|
dnode "^1.2.2"
|
||||||
fb-watchman "^2.0.0"
|
fb-watchman "^2.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user