parent
6275737ced
commit
16ff1176f7
@ -0,0 +1,30 @@
|
|||||||
|
import React, { Component } from 'react';
|
||||||
|
import { DragDropContext } from 'react-dnd';
|
||||||
|
import HTML5Backend from 'react-dnd-html5-backend';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import CustomFormatsConnector from './CustomFormats/CustomFormatsConnector';
|
||||||
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
|
|
||||||
|
class CustomFormatSettingsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PageContent title="Custom Formats Settings">
|
||||||
|
<SettingsToolbarConnector
|
||||||
|
showSave={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageContentBodyConnector>
|
||||||
|
<CustomFormatsConnector />
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragDropContext(HTML5Backend)(CustomFormatSettingsConnector);
|
||||||
|
|
@ -0,0 +1,38 @@
|
|||||||
|
.customFormat {
|
||||||
|
composes: card from '~Components/Card.css';
|
||||||
|
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloneButton {
|
||||||
|
composes: button from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 5px;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltipLabel {
|
||||||
|
composes: label from '~Components/Label.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
@ -0,0 +1,141 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
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 styles from './CustomFormat.css';
|
||||||
|
|
||||||
|
class CustomFormat extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditCustomFormatModalOpen: false,
|
||||||
|
isDeleteCustomFormatModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditCustomFormatPress = () => {
|
||||||
|
this.setState({ isEditCustomFormatModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditCustomFormatModalClose = () => {
|
||||||
|
this.setState({ isEditCustomFormatModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteCustomFormatPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditCustomFormatModalOpen: false,
|
||||||
|
isDeleteCustomFormatModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteCustomFormatModalClose = () => {
|
||||||
|
this.setState({ isDeleteCustomFormatModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteCustomFormat = () => {
|
||||||
|
this.props.onConfirmDeleteCustomFormat(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloneCustomFormatPress = () => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onCloneCustomFormatPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onCloneCustomFormatPress(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
// id,
|
||||||
|
name,
|
||||||
|
items,
|
||||||
|
isDeleting
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.CustomFormat}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={this.onEditCustomFormatPress}
|
||||||
|
>
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={styles.name}>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.cloneButton}
|
||||||
|
title="Clone Profile"
|
||||||
|
name={icons.CLONE}
|
||||||
|
onPress={this.onCloneCustomFormatPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formats}>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
if (!item.allowed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={item.quality.id}
|
||||||
|
kind={kinds.default}
|
||||||
|
title={null}
|
||||||
|
>
|
||||||
|
{item.quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <EditCustomFormatModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={this.state.isEditCustomFormatModalOpen}
|
||||||
|
onModalClose={this.onEditCustomFormatModalClose}
|
||||||
|
onDeleteCustomFormatPress={this.onDeleteCustomFormatPress}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={this.state.isDeleteCustomFormatModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Custom Format"
|
||||||
|
message={`Are you sure you want to delete the custom format '${name}'?`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onConfirm={this.onConfirmDeleteCustomFormat}
|
||||||
|
onCancel={this.onDeleteCustomFormatModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFormat.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||||
|
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFormat;
|
@ -0,0 +1,21 @@
|
|||||||
|
.customFormats {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addCustomFormat {
|
||||||
|
composes: customFormat from '~./CustomFormat.css';
|
||||||
|
|
||||||
|
background-color: $cardAlternateBackgroundColor;
|
||||||
|
color: $gray;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 20px 0;
|
||||||
|
border: 1px solid $borderColor;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $white;
|
||||||
|
}
|
@ -0,0 +1,109 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import CustomFormat from './CustomFormat';
|
||||||
|
// import EditCustomFormatModalConnector from './EditCustomFormatModalConnector';
|
||||||
|
import styles from './CustomFormats.css';
|
||||||
|
|
||||||
|
class CustomFormats extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isCustomFormatModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onCloneCustomFormatPress = (id) => {
|
||||||
|
this.props.onCloneCustomFormatPress(id);
|
||||||
|
this.setState({ isCustomFormatModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditCustomFormatPress = () => {
|
||||||
|
this.setState({ isCustomFormatModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalClose = () => {
|
||||||
|
this.setState({ isCustomFormatModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
isDeleting,
|
||||||
|
onConfirmDeleteCustomFormat,
|
||||||
|
onCloneCustomFormatPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend="Custom Formats">
|
||||||
|
<PageSectionContent
|
||||||
|
errorMessage="Unable to load Custom Formats"
|
||||||
|
{...otherProps}c={true}
|
||||||
|
>
|
||||||
|
<div className={styles.CustomFormats}>
|
||||||
|
{
|
||||||
|
items.sort(sortByName).map((item) => {
|
||||||
|
return (
|
||||||
|
<CustomFormat
|
||||||
|
key={item.id}
|
||||||
|
{...item}
|
||||||
|
isDeleting={isDeleting}
|
||||||
|
onConfirmDeleteCustomFormat={onConfirmDeleteCustomFormat}
|
||||||
|
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={styles.addCustomFormat}
|
||||||
|
onPress={this.onEditCustomFormatPress}
|
||||||
|
>
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.ADD}
|
||||||
|
size={45}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
<EditCustomFormatModalConnector
|
||||||
|
isOpen={this.state.isCustomFormatModalOpen}
|
||||||
|
onModalClose={this.onModalClose}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFormats.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onConfirmDeleteCustomFormat: PropTypes.func.isRequired,
|
||||||
|
onCloneCustomFormatPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CustomFormats;
|
@ -0,0 +1,65 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchCustomFormats, deleteCustomFormat, cloneCustomFormat } from 'Store/Actions/settingsActions';
|
||||||
|
import CustomFormats from './CustomFormats';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.customFormats,
|
||||||
|
(customFormats) => {
|
||||||
|
return {
|
||||||
|
...customFormats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchCustomFormats: fetchCustomFormats,
|
||||||
|
dispatchDeleteCustomFormat: deleteCustomFormat,
|
||||||
|
dispatchCloneCustomFormat: cloneCustomFormat
|
||||||
|
};
|
||||||
|
|
||||||
|
class CustomFormatsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.dispatchFetchCustomFormats();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onConfirmDeleteCustomFormat = (id) => {
|
||||||
|
this.props.dispatchDeleteCustomFormat({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloneCustomFormatPress = (id) => {
|
||||||
|
this.props.dispatchCloneCustomFormat({ id });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CustomFormats
|
||||||
|
onConfirmDeleteCustomFormat={this.onConfirmDeleteCustomFormat}
|
||||||
|
onCloneCustomFormatPress={this.onCloneCustomFormatPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomFormatsConnector.propTypes = {
|
||||||
|
dispatchFetchCustomFormats: PropTypes.func.isRequired,
|
||||||
|
dispatchDeleteCustomFormat: PropTypes.func.isRequired,
|
||||||
|
dispatchCloneCustomFormat: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFormatsConnector);
|
@ -1,17 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
|
||||||
|
|
||||||
function CustomFormatsConnector() {
|
|
||||||
return (
|
|
||||||
<PageContent title="Custom Formats Settings">
|
|
||||||
<SettingsToolbarConnector
|
|
||||||
showSave={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CustomFormatsConnector;
|
|
||||||
|
|
@ -0,0 +1,97 @@
|
|||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { createThunk } from 'Store/thunks';
|
||||||
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||||
|
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||||
|
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||||
|
import createFetchSchemaHandler from 'Store/Actions/Creators/createFetchSchemaHandler';
|
||||||
|
import createSaveProviderHandler from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||||
|
import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHandler';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
const section = 'settings.customFormats';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_CUSTOM_FORMATS = 'settings/customFormats/fetchCustomFormats';
|
||||||
|
export const FETCH_CUSTOM_FORMAT_SCHEMA = 'settings/customFormats/fetchCustomFormatSchema';
|
||||||
|
export const SAVE_CUSTOM_FORMAT = 'settings/customFormats/saveCustomFormat';
|
||||||
|
export const DELETE_CUSTOM_FORMAT = 'settings/customFormats/deleteCustomFormat';
|
||||||
|
export const SET_CUSTOM_FORMAT_VALUE = 'settings/customFormats/setCustomFormatValue';
|
||||||
|
export const CLONE_CUSTOM_FORMAT = 'settings/customFormats/cloneCustomFormat';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchCustomFormats = createThunk(FETCH_CUSTOM_FORMATS);
|
||||||
|
export const fetchCustomFormatSchema = createThunk(FETCH_CUSTOM_FORMAT_SCHEMA);
|
||||||
|
export const saveCustomFormat = createThunk(SAVE_CUSTOM_FORMAT);
|
||||||
|
export const deleteCustomFormat = createThunk(DELETE_CUSTOM_FORMAT);
|
||||||
|
|
||||||
|
export const setCustomFormatValue = createAction(SET_CUSTOM_FORMAT_VALUE, (payload) => {
|
||||||
|
return {
|
||||||
|
section,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cloneCustomFormat = createAction(CLONE_CUSTOM_FORMAT);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Details
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
defaultState: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
isDeleting: false,
|
||||||
|
deleteError: null,
|
||||||
|
isSchemaFetching: false,
|
||||||
|
isSchemaPopulated: false,
|
||||||
|
schemaError: null,
|
||||||
|
schema: {},
|
||||||
|
isSaving: false,
|
||||||
|
saveError: null,
|
||||||
|
items: [],
|
||||||
|
pendingChanges: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
actionHandlers: {
|
||||||
|
[FETCH_CUSTOM_FORMATS]: createFetchHandler(section, '/customformat'),
|
||||||
|
[FETCH_CUSTOM_FORMAT_SCHEMA]: createFetchSchemaHandler(section, '/customformat/schema'),
|
||||||
|
[SAVE_CUSTOM_FORMAT]: createSaveProviderHandler(section, '/customformat'),
|
||||||
|
[DELETE_CUSTOM_FORMAT]: createRemoveItemHandler(section, '/customformat')
|
||||||
|
},
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
reducers: {
|
||||||
|
[SET_CUSTOM_FORMAT_VALUE]: createSetSettingValueReducer(section),
|
||||||
|
|
||||||
|
[CLONE_CUSTOM_FORMAT]: function(state, { payload }) {
|
||||||
|
const id = payload.id;
|
||||||
|
const newState = getSectionState(state, section);
|
||||||
|
const item = newState.items.find((i) => i.id === id);
|
||||||
|
const pendingChanges = { ...item, id: 0 };
|
||||||
|
delete pendingChanges.id;
|
||||||
|
|
||||||
|
pendingChanges.name = `${pendingChanges.name} - Copy`;
|
||||||
|
newState.pendingChanges = pendingChanges;
|
||||||
|
|
||||||
|
return updateSectionState(state, section, newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Profiles;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.Qualities
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class QualityIndexCompareToFixture : CoreTest
|
||||||
|
{
|
||||||
|
[TestCase(1, 0, 1, 0, 0)]
|
||||||
|
[TestCase(1, 1, 1, 0, 1)]
|
||||||
|
[TestCase(2, 0, 1, 0, 1)]
|
||||||
|
[TestCase(1, 0, 1, 1, -1)]
|
||||||
|
[TestCase(1, 0, 2, 0, -1)]
|
||||||
|
public void should_match_expected_when_respect_group_order_is_true(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected)
|
||||||
|
{
|
||||||
|
var left = new QualityIndex(leftIndex, leftGroupIndex);
|
||||||
|
var right = new QualityIndex(rightIndex, rightGroupIndex);
|
||||||
|
left.CompareTo(right, true).Should().Be(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1, 0, 1, 0, 0)]
|
||||||
|
[TestCase(1, 1, 1, 0, 0)]
|
||||||
|
[TestCase(2, 0, 1, 0, 1)]
|
||||||
|
[TestCase(1, 0, 1, 1, 0)]
|
||||||
|
[TestCase(1, 0, 2, 0, -1)]
|
||||||
|
public void should_match_expected_when_respect_group_order_is_false(int leftIndex, int leftGroupIndex, int rightIndex, int rightGroupIndex, int expected)
|
||||||
|
{
|
||||||
|
var left = new QualityIndex(leftIndex, leftGroupIndex);
|
||||||
|
var right = new QualityIndex(rightIndex, rightGroupIndex);
|
||||||
|
left.CompareTo(right, false).Should().Be(expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Profiles
|
||||||
|
{
|
||||||
|
public class QualityIndex : IComparable, IComparable<QualityIndex>
|
||||||
|
{
|
||||||
|
public int Index { get; set; }
|
||||||
|
public int GroupIndex { get; set; }
|
||||||
|
|
||||||
|
public QualityIndex()
|
||||||
|
{
|
||||||
|
Index = 0;
|
||||||
|
GroupIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QualityIndex(int index)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
GroupIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public QualityIndex(int index, int groupIndex)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
GroupIndex = groupIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(object obj)
|
||||||
|
{
|
||||||
|
return CompareTo((QualityIndex)obj, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(QualityIndex other)
|
||||||
|
{
|
||||||
|
return CompareTo(other, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int CompareTo(QualityIndex right, bool respectGroupOrder)
|
||||||
|
{
|
||||||
|
if (right == null)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexCompare = Index.CompareTo(right.Index);
|
||||||
|
|
||||||
|
if (respectGroupOrder && indexCompare == 0)
|
||||||
|
{
|
||||||
|
return GroupIndex.CompareTo(right.GroupIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexCompare; ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V2.Profiles.Quality
|
||||||
|
{
|
||||||
|
public static class QualityCutoffValidator
|
||||||
|
{
|
||||||
|
public static IRuleBuilderOptions<T, int> ValidCutoff<T>(this IRuleBuilder<T, int> ruleBuilder)
|
||||||
|
{
|
||||||
|
return ruleBuilder.SetValidator(new ValidCutoffValidator<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidCutoffValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ValidCutoffValidator()
|
||||||
|
: base("Cutoff must be an allowed quality or group")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var cutoff = (int)context.PropertyValue;
|
||||||
|
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||||
|
var items = instance.Items as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
var cutoffItem = items.SingleOrDefault(i => (i.Quality == null && i.Id == cutoff) || i.Quality?.Id == cutoff);
|
||||||
|
|
||||||
|
if (cutoffItem == null) return false;
|
||||||
|
|
||||||
|
if (!cutoffItem.Allowed) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,197 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V2.Profiles.Quality
|
||||||
|
{
|
||||||
|
public static class QualityItemsValidator
|
||||||
|
{
|
||||||
|
public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> ValidItems<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
|
||||||
|
{
|
||||||
|
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||||
|
ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new QualityNameValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new EmptyItemGroupNameValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||||
|
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AllowedValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public AllowedValidator()
|
||||||
|
: base("Must contain at least one allowed quality")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.Any(c => c.Allowed))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmptyItemGroupNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public EmptyItemGroupNameValidator()
|
||||||
|
: base("Groups must not be empty")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QualityNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public QualityNameValidator()
|
||||||
|
: base("Individual qualities should not be named")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemGroupNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ItemGroupNameValidator()
|
||||||
|
: base("Groups must have a name")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemGroupIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ItemGroupIdValidator()
|
||||||
|
: base("Groups must have an ID")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Quality == null && i.Id == 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UniqueIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public UniqueIdValidator()
|
||||||
|
: base("Groups must have a unique ID")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UniqueQualityIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public UniqueQualityIdValidator()
|
||||||
|
: base("Qualities can only be used once")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
var qualityIds = new HashSet<int>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Id > 0)
|
||||||
|
{
|
||||||
|
foreach (var quality in item.Items)
|
||||||
|
{
|
||||||
|
if (qualityIds.Contains(quality.Quality.Id))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityIds.Add(quality.Quality.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (qualityIds.Contains(item.Quality.Id))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityIds.Add(item.Quality.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,54 +1,25 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using NzbDrone.Core.CustomFormats;
|
|
||||||
using NzbDrone.Core.Parser;
|
|
||||||
using NzbDrone.Core.Profiles;
|
using NzbDrone.Core.Profiles;
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
using Radarr.Http;
|
using Radarr.Http;
|
||||||
using Radarr.Http.Mapping;
|
|
||||||
|
|
||||||
namespace Radarr.Api.V2.Profiles.Quality
|
namespace Radarr.Api.V2.Profiles.Quality
|
||||||
{
|
{
|
||||||
public class QualityProfileSchemaModule : RadarrRestModule<QualityProfileResource>
|
public class QualityProfileSchemaModule : RadarrRestModule<QualityProfileResource>
|
||||||
{
|
{
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
private readonly IProfileService _profileService;
|
||||||
private readonly ICustomFormatService _formatService;
|
|
||||||
|
|
||||||
public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService, ICustomFormatService formatService)
|
public QualityProfileSchemaModule(IProfileService profileService)
|
||||||
: base("/profile/schema")
|
: base("/qualityprofile/schema")
|
||||||
{
|
{
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
_profileService = profileService;
|
||||||
_formatService = formatService;
|
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
GetResourceSingle = GetSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<QualityProfileResource> GetAll()
|
private QualityProfileResource GetSchema()
|
||||||
{
|
{
|
||||||
var items = _qualityDefinitionService.All()
|
var qualityProfile = _profileService.GetDefaultProfile(string.Empty);
|
||||||
.OrderBy(v => v.Weight)
|
|
||||||
.Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false })
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var formatItems = _formatService.All().Select(v => new ProfileFormatItem
|
return qualityProfile.ToResource();
|
||||||
{
|
|
||||||
Format = v, Allowed = true
|
|
||||||
}).ToList();
|
|
||||||
|
|
||||||
formatItems.Insert(0, new ProfileFormatItem
|
|
||||||
{
|
|
||||||
Format = CustomFormat.None,
|
|
||||||
Allowed = true
|
|
||||||
});
|
|
||||||
|
|
||||||
var profile = new Profile();
|
|
||||||
profile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown;
|
|
||||||
profile.Items = items;
|
|
||||||
profile.FormatCutoff = CustomFormat.None;
|
|
||||||
profile.FormatItems = formatItems;
|
|
||||||
profile.Language = Language.English;
|
|
||||||
|
|
||||||
return new List<QualityProfileResource> { profile.ToResource() };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using FluentValidation;
|
|
||||||
using FluentValidation.Validators;
|
|
||||||
|
|
||||||
namespace Radarr.Api.V2.Profiles.Quality
|
|
||||||
{
|
|
||||||
public static class QualityProfileValidation
|
|
||||||
{
|
|
||||||
public static IRuleBuilderOptions<T, IList<ProfileQualityItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<ProfileQualityItemResource>> ruleBuilder)
|
|
||||||
{
|
|
||||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
|
||||||
|
|
||||||
return ruleBuilder.SetValidator(new AllowedValidator<T>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AllowedValidator<T> : PropertyValidator
|
|
||||||
{
|
|
||||||
public AllowedValidator()
|
|
||||||
: base("Must contain at least one allowed quality")
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool IsValid(PropertyValidatorContext context)
|
|
||||||
{
|
|
||||||
var list = context.PropertyValue as IList<ProfileQualityItemResource>;
|
|
||||||
|
|
||||||
if (list == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!list.Any(c => c.Allowed))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in new issue