-
+ onBodyMeasure = ({ height }) => {
+
+ if (height > this.state.bodyHeight) {
+ this.setState({ bodyHeight: height });
+ }
+ }
+
+ onFooterMeasure = ({ height }) => {
+ if (height > this.state.footerHeight) {
+ this.setState({ footerHeight: height });
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ editGroups,
+ isFetching,
+ error,
+ isSaving,
+ saveError,
+ qualities,
+ item,
+ isInUse,
+ onInputChange,
+ onCutoffChange,
+ onSavePress,
+ onModalClose,
+ onDeleteQualityProfilePress,
+ ...otherProps
+ } = this.props;
+
+ const {
+ id,
+ name,
+ cutoff,
+ items
+ } = item;
+
+ return (
+
+
+
+ {id ? 'Edit Quality Profile' : 'Add Quality Profile'}
+
+
+
+
+
+
+ {
+ isFetching &&
+
+ }
+
+ {
+ !isFetching && !!error &&
+
Unable to add a new quality profile, please try again.
+ }
+
+ {
+ !isFetching && !error &&
+
+
+ }
- }
+
+
-
+
+ {
+ id &&
+
+
+
+ }
-
- Save
-
-
-
- );
+
+
+
+ Save
+
+
+
+
+ );
+ }
}
EditQualityProfileModalContent.propTypes = {
+ editGroups: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
@@ -142,6 +244,7 @@ EditQualityProfileModalContent.propTypes = {
onInputChange: PropTypes.func.isRequired,
onCutoffChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
+ onContentHeightChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteQualityProfilePress: PropTypes.func
};
diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
index 9de5080ca..c06392fe1 100644
--- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
+++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContentConnector.js
@@ -8,6 +8,29 @@ import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile }
import connectSection from 'Store/connectSection';
import EditQualityProfileModalContent from './EditQualityProfileModalContent';
+function getQualityItemGroupId(qualityProfile) {
+ // Get items with an `id` and filter out null/undefined values
+ const ids = _.filter(_.map(qualityProfile.items.value, 'id'), (i) => i != null);
+
+ return Math.max(1000, ...ids) + 1;
+}
+
+function parseIndex(index) {
+ const split = index.split('.');
+
+ if (split.length === 1) {
+ return [
+ null,
+ parseInt(split[0]) - 1
+ ];
+ }
+
+ return [
+ parseInt(split[0]) - 1,
+ parseInt(split[1]) - 1
+ ];
+}
+
function createQualitiesSelector() {
return createSelector(
createProviderSettingsSelector(),
@@ -17,12 +40,19 @@ function createQualitiesSelector() {
return [];
}
- return _.reduceRight(items.value, (result, { allowed, quality }) => {
+ return _.reduceRight(items.value, (result, { allowed, id, name, quality }) => {
if (allowed) {
- result.push({
- key: quality.id,
- value: quality.name
- });
+ if (id) {
+ result.push({
+ key: id,
+ value: name
+ });
+ } else {
+ result.push({
+ key: quality.id,
+ value: quality.name
+ });
+ }
}
return result;
@@ -61,8 +91,10 @@ class EditQualityProfileModalContentConnector extends Component {
super(props, context);
this.state = {
- dragIndex: null,
- dropIndex: null
+ dragQualityIndex: null,
+ dropQualityIndex: null,
+ dropPosition: null,
+ editGroups: true
};
}
@@ -78,6 +110,33 @@ class EditQualityProfileModalContentConnector extends Component {
}
}
+ //
+ // Control
+
+ ensureCutoff = (qualityProfile) => {
+ const cutoff = qualityProfile.cutoff.value;
+
+ const cutoffItem = _.find(qualityProfile.items.value, (i) => {
+ if (!cutoff) {
+ return false;
+ }
+
+ return i.id === cutoff || (i.quality && i.quality.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.items.value, { allowed: true });
+ let cutoffId = null;
+
+ if (firstAllowed) {
+ cutoffId = firstAllowed.quality ? firstAllowed.quality.id : firstAllowed.id;
+ }
+
+ this.props.setQualityProfileValue({ name: 'cutoff', value: cutoffId });
+ }
+ }
+
//
// Listeners
@@ -87,9 +146,17 @@ class EditQualityProfileModalContentConnector extends Component {
onCutoffChange = ({ name, value }) => {
const id = parseInt(value);
- const item = _.find(this.props.item.items.value, (i) => i.quality.id === id);
+ const item = _.find(this.props.item.items.value, (i) => {
+ if (i.quality) {
+ return i.quality.id === id;
+ }
- this.props.setQualityProfileValue({ name, value: item.quality });
+ return i.id === id;
+ });
+
+ const cutoffId = item.quality ? item.quality.id : item.id;
+
+ this.props.setQualityProfileValue({ name, value: cutoffId });
}
onSavePress = () => {
@@ -98,58 +165,239 @@ class EditQualityProfileModalContentConnector extends Component {
onQualityProfileItemAllowedChange = (id, allowed) => {
const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const item = _.find(qualityProfile.items.value, (i) => i.quality && i.quality.id === id);
- const item = _.find(qualityProfile.items.value, (i) => i.quality.id === id);
item.allowed = allowed;
this.props.setQualityProfileValue({
name: 'items',
- value: qualityProfile.items.value
+ value: items
});
- const cutoff = qualityProfile.cutoff.value;
-
- // If the cutoff isn't allowed anymore or there isn't a cutoff set one
- if (!cutoff || !_.find(qualityProfile.items.value, (i) => i.quality.id === cutoff.id).allowed) {
- const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
-
- this.props.setQualityProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.quality : null });
- }
+ this.ensureCutoff(qualityProfile);
}
- onQualityProfileItemDragMove = (dragIndex, dropIndex) => {
- if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
+ onItemGroupAllowedChange = (id, allowed) => {
+ const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const item = _.find(qualityProfile.items.value, (i) => i.id === id);
+
+ item.allowed = allowed;
+
+ // Update each item in the group (for consistency only)
+ item.items.forEach((i) => {
+ i.allowed = allowed;
+ });
+
+ this.props.setQualityProfileValue({
+ name: 'items',
+ value: items
+ });
+
+ this.ensureCutoff(qualityProfile);
+ }
+
+ onItemGroupNameChange = (id, name) => {
+ const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const group = _.find(items, (i) => i.id === id);
+
+ group.name = name;
+
+ this.props.setQualityProfileValue({
+ name: 'items',
+ value: items
+ });
+ }
+
+ onCreateGroupPress = (id) => {
+ const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const item = _.find(items, (i) => i.quality && i.quality.id === id);
+ const index = items.indexOf(item);
+ const groupId = getQualityItemGroupId(qualityProfile);
+
+ const group = {
+ id: groupId,
+ name: item.quality.name,
+ allowed: item.allowed,
+ items: [
+ item
+ ]
+ };
+
+ // Add the group in the same location the quality item was in.
+ items.splice(index, 1, group);
+
+ this.props.setQualityProfileValue({
+ name: 'items',
+ value: items
+ });
+
+ this.ensureCutoff(qualityProfile);
+ }
+
+ onDeleteGroupPress = (id) => {
+ const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const group = _.find(items, (i) => i.id === id);
+ const index = items.indexOf(group);
+
+ // Add the items in the same location the group was in
+ items.splice(index, 1, ...group.items);
+
+ this.props.setQualityProfileValue({
+ name: 'items',
+ value: items
+ });
+
+ this.ensureCutoff(qualityProfile);
+ }
+
+ onQualityProfileItemDragMove = (options) => {
+ const {
+ dragQualityIndex,
+ dropQualityIndex,
+ dropPosition
+ } = options;
+
+ const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
+ const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
+
+ if (
+ (dropPosition === 'below' && dropItemIndex - 1 === dragItemIndex) ||
+ (dropPosition === 'above' && dropItemIndex + 1 === dragItemIndex)
+ ) {
+ if (
+ this.state.dragQualityIndex != null &&
+ this.state.dropQualityIndex != null &&
+ this.state.dropPosition != null
+ ) {
+ this.setState({
+ dragQualityIndex: null,
+ dropQualityIndex: null,
+ dropPosition: null
+ });
+ }
+
+ return;
+ }
+
+ let adjustedDropQualityIndex = dropQualityIndex;
+
+ // Correct dragging out of a group to the position above
+ if (
+ dropPosition === 'above' &&
+ dragGroupIndex !== dropGroupIndex &&
+ dropGroupIndex != null
+ ) {
+ // Add 1 to the group index and 2 to the item index so it's inserted above in the correct group
+ adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex + 2}`;
+ }
+
+ // Correct inserting above outside a group
+ if (
+ dropPosition === 'above' &&
+ dragGroupIndex !== dropGroupIndex &&
+ dropGroupIndex == null
+ ) {
+ // Add 2 to the item index so it's entered in the correct place
+ adjustedDropQualityIndex = `${dropItemIndex + 2}`;
+ }
+
+ // Correct inserting below a quality within the same group (when moving a lower item)
+ if (
+ dropPosition === 'below' &&
+ dragGroupIndex === dropGroupIndex &&
+ dropGroupIndex != null &&
+ dragItemIndex < dropItemIndex
+ ) {
+ // Add 1 to the group index leave the item index
+ adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex}`;
+ }
+
+ // Correct inserting below a quality outside a group (when moving a lower item)
+ if (
+ dropPosition === 'below' &&
+ dragGroupIndex === dropGroupIndex &&
+ dropGroupIndex == null &&
+ dragItemIndex < dropItemIndex
+ ) {
+ // Leave the item index so it's inserted below the item
+ adjustedDropQualityIndex = `${dropItemIndex}`;
+ }
+
+ if (
+ dragQualityIndex !== this.state.dragQualityIndex ||
+ adjustedDropQualityIndex !== this.state.dropQualityIndex ||
+ dropPosition !== this.state.dropPosition
+ ) {
this.setState({
- dragIndex,
- dropIndex
+ dragQualityIndex,
+ dropQualityIndex: adjustedDropQualityIndex,
+ dropPosition
});
}
}
- onQualityProfileItemDragEnd = ({ id }, didDrop) => {
+ onQualityProfileItemDragEnd = (didDrop) => {
const {
- dragIndex,
- dropIndex
+ dragQualityIndex,
+ dropQualityIndex
} = this.state;
- if (didDrop && dropIndex !== null) {
+ if (didDrop && dropQualityIndex != null) {
const qualityProfile = _.cloneDeep(this.props.item);
+ const items = qualityProfile.items.value;
+ const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
+ const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
- const items = qualityProfile.items.value.splice(dragIndex, 1);
- qualityProfile.items.value.splice(dropIndex, 0, items[0]);
+ let item = null;
+ let dropGroup = null;
+
+ // Get the group before moving anything so we know the correct place to drop it.
+ if (dropGroupIndex != null) {
+ dropGroup = items[dropGroupIndex];
+ }
+
+ if (dragGroupIndex == null) {
+ item = items.splice(dragItemIndex, 1)[0];
+ } else {
+ const group = items[dragGroupIndex];
+ item = group.items.splice(dragItemIndex, 1)[0];
+
+ // If the group is now empty, destroy it.
+ if (!group.items.length) {
+ items.splice(dragGroupIndex, 1);
+ }
+ }
+
+ if (dropGroupIndex == null) {
+ items.splice(dropItemIndex, 0, item);
+ } else {
+ dropGroup.items.splice(dropItemIndex, 0, item);
+ }
this.props.setQualityProfileValue({
name: 'items',
- value: qualityProfile.items.value
+ value: items
});
+
+ this.ensureCutoff(qualityProfile);
}
this.setState({
- dragIndex: null,
- dropIndex: null
+ dragQualityIndex: null,
+ dropQualityIndex: null,
+ dropPosition: null
});
}
+ onToggleEditGroupsMode = () => {
+ this.setState({ editGroups: !this.state.editGroups });
+ }
+
//
// Render
@@ -165,9 +413,14 @@ class EditQualityProfileModalContentConnector extends Component {
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
onCutoffChange={this.onCutoffChange}
+ onCreateGroupPress={this.onCreateGroupPress}
+ onDeleteGroupPress={this.onDeleteGroupPress}
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
+ onItemGroupAllowedChange={this.onItemGroupAllowedChange}
+ onItemGroupNameChange={this.onItemGroupNameChange}
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
+ onToggleEditGroupsMode={this.onToggleEditGroupsMode}
/>
);
}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.css b/frontend/src/Settings/Profiles/Quality/QualityProfile.css
index 1ac9041bd..02a5999cc 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfile.css
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.css
@@ -17,3 +17,10 @@
flex-wrap: wrap;
margin-top: 5px;
}
+
+.tooltipLabel {
+ composes: label from 'Components/Label.css';
+
+ margin: 0;
+ border: none;
+}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfile.js b/frontend/src/Settings/Profiles/Quality/QualityProfile.js
index 211656ab1..8fe5810f5 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfile.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfile.js
@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { kinds } from 'Helpers/Props';
+import { kinds, tooltipPositions } from 'Helpers/Props';
import Card from 'Components/Card';
import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal';
+import Tooltip from 'Components/Tooltip/Tooltip';
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
import styles from './QualityProfile.css';
@@ -75,16 +76,54 @@ class QualityProfile extends Component {
return null;
}
- const isCutoff = item.quality.id === cutoff.id;
+ if (item.quality) {
+ const isCutoff = item.quality.id === cutoff;
+
+ return (
+
+ );
+ }
+
+ const isCutoff = item.id === cutoff;
return (
-
+
+ {item.name}
+
+ }
+ tooltip={
+
+ {
+ item.items.map((groupItem) => {
+ return (
+
+ );
+ })
+ }
+
+ }
+ kind={kinds.INVERSE}
+ position={tooltipPositions.TOP}
+ />
);
})
}
@@ -115,7 +154,7 @@ class QualityProfile extends Component {
QualityProfile.propTypes = {
id: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
- cutoff: PropTypes.object.isRequired,
+ cutoff: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isDeleting: PropTypes.bool.isRequired,
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css
index 90d48a2c5..7e6370ff8 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css
@@ -5,25 +5,56 @@
border: 1px solid #aaa;
border-radius: 4px;
background: #fafafa;
+
+ &.isInGroup {
+ border-style: dashed;
+ }
}
-.checkContainer {
+.checkInputContainer {
position: relative;
margin-right: 4px;
- margin-bottom: 7px;
+ margin-bottom: 5px;
margin-left: 8px;
}
-.qualityName {
+.checkInput {
+ composes: input from 'Components/Form/CheckInput.css';
+
+ margin-top: 5px;
+}
+
+.qualityNameContainer {
display: flex;
flex-grow: 1;
margin-bottom: 0;
margin-left: 2px;
font-weight: normal;
- line-height: 36px;
+ line-height: $qualityProfileItemHeight;
cursor: pointer;
}
+.qualityName {
+ &.isInGroup {
+ margin-left: 14px;
+ }
+
+ &.notAllowed {
+ color: #c6c6c6;
+ }
+}
+
+.createGroupButton {
+ composes: buton from 'Components/Link/IconButton.css';
+
+ display: flex;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-right: 5px;
+ margin-left: 8px;
+ width: 20px;
+}
+
.dragHandle {
display: flex;
align-items: center;
@@ -42,3 +73,13 @@
.isDragging {
opacity: 0.25;
}
+
+.isPreview {
+ .qualityName {
+ margin-left: 14px;
+
+ &.isInGroup {
+ margin-left: 28px;
+ }
+ }
+}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
index 1a0fa0114..8161e7061 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
+import IconButton from 'Components/Link/IconButton';
import CheckInput from 'Components/Form/CheckInput';
import styles from './QualityProfileItem.css';
@@ -20,14 +21,27 @@ class QualityProfileItem extends Component {
onQualityProfileItemAllowedChange(qualityId, value);
}
+ onCreateGroupPress = () => {
+ const {
+ qualityId,
+ onCreateGroupPress
+ } = this.props;
+
+ onCreateGroupPress(qualityId);
+ }
+
//
// Render
render() {
const {
+ editGroups,
+ isPreview,
+ groupId,
name,
allowed,
isDragging,
+ isOverCurrent,
connectDragSource
} = this.props;
@@ -36,18 +50,44 @@ class QualityProfileItem extends Component {
className={classNames(
styles.qualityProfileItem,
isDragging && styles.isDragging,
+ isPreview && styles.isPreview,
+ isOverCurrent && styles.isOverCurrent,
+ groupId && styles.isInGroup
)}
>
{
@@ -55,6 +95,7 @@ class QualityProfileItem extends Component {
@@ -66,16 +107,23 @@ class QualityProfileItem extends Component {
}
QualityProfileItem.propTypes = {
+ editGroups: PropTypes.bool,
+ isPreview: PropTypes.bool,
+ groupId: PropTypes.number,
qualityId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
- sortIndex: PropTypes.number.isRequired,
isDragging: PropTypes.bool.isRequired,
+ isOverCurrent: PropTypes.bool.isRequired,
+ isInGroup: PropTypes.bool,
connectDragSource: PropTypes.func,
+ onCreateGroupPress: PropTypes.func,
onQualityProfileItemAllowedChange: PropTypes.func
};
QualityProfileItem.defaultProps = {
+ isPreview: false,
+ isOverCurrent: false,
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js
index 1fd249714..e0c6e8e8c 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragPreview.js
@@ -7,8 +7,8 @@ import DragPreviewLayer from 'Components/DragPreviewLayer';
import QualityProfileItem from './QualityProfileItem';
import styles from './QualityProfileItemDragPreview.css';
-const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
-const formLabelWidth = parseInt(dimensions.formLabelWidth);
+const formGroupExtraSmallWidth = parseInt(dimensions.formGroupExtraSmallWidth);
+const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
@@ -40,7 +40,7 @@ class QualityProfileItemDragPreview extends Component {
// list item and the preview is wider than the drag handle.
const { x, y } = currentOffset;
- const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
+ const handleOffset = formGroupExtraSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
const style = {
@@ -51,12 +51,15 @@ class QualityProfileItemDragPreview extends Component {
};
const {
+ editGroups,
+ groupId,
qualityId,
name,
- allowed,
- sortIndex
+ allowed
} = item;
+ // TODO: Show a different preview for groups
+
return (
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css
index 5b9f36fe9..d5061cc95 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.css
@@ -1,10 +1,10 @@
.qualityProfileItemDragSource {
- padding: 4px 0;
+ padding: $qualityProfileItemDragSourcePadding 0;
}
.qualityProfileItemPlaceholder {
width: 100%;
- height: 36px;
+ height: $qualityProfileItemHeight;
border: 1px dotted #aaa;
border-radius: 4px;
}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js
index ed8adc107..0e1838eb3 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js
@@ -5,44 +5,86 @@ import { DragSource, DropTarget } from 'react-dnd';
import classNames from 'classnames';
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
import QualityProfileItem from './QualityProfileItem';
+import QualityProfileItemGroup from './QualityProfileItemGroup';
import styles from './QualityProfileItemDragSource.css';
const qualityProfileItemDragSource = {
- beginDrag({ qualityId, name, allowed, sortIndex }) {
- return {
+ beginDrag(props) {
+ const {
+ editGroups,
+ qualityIndex,
+ groupId,
qualityId,
name,
- allowed,
- sortIndex
+ allowed
+ } = props;
+
+ return {
+ editGroups,
+ qualityIndex,
+ groupId,
+ qualityId,
+ isGroup: !qualityId,
+ name,
+ allowed
};
},
endDrag(props, monitor, component) {
- props.onQualityProfileItemDragEnd(monitor.getItem(), monitor.didDrop());
+ props.onQualityProfileItemDragEnd(monitor.didDrop());
}
};
const qualityProfileItemDropTarget = {
hover(props, monitor, component) {
- const dragIndex = monitor.getItem().sortIndex;
- const hoverIndex = props.sortIndex;
+ const {
+ qualityIndex: dragQualityIndex,
+ isGroup: isDragGroup
+ } = monitor.getItem();
- const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
+ const dropQualityIndex = props.qualityIndex;
+ const isDropGroupItem = !!(props.qualityId && props.groupId);
+
+ // 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;
- // Moving up, only trigger if drag position is above 50%
- if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
+ // If we're hovering over a child don't trigger on the parent
+ if (!monitor.isOver({ shallow: true })) {
return;
}
- // Moving down, only trigger if drag position is below 50%
- if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
+ // Don't show targets for dropping on self
+ if (dragQualityIndex === dropQualityIndex) {
return;
}
- props.onQualityProfileItemDragMove(dragIndex, hoverIndex);
+ // Don't allow a group to be dropped inside a group
+ if (isDragGroup && isDropGroupItem) {
+ 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.onQualityProfileItemDragMove({
+ dragQualityIndex,
+ dropQualityIndex,
+ dropPosition
+ });
}
};
@@ -56,7 +98,8 @@ function collectDragSource(connect, monitor) {
function collectDropTarget(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
- isOver: monitor.isOver()
+ isOver: monitor.isOver(),
+ isOverCurrent: monitor.isOver({ shallow: true })
};
}
@@ -67,25 +110,30 @@ class QualityProfileItemDragSource extends Component {
render() {
const {
+ editGroups,
+ groupId,
qualityId,
name,
allowed,
- sortIndex,
+ items,
+ qualityIndex,
isDragging,
isDraggingUp,
isDraggingDown,
- isOver,
+ isOverCurrent,
connectDragSource,
connectDropTarget,
- onQualityProfileItemAllowedChange
+ onCreateGroupPress,
+ onDeleteGroupPress,
+ onQualityProfileItemAllowedChange,
+ onItemGroupAllowedChange,
+ onItemGroupNameChange,
+ onQualityProfileItemDragMove,
+ onQualityProfileItemDragEnd
} = this.props;
- const isBefore = !isDragging && isDraggingUp && isOver;
- const isAfter = !isDragging && isDraggingDown && isOver;
-
- // if (isDragging && !isOver) {
- // return null;
- // }
+ const isBefore = !isDragging && isDraggingUp && isOverCurrent;
+ const isAfter = !isDragging && isDraggingDown && isOverCurrent;
return connectDropTarget(
}
-
+ {
+ !!groupId && qualityId == null &&
+
+ }
+
+ {
+ qualityId != null &&
+
+ }
{
isAfter &&
@@ -131,17 +207,25 @@ class QualityProfileItemDragSource extends Component {
}
QualityProfileItemDragSource.propTypes = {
- qualityId: PropTypes.number.isRequired,
+ editGroups: PropTypes.bool.isRequired,
+ groupId: PropTypes.number,
+ qualityId: PropTypes.number,
name: PropTypes.string.isRequired,
allowed: PropTypes.bool.isRequired,
- sortIndex: PropTypes.number.isRequired,
+ items: PropTypes.arrayOf(PropTypes.object),
+ qualityIndex: PropTypes.string.isRequired,
isDragging: PropTypes.bool,
isDraggingUp: PropTypes.bool,
isDraggingDown: PropTypes.bool,
- isOver: PropTypes.bool,
+ isOverCurrent: PropTypes.bool,
+ isInGroup: PropTypes.bool,
connectDragSource: PropTypes.func,
connectDropTarget: PropTypes.func,
+ onCreateGroupPress: PropTypes.func,
+ onDeleteGroupPress: PropTypes.func,
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
+ onItemGroupAllowedChange: PropTypes.func,
+ onItemGroupNameChange: PropTypes.func,
onQualityProfileItemDragMove: PropTypes.func.isRequired,
onQualityProfileItemDragEnd: PropTypes.func.isRequired
};
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css
new file mode 100644
index 000000000..dc4d40e53
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css
@@ -0,0 +1,105 @@
+.qualityProfileItemGroup {
+ width: 100%;
+ border: 1px solid #aaa;
+ border-radius: 4px;
+ background: #fafafa;
+
+ &.editGroups {
+ background: #fcfcfc;
+ }
+}
+
+.qualityProfileItemGroupInfo {
+ display: flex;
+ align-items: stretch;
+ width: 100%;
+}
+
+.checkInputContainer {
+ composes: checkInputContainer from './QualityProfileItem.css';
+
+ display: flex;
+ align-items: center;
+}
+
+.checkInput {
+ composes: checkInput from './QualityProfileItem.css';
+}
+
+.nameInput {
+ composes: text from 'Components/Form/TextInput.css';
+
+ margin-top: 4px;
+ margin-right: 10px;
+}
+
+.nameContainer {
+ display: flex;
+ align-items: center;
+ flex-grow: 1;
+}
+
+.name {
+ flex-shrink: 0;
+
+ &.notAllowed {
+ color: #c6c6c6;
+ }
+}
+
+.groupQualities {
+ display: flex;
+ justify-content: flex-end;
+ flex-grow: 1;
+ flex-wrap: wrap;
+ margin: 2px 0 2px 10px;
+}
+
+.qualityNameContainer {
+ display: flex;
+ align-items: stretch;
+ flex-grow: 1;
+ margin-bottom: 0;
+ margin-left: 2px;
+ font-weight: normal;
+}
+
+.qualityNameLabel {
+ composes: qualityNameContainer;
+
+ cursor: pointer;
+}
+
+.deleteGroupButton {
+ composes: buton from 'Components/Link/IconButton.css';
+
+ display: flex;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-right: 5px;
+ margin-left: 8px;
+ width: 20px;
+}
+
+.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;
+}
+
+.items {
+ margin: 0 50px 0 35px;
+}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js
new file mode 100644
index 000000000..34008b1ec
--- /dev/null
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js
@@ -0,0 +1,200 @@
+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 Label from 'Components/Label';
+import IconButton from 'Components/Link/IconButton';
+import CheckInput from 'Components/Form/CheckInput';
+import TextInput from 'Components/Form/TextInput';
+import QualityProfileItemDragSource from './QualityProfileItemDragSource';
+import styles from './QualityProfileItemGroup.css';
+
+class QualityProfileItemGroup extends Component {
+
+ //
+ // Listeners
+
+ onAllowedChange = ({ value }) => {
+ const {
+ groupId,
+ onItemGroupAllowedChange
+ } = this.props;
+
+ onItemGroupAllowedChange(groupId, value);
+ }
+
+ onNameChange = ({ value }) => {
+ const {
+ groupId,
+ onItemGroupNameChange
+ } = this.props;
+
+ onItemGroupNameChange(groupId, value);
+ }
+
+ onDeleteGroupPress = ({ value }) => {
+ const {
+ groupId,
+ onDeleteGroupPress
+ } = this.props;
+
+ onDeleteGroupPress(groupId, value);
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ editGroups,
+ groupId,
+ name,
+ allowed,
+ items,
+ qualityIndex,
+ isDragging,
+ isDraggingUp,
+ isDraggingDown,
+ connectDragSource,
+ onQualityProfileItemAllowedChange,
+ onQualityProfileItemDragMove,
+ onQualityProfileItemDragEnd
+ } = this.props;
+
+ return (
+
+
+ {
+ editGroups &&
+
+
+
+
+
+ }
+
+ {
+ !editGroups &&
+
+ }
+
+ {
+ connectDragSource(
+
+
+
+ )
+ }
+
+
+ {
+ editGroups &&
+
+ {
+ items.map(({ quality }, index) => {
+ return (
+
+ );
+ }).reverse()
+ }
+
+ }
+
+ );
+ }
+}
+
+QualityProfileItemGroup.propTypes = {
+ editGroups: PropTypes.bool,
+ groupId: PropTypes.number.isRequired,
+ name: PropTypes.string.isRequired,
+ allowed: PropTypes.bool.isRequired,
+ items: PropTypes.arrayOf(PropTypes.object).isRequired,
+ qualityIndex: PropTypes.string.isRequired,
+ isDragging: PropTypes.bool.isRequired,
+ isDraggingUp: PropTypes.bool.isRequired,
+ isDraggingDown: PropTypes.bool.isRequired,
+ connectDragSource: PropTypes.func,
+ onItemGroupAllowedChange: PropTypes.func.isRequired,
+ onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
+ onItemGroupNameChange: PropTypes.func.isRequired,
+ onDeleteGroupPress: PropTypes.func.isRequired,
+ onQualityProfileItemDragMove: PropTypes.func.isRequired,
+ onQualityProfileItemDragEnd: PropTypes.func.isRequired
+};
+
+QualityProfileItemGroup.defaultProps = {
+ // The drag preview will not connect the drag handle.
+ connectDragSource: (node) => node
+};
+
+export default QualityProfileItemGroup;
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css
index 344df5b08..6b4268de9 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css
@@ -1,6 +1,15 @@
+.editGroupsButton {
+ composes: button from 'Components/Link/Button.css';
+
+ margin-top: 10px;
+}
+
+.editGroupsButtonIcon {
+ margin-right: 8px;
+}
+
.qualities {
margin-top: 10px;
- /* TODO: This should consider the number of qualities in the list */
- min-height: 550px;
+ transition: min-height 200ms;
user-select: none;
}
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js
index 5a58da630..e2bf1e3f9 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js
@@ -1,5 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
+import Measure from 'react-measure';
+import { icons, kinds, sizes } from 'Helpers/Props';
+import Icon from 'Components/Icon';
+import Button from 'Components/Link/Button';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText';
@@ -9,26 +13,69 @@ import styles from './QualityProfileItems.css';
class QualityProfileItems extends Component {
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ qualitiesHeight: 0,
+ qualitiesHeightEditGroups: 0
+ };
+ }
+
+ componentDidMount() {
+ this.props.onToggleEditGroupsMode();
+ }
+
+ //
+ // Listeners
+
+ onMeasure = ({ height }) => {
+ if (this.props.editGroups) {
+ this.setState({
+ qualitiesHeightEditGroups: height
+ });
+ } else {
+ this.setState({ qualitiesHeight: height });
+ }
+ }
+
+ onToggleEditGroupsMode = () => {
+ this.props.onToggleEditGroupsMode();
+ }
+
//
// Render
render() {
const {
- dragIndex,
- dropIndex,
+ editGroups,
+ dropQualityIndex,
+ dropPosition,
qualityProfileItems,
errors,
warnings,
...otherProps
} = this.props;
- const isDragging = dropIndex !== null;
- const isDraggingUp = isDragging && dropIndex > dragIndex;
- const isDraggingDown = isDragging && dropIndex < dragIndex;
+ const {
+ qualitiesHeight,
+ qualitiesHeightEditGroups
+ } = this.state;
+
+ const isDragging = dropQualityIndex !== null;
+ const isDraggingUp = isDragging && dropPosition === 'above';
+ const isDraggingDown = isDragging && dropPosition === 'below';
+ const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight;
return (
-
- Qualities
+
+
+ Qualities
+
+
- {
- qualityProfileItems.map(({ allowed, quality }, index) => {
- return (
-
- );
- }).reverse()
- }
+
+
+
+
+
+ {
+ qualityProfileItems.map(({ id, name, allowed, quality, items }, index) => {
+ const identifier = quality ? quality.id : id;
+
+ return (
+
+ );
+ }).reverse()
+ }
+
+
+
+
);
@@ -88,11 +167,14 @@ class QualityProfileItems extends Component {
}
QualityProfileItems.propTypes = {
- dragIndex: PropTypes.number,
- dropIndex: PropTypes.number,
+ editGroups: PropTypes.bool.isRequired,
+ dragQualityIndex: PropTypes.string,
+ dropQualityIndex: PropTypes.string,
+ dropPosition: PropTypes.string,
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
errors: PropTypes.arrayOf(PropTypes.object),
- warnings: PropTypes.arrayOf(PropTypes.object)
+ warnings: PropTypes.arrayOf(PropTypes.object),
+ onToggleEditGroupsMode: PropTypes.func.isRequired
};
QualityProfileItems.defaultProps = {
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js
index 75f049695..679cc9ed4 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfiles.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfiles.js
@@ -91,7 +91,6 @@ class QualityProfiles extends Component {
}
QualityProfiles.propTypes = {
- advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js
index 4bb1529ee..bb7fdfd71 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfilesConnector.js
@@ -7,11 +7,9 @@ import QualityProfiles from './QualityProfiles';
function createMapStateToProps() {
return createSelector(
- (state) => state.settings.advancedSettings,
(state) => state.settings.qualityProfiles,
- (advancedSettings, qualityProfiles) => {
+ (qualityProfiles) => {
return {
- advancedSettings,
...qualityProfiles
};
}
diff --git a/frontend/src/Store/Actions/Creators/createFetchHandler.js b/frontend/src/Store/Actions/Creators/createFetchHandler.js
index 5bf31b92e..e0f572dbc 100644
--- a/frontend/src/Store/Actions/Creators/createFetchHandler.js
+++ b/frontend/src/Store/Actions/Creators/createFetchHandler.js
@@ -1,5 +1,5 @@
-import $ from 'jquery';
import { batchActions } from 'redux-batched-actions';
+import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update, updateItem } from '../baseActions';
function createFetchHandler(section, url) {
@@ -12,13 +12,13 @@ function createFetchHandler(section, url) {
...otherPayload
} = payload;
- const promise = $.ajax({
+ const { request, abortRequest } = createAjaxRequest({
url: id == null ? url : `${url}/${id}`,
data: otherPayload,
traditional: true
});
- promise.done((data) => {
+ request.done((data) => {
dispatch(batchActions([
id == null ? update({ section, data }) : updateItem({ section, ...data }),
@@ -31,14 +31,16 @@ function createFetchHandler(section, url) {
]));
});
- promise.fail((xhr) => {
+ request.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
- error: xhr
+ error: xhr.aborted ? null : xhr
}));
});
+
+ return abortRequest;
};
};
}
diff --git a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
index 6b6d89314..7d9f88ee0 100644
--- a/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
+++ b/frontend/src/Store/Actions/Creators/createSaveProviderHandler.js
@@ -37,8 +37,8 @@ function createSaveProviderHandler(section, url, getFromState) {
ajaxOptions.method = 'PUT';
}
- const { request, abortRequest } = createAjaxRequest()(ajaxOptions);
-
+ const { request, abortRequest } = createAjaxRequest(ajaxOptions);
+
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
diff --git a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
index f3cf354bc..352eec001 100644
--- a/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
+++ b/frontend/src/Store/Actions/Creators/createTestProviderHandler.js
@@ -30,8 +30,8 @@ function createTestProviderHandler(section, url, getFromState) {
data: JSON.stringify(testData)
};
- const { request, abortRequest } = createAjaxRequest()(ajaxOptions);
-
+ const { request, abortRequest } = createAjaxRequest(ajaxOptions);
+
abortCurrentRequests[section] = abortRequest;
request.done((data) => {
diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js
index 2c7a4dc50..fd615fd7e 100644
--- a/frontend/src/Store/Actions/actionTypes.js
+++ b/frontend/src/Store/Actions/actionTypes.js
@@ -113,6 +113,7 @@ export const ALBUM_HISTORY_MARK_AS_FAILED = 'ALBUM_HISTORY_MARK_AS_FAILED';
// Releases
export const FETCH_RELEASES = 'FETCH_RELEASES';
+export const CANCEL_FETCH_RELEASES = 'CANCEL_FETCH_RELEASES';
export const SET_RELEASES_SORT = 'SET_RELEASES_SORT';
export const CLEAR_RELEASES = 'CLEAR_RELEASES';
export const GRAB_RELEASE = 'GRAB_RELEASE';
diff --git a/frontend/src/Store/Actions/addArtistActionHandlers.js b/frontend/src/Store/Actions/addArtistActionHandlers.js
index 09ca0f9bf..0d811990b 100644
--- a/frontend/src/Store/Actions/addArtistActionHandlers.js
+++ b/frontend/src/Store/Actions/addArtistActionHandlers.js
@@ -18,7 +18,7 @@ const addArtistActionHandlers = {
abortCurrentRequest();
}
- const { request, abortRequest } = createAjaxRequest()({
+ const { request, abortRequest } = createAjaxRequest({
url: '/artist/lookup',
data: {
term: payload.term
diff --git a/frontend/src/Store/Actions/releaseActionHandlers.js b/frontend/src/Store/Actions/releaseActionHandlers.js
index 15ebaa2b0..6fc0520f5 100644
--- a/frontend/src/Store/Actions/releaseActionHandlers.js
+++ b/frontend/src/Store/Actions/releaseActionHandlers.js
@@ -3,10 +3,27 @@ import createFetchHandler from './Creators/createFetchHandler';
import * as types from './actionTypes';
import { updateRelease } from './releaseActions';
+let abortCurrentRequest = null;
const section = 'releases';
+const fetchReleases = createFetchHandler(section, '/release');
+
const releaseActionHandlers = {
- [types.FETCH_RELEASES]: createFetchHandler(section, '/release'),
+ [types.FETCH_RELEASES]: function(payload) {
+ return function(dispatch, getState) {
+ const abortRequest = fetchReleases(payload)(dispatch, getState);
+
+ abortCurrentRequest = abortRequest;
+ };
+ },
+
+ [types.CANCEL_FETCH_RELEASES]: function(payload) {
+ return function(dispatch, getState) {
+ if (abortCurrentRequest) {
+ abortCurrentRequest = abortCurrentRequest();
+ }
+ };
+ },
[types.GRAB_RELEASE]: function(payload) {
return function(dispatch, getState) {
diff --git a/frontend/src/Store/Actions/releaseActions.js b/frontend/src/Store/Actions/releaseActions.js
index 580ffe804..065ff1253 100644
--- a/frontend/src/Store/Actions/releaseActions.js
+++ b/frontend/src/Store/Actions/releaseActions.js
@@ -3,6 +3,7 @@ import * as types from './actionTypes';
import releaseActionHandlers from './releaseActionHandlers';
export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES];
+export const cancelFetchReleases = releaseActionHandlers[types.CANCEL_FETCH_RELEASES];
export const setReleasesSort = createAction(types.SET_RELEASES_SORT);
export const clearReleases = createAction(types.CLEAR_RELEASES);
export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE];
diff --git a/frontend/src/Store/Reducers/episodeReducers.js b/frontend/src/Store/Reducers/episodeReducers.js
index 479ea3fe0..dcd572368 100644
--- a/frontend/src/Store/Reducers/episodeReducers.js
+++ b/frontend/src/Store/Reducers/episodeReducers.js
@@ -37,6 +37,11 @@ export const defaultState = {
label: 'Release Date',
isVisible: true
},
+ {
+ name: 'mediumCount',
+ label: 'Media Count',
+ isVisible: false
+ },
{
name: 'trackCount',
label: 'Track Count',
diff --git a/frontend/src/Store/Reducers/trackReducers.js b/frontend/src/Store/Reducers/trackReducers.js
index 3f684fd9d..0a7558691 100644
--- a/frontend/src/Store/Reducers/trackReducers.js
+++ b/frontend/src/Store/Reducers/trackReducers.js
@@ -11,14 +11,19 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
- sortKey: 'trackNumber',
+ sortKey: 'mediumNumber',
sortDirection: sortDirections.DESCENDING,
items: [],
columns: [
{
- name: 'trackNumber',
- label: '#',
+ name: 'medium',
+ label: 'Medium',
+ isVisible: true
+ },
+ {
+ name: 'absoluteTrackNumber',
+ label: 'Track',
isVisible: true
},
{
diff --git a/frontend/src/Styles/Variables/dimensions.js b/frontend/src/Styles/Variables/dimensions.js
index 54282902c..927f04895 100644
--- a/frontend/src/Styles/Variables/dimensions.js
+++ b/frontend/src/Styles/Variables/dimensions.js
@@ -19,16 +19,21 @@ module.exports = {
breakpointSmall: '768px',
breakpointMedium: '992px',
breakpointLarge: '1200px',
+ breakpointExtraLarge: '1450px',
// Form
+ formGroupExtraSmallWidth: '550px',
formGroupSmallWidth: '650px',
formGroupMediumWidth: '800px',
formGroupLargeWidth: '1200px',
- formLabelWidth: '250px',
+ formLabelSmallWidth: '150px',
+ formLabelLargeWidth: '250px',
formLabelRightMarginWidth: '20px',
// Drag
dragHandleWidth: '40px',
+ qualityProfileItemHeight: '30px',
+ qualityProfileItemDragSourcePadding: '4px',
// Progress Bar
progressBarSmallHeight: '5px',
@@ -38,6 +43,9 @@ module.exports = {
// Jump Bar
jumpBarItemHeight: '25px',
+ // Modal
+ modalBodyPadding: '30px',
+
// Artist
artistIndexColumnPadding: '20px',
artistIndexColumnPaddingSmallScreen: '10px',
diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
index 1fd99480c..e60ac423f 100644
--- a/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
+++ b/frontend/src/TrackFile/Editor/TrackFileEditorModalContentConnector.js
@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
+import getQualities from 'Utilities/Quality/getQualities';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { deleteTrackFiles, updateTrackFiles } from 'Store/Actions/trackFileActions';
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
@@ -52,8 +53,8 @@ function createMapStateToProps() {
});
const languages = _.map(languageProfilesSchema.languages, 'language');
- const qualities = _.map(qualityProfileSchema.items, 'quality');
-
+ const qualities = getQualities(qualityProfileSchema.items);
+
return {
items,
artistType: artist.artistType,
@@ -90,18 +91,6 @@ function createMapDispatchToProps(dispatch, props) {
onDeletePress(trackFileIds) {
dispatch(deleteTrackFiles({ trackFileIds }));
- },
-
- onQualityChange(trackFileIds, qualityId) {
- const quality = {
- quality: _.find(this.props.qualities, { id: qualityId }),
- revision: {
- version: 1,
- real: 0
- }
- };
-
- dispatch(updateTrackFiles({ trackFileIds, quality }));
}
};
}
diff --git a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
index 2e1d3f2cd..4683735ee 100644
--- a/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
+++ b/frontend/src/TrackFile/Editor/TrackFileEditorRow.js
@@ -53,7 +53,7 @@ function TrackFileEditorRow(props) {
TrackFileEditorRow.propTypes = {
id: PropTypes.number.isRequired,
- trackNumber: PropTypes.number.isRequired,
+ trackNumber: PropTypes.string.isRequired,
relativePath: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired,
diff --git a/frontend/src/Utilities/Quality/getQualities.js b/frontend/src/Utilities/Quality/getQualities.js
new file mode 100644
index 000000000..da09851ea
--- /dev/null
+++ b/frontend/src/Utilities/Quality/getQualities.js
@@ -0,0 +1,16 @@
+export default function getQualities(qualities) {
+ if (!qualities) {
+ return [];
+ }
+
+ return qualities.reduce((acc, item) => {
+ if (item.quality) {
+ acc.push(item.quality);
+ } else {
+ const groupQualities = item.items.map((i) => i.quality);
+ acc.push(...groupQualities);
+ }
+
+ return acc;
+ }, []);
+}
diff --git a/frontend/src/Utilities/createAjaxRequest.js b/frontend/src/Utilities/createAjaxRequest.js
index fc75d65b8..7ad8961da 100644
--- a/frontend/src/Utilities/createAjaxRequest.js
+++ b/frontend/src/Utilities/createAjaxRequest.js
@@ -1,32 +1,30 @@
import $ from 'jquery';
-export default function createAjaxRequest() {
- return function(ajaxOptions) {
- const requestXHR = new window.XMLHttpRequest();
- let aborted = false;
- let complete = false;
+export default function createAjaxRequest(ajaxOptions) {
+ const requestXHR = new window.XMLHttpRequest();
+ let aborted = false;
+ let complete = false;
- function abortRequest() {
- if (!complete) {
- aborted = true;
- requestXHR.abort();
- }
+ function abortRequest() {
+ if (!complete) {
+ aborted = true;
+ requestXHR.abort();
}
+ }
- const request = $.ajax({
- xhr: () => requestXHR,
- ...ajaxOptions
- }).then(null, (xhr, textStatus, errorThrown) => {
- xhr.aborted = aborted;
+ const request = $.ajax({
+ xhr: () => requestXHR,
+ ...ajaxOptions
+ }).then(null, (xhr, textStatus, errorThrown) => {
+ xhr.aborted = aborted;
- return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
- }).always(() => {
- complete = true;
- });
+ return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
+ }).always(() => {
+ complete = true;
+ });
- return {
- request,
- abortRequest
- };
+ return {
+ request,
+ abortRequest
};
}
diff --git a/package.json b/package.json
index e682076ef..7b6c90d0f 100644
--- a/package.json
+++ b/package.json
@@ -93,7 +93,6 @@
"react-tag-autocomplete": "5.4.1",
"react-tether": "0.5.7",
"react-text-truncate": "0.12.0",
- "react-truncate": "2.2.2",
"react-virtualized": "9.10.1",
"redux": "3.7.2",
"redux-actions": "2.2.1",
diff --git a/src/Lidarr.Api.V1/Albums/AlbumResource.cs b/src/Lidarr.Api.V1/Albums/AlbumResource.cs
index b4df959d4..a7043a37f 100644
--- a/src/Lidarr.Api.V1/Albums/AlbumResource.cs
+++ b/src/Lidarr.Api.V1/Albums/AlbumResource.cs
@@ -20,9 +20,22 @@ namespace Lidarr.Api.V1.Albums
public int ProfileId { get; set; }
public int Duration { get; set; }
public string AlbumType { get; set; }
+ public int MediumCount
+ {
+ get
+ {
+ if (Media == null)
+ {
+ return 0;
+ }
+
+ return Media.Where(s => s.MediumNumber > 0).Count();
+ }
+ }
public Ratings Ratings { get; set; }
public DateTime? ReleaseDate { get; set; }
public List