diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css
new file mode 100644
index 000000000..f0de9d6af
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css
@@ -0,0 +1,82 @@
+.qualityProfileItem {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ background: #fafafa;
+
+ &.isInGroup {
+ border-style: dashed;
+ }
+}
+
+.checkInputContainer {
+ position: relative;
+ margin-right: 4px;
+ margin-bottom: 5px;
+ margin-left: 8px;
+}
+
+.checkInput {
+ composes: input from '~Components/Form/CheckInput.css';
+
+ margin-top: 5px;
+}
+
+.delayContainer {
+ display: flex;
+ flex-grow: 0;
+}
+
+.delayInput {
+ composes: input from '~Components/Form/Input.css';
+
+ width: 150px;
+ height: 30px;
+ border: unset;
+ border-radius: unset;
+ background-color: unset;
+ box-shadow: unset;
+}
+
+.qualityNameContainer {
+ display: flex;
+ flex-grow: 1;
+ margin-bottom: 0;
+ margin-left: 2px;
+ font-weight: normal;
+ line-height: $qualityProfileItemHeight;
+ cursor: pointer;
+}
+
+.qualityName {
+ &.notAllowed {
+ color: #c6c6c6;
+ }
+}
+
+.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;
+}
+
+.isPreview {
+ .qualityName {
+ margin-left: 14px;
+ }
+}
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css.d.ts b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css.d.ts
new file mode 100644
index 000000000..ff6e4ad9e
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.css.d.ts
@@ -0,0 +1,19 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'checkInput': string;
+ 'checkInputContainer': string;
+ 'delayContainer': string;
+ 'delayInput': string;
+ 'dragHandle': string;
+ 'dragIcon': string;
+ 'isDragging': string;
+ 'isInGroup': string;
+ 'isPreview': string;
+ 'notAllowed': string;
+ 'qualityName': string;
+ 'qualityNameContainer': string;
+ 'qualityProfileItem': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.js b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.js
new file mode 100644
index 000000000..68c84f752
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItem.js
@@ -0,0 +1,113 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import CheckInput from 'Components/Form/CheckInput';
+import NumberInput from 'Components/Form/NumberInput';
+import Icon from 'Components/Icon';
+import { icons } from 'Helpers/Props';
+import styles from './DownloadProtocolItem.css';
+
+class DownloadProtocolItem extends Component {
+
+ //
+ // Listeners
+
+ onChange = ({ name, value }) => {
+ const {
+ protocol,
+ onDownloadProtocolItemFieldChange
+ } = this.props;
+
+ onDownloadProtocolItemFieldChange(protocol, name, value);
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ isPreview,
+ name,
+ allowed,
+ delay,
+ isDragging,
+ isOverCurrent,
+ connectDragSource
+ } = this.props;
+
+ return (
+
+
+
+
+
+ {
+ connectDragSource(
+
+
+
+ )
+ }
+
+ );
+ }
+}
+
+DownloadProtocolItem.propTypes = {
+ isPreview: PropTypes.bool,
+ protocol: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ allowed: PropTypes.bool.isRequired,
+ delay: PropTypes.number.isRequired,
+ isDragging: PropTypes.bool.isRequired,
+ isOverCurrent: PropTypes.bool.isRequired,
+ connectDragSource: PropTypes.func,
+ onDownloadProtocolItemFieldChange: PropTypes.func
+};
+
+DownloadProtocolItem.defaultProps = {
+ isPreview: false,
+ isOverCurrent: false,
+ // The drag preview will not connect the drag handle.
+ connectDragSource: (node) => node
+};
+
+export default DownloadProtocolItem;
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css
new file mode 100644
index 000000000..afe4c76ca
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css
@@ -0,0 +1,4 @@
+.dragPreview {
+ width: 480px;
+ opacity: 0.75;
+}
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css.d.ts b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css.d.ts
new file mode 100644
index 000000000..1f1f3c320
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.css.d.ts
@@ -0,0 +1,7 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'dragPreview': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.js b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.js
new file mode 100644
index 000000000..8cb5ca023
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragPreview.js
@@ -0,0 +1,89 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { DragLayer } from 'react-dnd';
+import DragPreviewLayer from 'Components/DragPreviewLayer';
+import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
+import dimensions from 'Styles/Variables/dimensions.js';
+import DownloadProtocolItem from './DownloadProtocolItem';
+import styles from './DownloadProtocolItemDragPreview.css';
+
+const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
+const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
+const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
+const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
+
+function collectDragLayer(monitor) {
+ return {
+ item: monitor.getItem(),
+ itemType: monitor.getItemType(),
+ currentOffset: monitor.getSourceClientOffset()
+ };
+}
+
+class DownloadProtocolItemDragPreview extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ item,
+ itemType,
+ currentOffset
+ } = this.props;
+
+ if (!currentOffset || itemType !== DOWNLOAD_PROTOCOL_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 - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
+ const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
+
+ const style = {
+ position: 'absolute',
+ WebkitTransform: transform,
+ msTransform: transform,
+ transform
+ };
+
+ const {
+ id,
+ name,
+ allowed,
+ delay
+ } = item;
+
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+DownloadProtocolItemDragPreview.propTypes = {
+ item: PropTypes.object,
+ itemType: PropTypes.string,
+ currentOffset: PropTypes.shape({
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired
+ })
+};
+
+export default DragLayer(collectDragLayer)(DownloadProtocolItemDragPreview);
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css
new file mode 100644
index 000000000..0d960aed9
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css
@@ -0,0 +1,18 @@
+.downloadProtocolItemDragSource {
+ padding: $qualityProfileItemDragSourcePadding 0;
+}
+
+.downloadProtocolItemPlaceholder {
+ width: 100%;
+ height: $qualityProfileItemHeight;
+ border: 1px dotted #aaa;
+ border-radius: 4px;
+}
+
+.downloadProtocolItemPlaceholderBefore {
+ margin-bottom: 8px;
+}
+
+.downloadProtocolItemPlaceholderAfter {
+ margin-top: 8px;
+}
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css.d.ts b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css.d.ts
new file mode 100644
index 000000000..5c9e71eb8
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.css.d.ts
@@ -0,0 +1,10 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'downloadProtocolItemDragSource': string;
+ 'downloadProtocolItemPlaceholder': string;
+ 'downloadProtocolItemPlaceholderAfter': string;
+ 'downloadProtocolItemPlaceholderBefore': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.js b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.js
new file mode 100644
index 000000000..cb63c6e9b
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItemDragSource.js
@@ -0,0 +1,188 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { DragSource, DropTarget } from 'react-dnd';
+import { findDOMNode } from 'react-dom';
+import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
+import DownloadProtocolItem from './DownloadProtocolItem';
+import styles from './DownloadProtocolItemDragSource.css';
+
+const downloadProtocolItemDragSource = {
+ beginDrag(props) {
+ const {
+ index,
+ protocol,
+ name,
+ allowed,
+ delay
+ } = props;
+
+ return {
+ index,
+ protocol,
+ name,
+ allowed,
+ delay
+ };
+ },
+
+ endDrag(props, monitor, component) {
+ props.onDownloadProtocolItemDragEnd(monitor.didDrop());
+ }
+};
+
+const downloadProtocolItemDropTarget = {
+ hover(props, monitor, component) {
+ const {
+ index: dragIndex
+ } = monitor.getItem();
+
+ const dropIndex = props.index;
+
+ // Use childNodeIndex to select the correct node to get the middle of so
+ // we don't bounce between above and below causing rapid setState calls.
+ const childNodeIndex = component.props.isOverCurrent && component.props.isDraggingUp ? 1 :0;
+ const componentDOMNode = findDOMNode(component).children[childNodeIndex];
+ const hoverBoundingRect = componentDOMNode.getBoundingClientRect();
+ const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
+ const clientOffset = monitor.getClientOffset();
+ const hoverClientY = clientOffset.y - hoverBoundingRect.top;
+
+ // If we're hovering over a child don't trigger on the parent
+ if (!monitor.isOver({ shallow: true })) {
+ return;
+ }
+
+ // Don't show targets for dropping on self
+ if (dragIndex === dropIndex) {
+ return;
+ }
+
+ let dropPosition = null;
+
+ // Determine drop position based on position over target
+ if (hoverClientY > hoverMiddleY) {
+ dropPosition = 'below';
+ } else if (hoverClientY < hoverMiddleY) {
+ dropPosition = 'above';
+ } else {
+ return;
+ }
+
+ props.onDownloadProtocolItemDragMove({
+ dragIndex,
+ dropIndex,
+ dropPosition
+ });
+ }
+};
+
+function collectDragSource(connect, monitor) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging()
+ };
+}
+
+function collectDropTarget(connect, monitor) {
+ return {
+ connectDropTarget: connect.dropTarget(),
+ isOver: monitor.isOver(),
+ isOverCurrent: monitor.isOver({ shallow: true })
+ };
+}
+
+class DownloadProtocolItemDragSource extends Component {
+
+ //
+ // Render
+
+ render() {
+ const {
+ protocol,
+ name,
+ allowed,
+ delay,
+ index,
+ isDragging,
+ isDraggingUp,
+ isDraggingDown,
+ isOverCurrent,
+ connectDragSource,
+ connectDropTarget,
+ onDownloadProtocolItemFieldChange
+ } = this.props;
+
+ const isBefore = !isDragging && isDraggingUp && isOverCurrent;
+ const isAfter = !isDragging && isDraggingDown && isOverCurrent;
+
+ return connectDropTarget(
+
+ {
+ isBefore &&
+
+ }
+
+
+
+ {
+ isAfter &&
+
+ }
+
+ );
+ }
+}
+
+DownloadProtocolItemDragSource.propTypes = {
+ protocol: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ allowed: PropTypes.bool.isRequired,
+ delay: PropTypes.number.isRequired,
+ index: PropTypes.number.isRequired,
+ isDragging: PropTypes.bool,
+ isDraggingUp: PropTypes.bool,
+ isDraggingDown: PropTypes.bool,
+ isOverCurrent: PropTypes.bool,
+ connectDragSource: PropTypes.func,
+ connectDropTarget: PropTypes.func,
+ onDownloadProtocolItemFieldChange: PropTypes.func.isRequired,
+ onDownloadProtocolItemDragMove: PropTypes.func.isRequired,
+ onDownloadProtocolItemDragEnd: PropTypes.func.isRequired
+};
+
+export default DropTarget(
+ DOWNLOAD_PROTOCOL_ITEM,
+ downloadProtocolItemDropTarget,
+ collectDropTarget
+)(DragSource(
+ DOWNLOAD_PROTOCOL_ITEM,
+ downloadProtocolItemDragSource,
+ collectDragSource
+)(DownloadProtocolItemDragSource));
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css
new file mode 100644
index 000000000..e186bec50
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css
@@ -0,0 +1,24 @@
+.qualities {
+ margin-top: 10px;
+ transition: min-height 200ms;
+ user-select: none;
+}
+
+.headerContainer {
+ display: flex;
+ font-weight: bold;
+ line-height: 35px;
+}
+
+.headerTitle {
+ display: flex;
+ flex-grow: 1;
+}
+
+.headerDelay {
+ display: flex;
+ flex-grow: 0;
+ margin-right: 40px;
+ padding-left: 16px;
+ width: 150px;
+}
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css.d.ts b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css.d.ts
new file mode 100644
index 000000000..9e0963ed9
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.css.d.ts
@@ -0,0 +1,10 @@
+// This file is automatically generated.
+// Please do not change this file!
+interface CssExports {
+ 'headerContainer': string;
+ 'headerDelay': string;
+ 'headerTitle': string;
+ 'qualities': string;
+}
+export const cssExports: CssExports;
+export default cssExports;
diff --git a/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.js b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.js
new file mode 100644
index 000000000..90d379f07
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Delay/DownloadProtocolItems.js
@@ -0,0 +1,150 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import FormGroup from 'Components/Form/FormGroup';
+import FormInputHelpText from 'Components/Form/FormInputHelpText';
+import FormLabel from 'Components/Form/FormLabel';
+import Measure from 'Components/Measure';
+import { sizes } from 'Helpers/Props';
+import DownloadProtocolItemDragPreview from './DownloadProtocolItemDragPreview';
+import DownloadProtocolItemDragSource from './DownloadProtocolItemDragSource';
+import styles from './DownloadProtocolItems.css';
+
+class DownloadProtocolItems extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ height: 0
+ };
+ }
+
+ //
+ // Listeners
+
+ onMeasure = ({ height }) => {
+ this.setState({ height });
+ };
+
+ //
+ // Render
+
+ render() {
+ const {
+ dropIndex,
+ dropPosition,
+ items,
+ errors,
+ warnings,
+ ...otherProps
+ } = this.props;
+
+ const {
+ height
+ } = this.state;
+
+ const isDragging = dropIndex !== null;
+ const isDraggingUp = isDragging && dropPosition === 'above';
+ const isDraggingDown = isDragging && dropPosition === 'below';
+
+ return (
+
+
+ Download Protocols
+
+
+
+
+
+ {
+ errors.map((error, index) => {
+ return (
+
+ );
+ })
+ }
+
+ {
+ warnings.map((warning, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+ Protocol
+
+
+ Delay (minutes)
+
+
+
+ {
+ items.map(({ protocol, name, allowed, delay }, index) => {
+ return (
+
+ );
+ })
+ }
+
+
+
+
+
+
+ );
+ }
+}
+
+DownloadProtocolItems.propTypes = {
+ dragIndex: PropTypes.number,
+ dropIndex: PropTypes.number,
+ dropPosition: PropTypes.string,
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ errors: PropTypes.arrayOf(PropTypes.object),
+ warnings: PropTypes.arrayOf(PropTypes.object)
+};
+
+DownloadProtocolItems.defaultProps = {
+ errors: [],
+ warnings: []
+};
+
+export default DownloadProtocolItems;
diff --git a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
index da1bf7f44..6fd0125c8 100644
--- a/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
+++ b/frontend/src/Settings/Profiles/Delay/EditDelayProfileModalContent.js
@@ -12,49 +12,22 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
-import { inputTypes, kinds } from 'Helpers/Props';
-import { boolSettingShape, numberSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
+import { inputTypes, kinds, sizes } from 'Helpers/Props';
+import { boolSettingShape, numberSettingShape, stringSettingShape, tagSettingShape } from 'Helpers/Props/Shapes/settingShape';
import translate from 'Utilities/String/translate';
+import DownloadProtocolItems from './DownloadProtocolItems';
import styles from './EditDelayProfileModalContent.css';
-const protocolOptions = [
- {
- key: 'preferUsenet',
- get value() {
- return translate('PreferUsenet');
- }
- },
- {
- key: 'preferTorrent',
- get value() {
- return translate('PreferTorrent');
- }
- },
- {
- key: 'onlyUsenet',
- get value() {
- return translate('OnlyUsenet');
- }
- },
- {
- key: 'onlyTorrent',
- get value() {
- return translate('OnlyTorrent');
- }
- }
-];
-
function EditDelayProfileModalContent(props) {
const {
id,
isFetching,
+ isPopulated,
error,
isSaving,
saveError,
item,
- protocol,
onInputChange,
- onProtocolChange,
onSavePress,
onModalClose,
onDeleteDelayProfilePress,
@@ -62,10 +35,8 @@ function EditDelayProfileModalContent(props) {
} = props;
const {
- enableUsenet,
- enableTorrent,
- usenetDelay,
- torrentDelay,
+ name,
+ items,
bypassIfHighestQuality,
bypassIfAboveCustomFormatScore,
minimumCustomFormatScore,
@@ -88,60 +59,35 @@ function EditDelayProfileModalContent(props) {
{
!isFetching && !!error ?
- {translate('UnableToAddANewQualityProfilePleaseTryAgain')}
+ {translate('UnableToAddANewDelayProfilePleaseTryAgain')}
:
null
}
{
- !isFetching && !error ?
+ !isFetching && isPopulated && !error ?