mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: User defined scores for each Custom Format
Brings it more into line with Sonarr preferred words
This commit is contained in:
@@ -94,10 +94,18 @@ class CustomFormat extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
if (item.required) {
|
||||
kind = kinds.SUCCESS;
|
||||
}
|
||||
if (item.negate) {
|
||||
kind = kinds.DANGER;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
key={index}
|
||||
kind={item.required ? kinds.DANGER : kinds.DEFAULT}
|
||||
kind={kind}
|
||||
>
|
||||
{item.name}
|
||||
</Label>
|
||||
|
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
@@ -48,8 +49,8 @@ class AddSpecificationModalContent extends Component {
|
||||
<div>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>Radarr supports custom conditions against the following release properties</div>
|
||||
<div>Visit github for more details</div>
|
||||
<div>Radarr supports custom conditions against the release properties below.</div>
|
||||
<div>Visit <Link to='https://github.com/Radarr/Radarr/wiki/Custom-Formats-Aphrodite'>GitHub</Link> for more details.</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.specifications}>
|
||||
|
@@ -90,14 +90,14 @@ class Specification extends Component {
|
||||
|
||||
{
|
||||
negate &&
|
||||
<Label kind={kinds.INVERSE}>
|
||||
<Label kind={kinds.DANGER}>
|
||||
{'Negated'}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
required &&
|
||||
<Label kind={kinds.DANGER}>
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{'Required'}
|
||||
</Label>
|
||||
}
|
||||
|
6
frontend/src/Settings/Profiles/Profiles.css
Normal file
6
frontend/src/Settings/Profiles/Profiles.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.addCustomFormatMessage {
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
@@ -3,9 +3,11 @@ import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import QualityProfilesConnector from './Quality/QualityProfilesConnector';
|
||||
import DelayProfilesConnector from './Delay/DelayProfilesConnector';
|
||||
import styles from './Profiles.css';
|
||||
// Only a single DragDrop Context can exist so it's done here to allow editing
|
||||
// quality profiles and reordering delay profiles to work.
|
||||
|
||||
@@ -25,6 +27,11 @@ class Profiles extends Component {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<QualityProfilesConnector />
|
||||
<DelayProfilesConnector />
|
||||
<div className={styles.addCustomFormatMessage}>
|
||||
Looking for Release Profiles? Try
|
||||
<Link to='/settings/customformats'> Custom Formats </Link>
|
||||
instead.
|
||||
</div>
|
||||
</DndProvider>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
|
@@ -99,7 +99,6 @@ class EditQualityProfileModalContent extends Component {
|
||||
isInUse,
|
||||
onInputChange,
|
||||
onCutoffChange,
|
||||
onFormatCutoffChange,
|
||||
onLanguageChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
@@ -112,7 +111,8 @@ class EditQualityProfileModalContent extends Component {
|
||||
name,
|
||||
upgradeAllowed,
|
||||
cutoff,
|
||||
formatCutoff,
|
||||
minFormatScore,
|
||||
cutoffFormatScore,
|
||||
language,
|
||||
items,
|
||||
formatItems
|
||||
@@ -201,19 +201,35 @@ class EditQualityProfileModalContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
upgradeAllowed.value &&
|
||||
formatItems.value.length > 0 &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until Format
|
||||
Minimum Custom Format Score
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="formatCutoff"
|
||||
{...formatCutoff}
|
||||
values={customFormats}
|
||||
helpText="Once this custom format is reached Radarr will no longer download movies"
|
||||
onChange={onFormatCutoffChange}
|
||||
type={inputTypes.NUMBER}
|
||||
name="minFormatScore"
|
||||
{...minFormatScore}
|
||||
helpText="Minimum custom format score allowed to download"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
{
|
||||
upgradeAllowed.value && formatItems.value.length > 0 &&
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Upgrade Until Custom Format Score
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="cutoffFormatScore"
|
||||
{...cutoffFormatScore}
|
||||
helpText="Once this custom format score is reached Radarr will no longer download movies"
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
@@ -320,7 +336,6 @@ EditQualityProfileModalContent.propTypes = {
|
||||
isInUse: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onCutoffChange: PropTypes.func.isRequired,
|
||||
onFormatCutoffChange: PropTypes.func.isRequired,
|
||||
onLanguageChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onContentHeightChange: PropTypes.func.isRequired,
|
||||
|
@@ -70,19 +70,19 @@ function createFormatsSelector() {
|
||||
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,
|
||||
value: name
|
||||
});
|
||||
}
|
||||
return _.reduceRight(items.value, (result, { id, name, format, score }) => {
|
||||
if (id) {
|
||||
result.push({
|
||||
key: id,
|
||||
value: name,
|
||||
score
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
key: format,
|
||||
value: name,
|
||||
score
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -193,30 +193,6 @@ 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 === 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;
|
||||
}
|
||||
|
||||
this.props.setQualityProfileValue({ name: 'formatCutoff', value: cutoffId });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -239,13 +215,6 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onFormatCutoffChange = ({ name, value }) => {
|
||||
const id = parseInt(value);
|
||||
const cutoffId = _.find(this.props.item.formatItems.value, (i) => i.format === id).format;
|
||||
|
||||
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||
}
|
||||
|
||||
onLanguageChange = ({ name, value }) => {
|
||||
|
||||
const id = parseInt(value);
|
||||
@@ -274,19 +243,17 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
this.ensureCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
onQualityProfileFormatItemAllowedChange = (id, allowed) => {
|
||||
onQualityProfileFormatItemScoreChange = (id, score) => {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
const formatItems = qualityProfile.formatItems.value;
|
||||
const item = _.find(qualityProfile.formatItems.value, (i) => i.format === id);
|
||||
|
||||
item.allowed = allowed;
|
||||
item.score = score;
|
||||
|
||||
this.props.setQualityProfileValue({
|
||||
name: 'formatItems',
|
||||
value: formatItems
|
||||
});
|
||||
|
||||
this.ensureFormatCutoff(qualityProfile);
|
||||
}
|
||||
|
||||
onItemGroupAllowedChange = (id, allowed) => {
|
||||
@@ -505,39 +472,6 @@ 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 });
|
||||
}
|
||||
@@ -557,18 +491,15 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
onSavePress={this.onSavePress}
|
||||
onInputChange={this.onInputChange}
|
||||
onCutoffChange={this.onCutoffChange}
|
||||
onFormatCutoffChange={this.onFormatCutoffChange}
|
||||
onLanguageChange={this.onLanguageChange}
|
||||
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}
|
||||
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
||||
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
||||
/>
|
||||
);
|
||||
|
@@ -1,3 +1,9 @@
|
||||
.qualityProfileFormatItemContainer {
|
||||
display: flex;
|
||||
padding: $qualityProfileItemDragSourcePadding 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItem {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
@@ -7,38 +13,33 @@
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.checkContainer {
|
||||
position: relative;
|
||||
margin-right: 4px;
|
||||
margin-bottom: 7px;
|
||||
margin-left: 8px;
|
||||
.formatNameContainer {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
margin-left: 14px;
|
||||
width: 100%;
|
||||
font-weight: normal;
|
||||
line-height: $qualityProfileItemHeight;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.formatName {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 0;
|
||||
margin-left: 2px;
|
||||
font-weight: normal;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dragHandle {
|
||||
.scoreContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
cursor: grab;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.dragIcon {
|
||||
top: 0;
|
||||
}
|
||||
.scoreInput {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
.isDragging {
|
||||
opacity: 0.25;
|
||||
width: 100px;
|
||||
height: 30px;
|
||||
border: unset;
|
||||
border-radius: unset;
|
||||
background-color: unset;
|
||||
}
|
||||
|
@@ -1,9 +1,6 @@
|
||||
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 NumberInput from 'Components/Form/NumberInput';
|
||||
import styles from './QualityProfileFormatItem.css';
|
||||
|
||||
class QualityProfileFormatItem extends Component {
|
||||
@@ -11,13 +8,12 @@ class QualityProfileFormatItem extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onAllowedChange = ({ value }) => {
|
||||
onScoreChange = ({ value }) => {
|
||||
const {
|
||||
formatId,
|
||||
onQualityProfileFormatItemAllowedChange
|
||||
formatId
|
||||
} = this.props;
|
||||
|
||||
onQualityProfileFormatItemAllowedChange(formatId, value);
|
||||
this.props.onScoreChange(formatId, value);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -26,40 +22,32 @@ class QualityProfileFormatItem extends Component {
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
allowed,
|
||||
isDragging,
|
||||
connectDragSource
|
||||
score
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileFormatItem,
|
||||
isDragging && styles.isDragging
|
||||
)}
|
||||
className={styles.qualityProfileFormatItemContainer}
|
||||
>
|
||||
<label
|
||||
className={styles.formatName}
|
||||
<div
|
||||
className={styles.qualityProfileFormatItem}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<label
|
||||
className={styles.formatNameContainer}
|
||||
>
|
||||
<div className={styles.formatName}>
|
||||
{name}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<NumberInput
|
||||
containerClassName={styles.scoreContainer}
|
||||
className={styles.scoreInput}
|
||||
name={name}
|
||||
value={score}
|
||||
onChange={this.onScoreChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -68,16 +56,13 @@ class QualityProfileFormatItem extends Component {
|
||||
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
|
||||
score: PropTypes.number.isRequired,
|
||||
onScoreChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileFormatItem.defaultProps = {
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
// To handle the case score is deleted during edit
|
||||
score: 0
|
||||
};
|
||||
|
||||
export default QualityProfileFormatItem;
|
||||
|
@@ -1,4 +0,0 @@
|
||||
.dragPreview {
|
||||
width: 380px;
|
||||
opacity: 0.75;
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
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);
|
@@ -1,18 +0,0 @@
|
||||
.qualityProfileFormatItemDragSource {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholder {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
border: 1px dotted #aaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderBefore {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.qualityProfileFormatItemPlaceholderAfter {
|
||||
margin-top: 8px;
|
||||
}
|
@@ -1,157 +0,0 @@
|
||||
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));
|
@@ -1,6 +1,31 @@
|
||||
.formats {
|
||||
margin-top: 10px;
|
||||
/* TODO: This should consider the number of languages in the list */
|
||||
min-height: 550px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.headerContainer {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.headerScore {
|
||||
display: flex;
|
||||
flex-grow: 0;
|
||||
padding-left: 16px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.addCustomFormatMessage {
|
||||
max-width: $formGroupExtraSmallWidth;
|
||||
color: $helpTextColor;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
@@ -1,37 +1,87 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
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 Link from 'Components/Link/Link';
|
||||
import QualityProfileFormatItem from './QualityProfileFormatItem';
|
||||
import styles from './QualityProfileFormatItems.css';
|
||||
|
||||
function calcOrder(profileFormatItems) {
|
||||
const items = profileFormatItems.reduce((acc, cur, index) => {
|
||||
acc[cur.format] = index;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return [...profileFormatItems].sort((a, b) => {
|
||||
if (b.score !== a.score) {
|
||||
return b.score - a.score;
|
||||
}
|
||||
return a.name > b.name ? 1 : -1;
|
||||
}).map((x) => items[x.format]);
|
||||
}
|
||||
|
||||
class QualityProfileFormatItems extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
order: calcOrder(this.props.profileFormatItems)
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScoreChange = (formatId, value) => {
|
||||
const {
|
||||
onQualityProfileFormatItemScoreChange
|
||||
} = this.props;
|
||||
|
||||
onQualityProfileFormatItemScoreChange(formatId, value);
|
||||
this.reorderItems();
|
||||
}
|
||||
|
||||
reorderItems = _.debounce(() => this.setState({ order: calcOrder(this.props.profileFormatItems) }), 1000);
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dragIndex,
|
||||
dropIndex,
|
||||
profileFormatItems,
|
||||
errors,
|
||||
warnings,
|
||||
...otherProps
|
||||
warnings
|
||||
} = this.props;
|
||||
|
||||
const isDragging = dropIndex !== null;
|
||||
const isDraggingUp = isDragging && dropIndex > dragIndex;
|
||||
const isDraggingDown = isDragging && dropIndex < dragIndex;
|
||||
const {
|
||||
order
|
||||
} = this.state;
|
||||
|
||||
if (profileFormatItems.length < 1) {
|
||||
return (
|
||||
<div className={styles.addCustomFormatMessage}>
|
||||
Want more control over which downloads are preferred? Add a
|
||||
<Link to='/settings/customformats'> Custom Format </Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormGroup>
|
||||
<FormLabel>Custom Formats</FormLabel>
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
<FormLabel size={sizes.SMALL}>
|
||||
Custom Formats
|
||||
</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text="Custom Formats higher in the list are more preferred. Only checked custom formats are wanted"
|
||||
text='Radarr scores each release using the sum of scores for matching custom formats. If a new release would improve the score, at the same or better quality, then Radarr will grab it.'
|
||||
/>
|
||||
|
||||
{
|
||||
@@ -61,25 +111,32 @@ class QualityProfileFormatItems extends Component {
|
||||
}
|
||||
|
||||
<div className={styles.formats}>
|
||||
<div className={styles.headerContainer}>
|
||||
<div className={styles.headerTitle}>
|
||||
Custom Format
|
||||
</div>
|
||||
<div className={styles.headerScore}>
|
||||
Score
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
profileFormatItems.map(({ allowed, format, name }, index) => {
|
||||
order.map((index) => {
|
||||
const {
|
||||
format,
|
||||
name,
|
||||
score
|
||||
} = profileFormatItems[index];
|
||||
return (
|
||||
<QualityProfileFormatItemDragSource
|
||||
<QualityProfileFormatItem
|
||||
key={format}
|
||||
formatId={format}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
sortIndex={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
{...otherProps}
|
||||
score={score}
|
||||
onScoreChange={this.onScoreChange}
|
||||
/>
|
||||
);
|
||||
}).reverse()
|
||||
})
|
||||
}
|
||||
|
||||
<QualityProfileFormatItemDragPreview />
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
@@ -88,11 +145,10 @@ class QualityProfileFormatItems extends Component {
|
||||
}
|
||||
|
||||
QualityProfileFormatItems.propTypes = {
|
||||
dragIndex: PropTypes.number,
|
||||
dropIndex: PropTypes.number,
|
||||
profileFormatItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object),
|
||||
warnings: PropTypes.arrayOf(PropTypes.object)
|
||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||
onQualityProfileFormatItemScoreChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileFormatItems.defaultProps = {
|
||||
|
Reference in New Issue
Block a user