mirror of
https://github.com/Radarr/Radarr.git
synced 2025-04-24 06:27:08 -04:00
New: Get Custom Formats Working in Aphrodite
This commit is contained in:
parent
86dde88fe6
commit
b2268c7452
35 changed files with 1066 additions and 50 deletions
|
@ -1,3 +1,4 @@
|
|||
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
||||
export const QUALITY_PROFILE_FORMAT_ITEM = 'qualityProfileFormatItem';
|
||||
export const DELAY_PROFILE = 'delayProfile';
|
||||
export const TABLE_COLUMN = 'tableColumn';
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import split from 'Utilities/String/split';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Card from 'Components/Card';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import styles from './CustomFormat.css';
|
||||
|
||||
class CustomFormat extends Component {
|
||||
|
@ -62,15 +63,15 @@ class CustomFormat extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
// id,
|
||||
id,
|
||||
name,
|
||||
items,
|
||||
formatTags,
|
||||
isDeleting
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={styles.CustomFormat}
|
||||
className={styles.customFormat}
|
||||
overlayContent={true}
|
||||
onPress={this.onEditCustomFormatPress}
|
||||
>
|
||||
|
@ -87,32 +88,31 @@ class CustomFormat extends Component {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formats}>
|
||||
<div>
|
||||
{
|
||||
items.map((item) => {
|
||||
if (!item.allowed) {
|
||||
split(formatTags).map((item) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={item.quality.id}
|
||||
kind={kinds.default}
|
||||
title={null}
|
||||
key={item}
|
||||
kind={kinds.DEFAULT}
|
||||
>
|
||||
{item.quality.name}
|
||||
{item}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* <EditCustomFormatModalConnector
|
||||
<EditCustomFormatModalConnector
|
||||
id={id}
|
||||
isOpen={this.state.isEditCustomFormatModalOpen}
|
||||
onModalClose={this.onEditCustomFormatModalClose}
|
||||
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||
|
@ -132,7 +132,7 @@ class CustomFormat extends Component {
|
|||
CustomFormat.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
formatTags: PropTypes.string.isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||
|
|
|
@ -7,7 +7,7 @@ import Card from 'Components/Card';
|
|||
import Icon from 'Components/Icon';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import CustomFormat from './CustomFormat';
|
||||
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||
import styles from './CustomFormats.css';
|
||||
|
||||
class CustomFormats extends Component {
|
||||
|
@ -57,7 +57,7 @@ class CustomFormats extends Component {
|
|||
errorMessage="Unable to load Custom Formats"
|
||||
{...otherProps}c={true}
|
||||
>
|
||||
<div className={styles.CustomFormats}>
|
||||
<div className={styles.customFormats}>
|
||||
{
|
||||
items.sort(sortByName).map((item) => {
|
||||
return (
|
||||
|
@ -85,11 +85,10 @@ class CustomFormats extends Component {
|
|||
</Card>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<EditCustomFormatModalConnector
|
||||
isOpen={this.state.isCustomFormatModalOpen}
|
||||
onModalClose={this.onModalClose}
|
||||
/> */}
|
||||
/>
|
||||
|
||||
</PageSectionContent>
|
||||
</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 FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import QualityProfileItems from './QualityProfileItems';
|
||||
import QualityProfileFormatItems from './QualityProfileFormatItems';
|
||||
import styles from './EditQualityProfileModalContent.css';
|
||||
|
||||
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||
|
@ -92,6 +93,7 @@ class EditQualityProfileModalContent extends Component {
|
|||
isSaving,
|
||||
saveError,
|
||||
qualities,
|
||||
customFormats,
|
||||
languages,
|
||||
item,
|
||||
isInUse,
|
||||
|
@ -109,11 +111,13 @@ class EditQualityProfileModalContent extends Component {
|
|||
name,
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
formatCutoff,
|
||||
language,
|
||||
items
|
||||
items,
|
||||
formatItems
|
||||
} = item;
|
||||
|
||||
const languageId = language.value.id;
|
||||
const languageId = language ? language.value.id : 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
|
@ -181,7 +185,7 @@ class EditQualityProfileModalContent extends Component {
|
|||
upgradeAllowed.value &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until
|
||||
Upgrade Until Quality
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
@ -195,6 +199,24 @@ class EditQualityProfileModalContent extends Component {
|
|||
</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}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Language
|
||||
|
@ -220,6 +242,15 @@ class EditQualityProfileModalContent extends Component {
|
|||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroupWrapper}>
|
||||
<QualityProfileFormatItems
|
||||
profileFormatItems={formatItems.value}
|
||||
errors={formatItems.errors}
|
||||
warnings={formatItems.warnings}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
|
@ -282,6 +313,7 @@ EditQualityProfileModalContent.propTypes = {
|
|||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
item: PropTypes.object.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() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languages,
|
||||
|
@ -87,11 +117,13 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
createProviderSettingsSelector('qualityProfiles'),
|
||||
createQualitiesSelector(),
|
||||
createFormatsSelector(),
|
||||
createLanguagesSelector(),
|
||||
createProfileInUseSelector('qualityProfileId'),
|
||||
(qualityProfile, qualities, languages, isInUse) => {
|
||||
(qualityProfile, qualities, customFormats, languages, isInUse) => {
|
||||
return {
|
||||
qualities,
|
||||
customFormats,
|
||||
languages,
|
||||
...qualityProfile,
|
||||
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
|
||||
|
||||
|
@ -211,6 +267,21 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
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) => {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
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 = () => {
|
||||
this.setState({ editGroups: !this.state.editGroups });
|
||||
}
|
||||
|
@ -450,10 +554,13 @@ class EditQualityProfileModalContentConnector extends Component {
|
|||
onCreateGroupPress={this.onCreateGroupPress}
|
||||
onDeleteGroupPress={this.onDeleteGroupPress}
|
||||
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
||||
onQualityProfileFormatItemAllowedChange={this.onQualityProfileFormatItemAllowedChange}
|
||||
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
|
||||
onItemGroupNameChange={this.onItemGroupNameChange}
|
||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||
onQualityProfileFormatItemDragMove={this.onQualityProfileFormatItemDragMove}
|
||||
onQualityProfileFormatItemDragEnd={this.onQualityProfileFormatItemDragEnd}
|
||||
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",
|
||||
"start": "gulp watch",
|
||||
"watch": "gulp watch",
|
||||
"clean": "git clean -fXd",
|
||||
"lint": "esprint check",
|
||||
"lint-fix": "eslint start --fix",
|
||||
"stylelint": "stylelint frontend/**/*.css --config frontend/.stylelintrc"
|
||||
|
@ -53,7 +54,7 @@
|
|||
"eslint": "6.0.1",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-react": "7.14.2",
|
||||
"esprint": "0.4.0",
|
||||
"esprint": "0.5.0",
|
||||
"file-loader": "4.0.0",
|
||||
"filesize": "4.1.2",
|
||||
"fuse.js": "3.4.5",
|
||||
|
|
|
@ -51,6 +51,17 @@ namespace NzbDrone.Api.Profiles
|
|||
? cutoffItem.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
|
||||
{
|
||||
Id = model.Id,
|
||||
|
@ -74,7 +85,7 @@ namespace NzbDrone.Api.Profiles
|
|||
|
||||
return new List<ProfileQualityItemResource> { ToResource(i) };
|
||||
}).ToList(),
|
||||
FormatCutoff = model.FormatCutoff.ToResource(),
|
||||
FormatCutoff = formatCutoff.ToResource(),
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||
Language = model.Language
|
||||
};
|
||||
|
@ -112,7 +123,7 @@ namespace NzbDrone.Api.Profiles
|
|||
Cutoff = resource.Cutoff.Id,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel().Id,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||
Language = resource.Language
|
||||
};
|
||||
|
|
|
@ -45,7 +45,7 @@ namespace NzbDrone.Api.Profiles
|
|||
var profile = new Profile();
|
||||
profile.Cutoff = Quality.Unknown.Id;
|
||||
profile.Items = items;
|
||||
profile.FormatCutoff = CustomFormat.None;
|
||||
profile.FormatCutoff = CustomFormat.None.Id;
|
||||
profile.FormatItems = formatItems;
|
||||
profile.Language = Language.English;
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
|||
{
|
||||
Cutoff = Quality.HDTV720p.Id,
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||
FormatItems = CustomFormatsFixture.GetSampleFormatItems("None", "My Format")
|
||||
}, old, newQ).Should().BeFalse();
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ namespace NzbDrone.Core.Test.MovieTests.MovieRepositoryTests
|
|||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.Profiles
|
|||
var profile = new Profile
|
||||
{
|
||||
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.Bluray1080p, Quality.DVD, Quality.HDTV720p),
|
||||
FormatCutoff = CustomFormats.CustomFormat.None,
|
||||
FormatCutoff = CustomFormats.CustomFormat.None.Id,
|
||||
FormatItems = CustomFormat.CustomFormatsFixture.GetDefaultFormatItems(),
|
||||
Cutoff = Quality.Bluray1080p.Id,
|
||||
Name = "TestProfile"
|
||||
|
|
|
@ -7,6 +7,8 @@ using NzbDrone.Core.Profiles;
|
|||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.NetImport;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Test.Profiles
|
||||
{
|
||||
|
@ -17,6 +19,10 @@ namespace NzbDrone.Core.Test.Profiles
|
|||
[Test]
|
||||
public void init_should_add_default_profiles()
|
||||
{
|
||||
Mocker.GetMock<ICustomFormatService>()
|
||||
.Setup(s => s.All())
|
||||
.Returns(new List<CustomFormats.CustomFormat>());
|
||||
|
||||
Subject.Handle(new ApplicationStartedEvent());
|
||||
|
||||
Mocker.GetMock<IProfileRepository>()
|
||||
|
|
|
@ -17,7 +17,7 @@ namespace NzbDrone.Core.Profiles
|
|||
public string Name { get; set; }
|
||||
public int Cutoff { 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<string> PreferredTags { 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;
|
||||
|
||||
namespace NzbDrone.Core.Profiles
|
||||
{
|
||||
public class ProfileFormatItem : IEmbeddedDocument
|
||||
{
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||
public int Id { get; set; }
|
||||
public CustomFormat Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
|
|
@ -74,9 +74,9 @@ namespace NzbDrone.Core.Profiles
|
|||
foreach (var profile in all)
|
||||
{
|
||||
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);
|
||||
|
@ -187,7 +187,9 @@ namespace NzbDrone.Core.Profiles
|
|||
public Profile GetDefaultProfile(string name, Quality cutoff = null, params Quality[] allowed)
|
||||
{
|
||||
var groupedQualites = Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight);
|
||||
var formats = _formatService.All();
|
||||
var items = new List<ProfileQualityItem>();
|
||||
var formatItems = new List<ProfileFormatItem>();
|
||||
var groupId = 1000;
|
||||
var profileCutoff = cutoff == null ? Quality.Unknown.Id : cutoff.Id;
|
||||
|
||||
|
@ -223,23 +225,36 @@ namespace NzbDrone.Core.Profiles
|
|||
groupId++;
|
||||
}
|
||||
|
||||
foreach (var format in formats)
|
||||
{
|
||||
formatItems.Add(new ProfileFormatItem
|
||||
{
|
||||
Id = format.Id,
|
||||
Format = format,
|
||||
Allowed = false
|
||||
});
|
||||
}
|
||||
|
||||
var qualityProfile = new Profile
|
||||
{
|
||||
Name = name,
|
||||
Cutoff = profileCutoff,
|
||||
Items = items,
|
||||
Language = Language.English,
|
||||
FormatCutoff = CustomFormat.None,
|
||||
FormatCutoff = CustomFormat.None.Id,
|
||||
FormatItems = new List<ProfileFormatItem>
|
||||
{
|
||||
new ProfileFormatItem
|
||||
{
|
||||
Id = 0,
|
||||
Allowed = true,
|
||||
Format = CustomFormat.None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
qualityProfile.FormatItems.AddRange(formatItems);
|
||||
|
||||
return qualityProfile;
|
||||
}
|
||||
|
||||
|
|
|
@ -94,5 +94,15 @@ namespace NzbDrone.Core.Qualities
|
|||
|
||||
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 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
return all.Except(ids).Empty();
|
||||
}).WithMessage("All Custom Formats and no extra ones need to be present inside your Profile! Try refreshing your browser.");
|
||||
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;
|
||||
GetResourceById = GetById;
|
||||
|
|
|
@ -6,6 +6,7 @@ using NzbDrone.Core.Parser;
|
|||
using NzbDrone.Core.Profiles;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
|
||||
namespace Radarr.Api.V2.Profiles.Quality
|
||||
{
|
||||
|
@ -16,7 +17,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
public int Cutoff { get; set; }
|
||||
public string PreferredTags { 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 Language Language { get; set; }
|
||||
}
|
||||
|
@ -36,7 +37,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
|
||||
public class ProfileFormatItemResource : RestResource
|
||||
{
|
||||
public CustomFormatResource Format { get; set; }
|
||||
public CustomFormat Format { get; set; }
|
||||
public bool Allowed { get; set; }
|
||||
}
|
||||
|
||||
|
@ -54,7 +55,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
Cutoff = model.Cutoff,
|
||||
PreferredTags = model.PreferredTags != null ? string.Join(",", model.PreferredTags) : "",
|
||||
Items = model.Items.ConvertAll(ToResource),
|
||||
FormatCutoff = model.FormatCutoff.ToResource(),
|
||||
FormatCutoff = model.FormatCutoff,
|
||||
FormatItems = model.FormatItems.ConvertAll(ToResource),
|
||||
Language = model.Language
|
||||
};
|
||||
|
@ -78,7 +79,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
{
|
||||
return new ProfileFormatItemResource
|
||||
{
|
||||
Format = model.Format.ToResource(),
|
||||
Format = model.Format,
|
||||
Allowed = model.Allowed
|
||||
};
|
||||
}
|
||||
|
@ -95,7 +96,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
Cutoff = resource.Cutoff,
|
||||
PreferredTags = resource.PreferredTags.Split(',').ToList(),
|
||||
Items = resource.Items.ConvertAll(ToModel),
|
||||
FormatCutoff = resource.FormatCutoff.ToModel(),
|
||||
FormatCutoff = resource.FormatCutoff,
|
||||
FormatItems = resource.FormatItems.ConvertAll(ToModel),
|
||||
Language = resource.Language
|
||||
};
|
||||
|
@ -119,7 +120,7 @@ namespace Radarr.Api.V2.Profiles.Quality
|
|||
{
|
||||
return new ProfileFormatItem
|
||||
{
|
||||
Format = resource.Format.ToModel(),
|
||||
Format = resource.Format,
|
||||
Allowed = resource.Allowed
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ namespace Radarr.Api.V2.Qualities
|
|||
return !allFormats.Any(f =>
|
||||
{
|
||||
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 newTags = allNewTags.ToList();
|
||||
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 string Name { get; set; }
|
||||
public List<string> FormatTags { get; set; }
|
||||
public string FormatTags { get; set; }
|
||||
public string Simplicity { get; set; }
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ namespace Radarr.Api.V2.Qualities
|
|||
{
|
||||
Id = model.Id,
|
||||
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 @@ namespace Radarr.Api.V2.Qualities
|
|||
{
|
||||
Id = resource.Id,
|
||||
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 @@ namespace Radarr.Api.V2.Qualities
|
|||
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));
|
||||
|
||||
|
|
|
@ -3295,10 +3295,10 @@ esprima@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
|
||||
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
|
||||
|
||||
esprint@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.4.0.tgz#f89c9bace36d90407968a8f9ceb0800ff786aab0"
|
||||
integrity sha1-+JybrONtkEB5aKj5zrCAD/eGqrA=
|
||||
esprint@0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/esprint/-/esprint-0.5.0.tgz#25975b855b9df625ce2e32655db6dff1a84bbe36"
|
||||
integrity sha512-TpaXKPy6g1saDqMYwqppZC6C0wQpYQAnhms6829oVvP6XieUbGjQdcNgatGQMihin2bMgE90tmX+1OOPc5tuiw==
|
||||
dependencies:
|
||||
dnode "^1.2.2"
|
||||
fb-watchman "^2.0.0"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue