mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-27 23:06:29 -04:00
@@ -21,7 +21,7 @@ class EditQualityProfileModal extends Component {
|
||||
// Listeners
|
||||
|
||||
onContentHeightChange = (height) => {
|
||||
if (this.state.height === 'auto' || height > this.state.height) {
|
||||
if (this.state.height === 'auto' || height !== 0) {
|
||||
this.setState({ height });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -44,6 +44,9 @@ class EditQualityProfileModalContent extends Component {
|
||||
this.state = {
|
||||
headerHeight: 0,
|
||||
bodyHeight: 0,
|
||||
defaultBodyHeight: 0,
|
||||
editGroupsBodyHeight: 0,
|
||||
editSizesBodyHeight: 0,
|
||||
footerHeight: 0
|
||||
};
|
||||
}
|
||||
@@ -51,17 +54,18 @@ class EditQualityProfileModalContent extends Component {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
headerHeight,
|
||||
bodyHeight,
|
||||
footerHeight
|
||||
} = this.state;
|
||||
|
||||
const bodyHeight = this.state[`${this.props.mode}BodyHeight`];
|
||||
|
||||
if (
|
||||
headerHeight > 0 &&
|
||||
bodyHeight > 0 &&
|
||||
footerHeight > 0 &&
|
||||
(
|
||||
headerHeight !== prevState.headerHeight ||
|
||||
bodyHeight !== prevState.bodyHeight ||
|
||||
bodyHeight !== prevState[`${prevProps.mode}BodyHeight`] ||
|
||||
footerHeight !== prevState.footerHeight
|
||||
)
|
||||
) {
|
||||
@@ -77,15 +81,16 @@ class EditQualityProfileModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onHeaderMeasure = ({ height }) => {
|
||||
if (height > this.state.headerHeight) {
|
||||
if (height !== this.state.headerHeight) {
|
||||
this.setState({ headerHeight: height });
|
||||
}
|
||||
};
|
||||
|
||||
onBodyMeasure = ({ height }) => {
|
||||
const heightKey = `${this.props.mode}BodyHeight`;
|
||||
|
||||
if (height > this.state.bodyHeight) {
|
||||
this.setState({ bodyHeight: height });
|
||||
if (height !== this.state[heightKey]) {
|
||||
this.setState({ [heightKey]: height });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -100,7 +105,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
isFetching,
|
||||
error,
|
||||
isSaving,
|
||||
@@ -271,7 +276,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
|
||||
<div className={styles.formGroupWrapper}>
|
||||
<QualityProfileItems
|
||||
editGroups={editGroups}
|
||||
mode={mode}
|
||||
qualityProfileItems={items.value}
|
||||
errors={items.errors}
|
||||
warnings={items.warnings}
|
||||
@@ -338,7 +343,7 @@ class EditQualityProfileModalContent extends Component {
|
||||
}
|
||||
|
||||
EditQualityProfileModalContent.propTypes = {
|
||||
editGroups: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -126,7 +126,7 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
dragQualityIndex: null,
|
||||
dropQualityIndex: null,
|
||||
dropPosition: null,
|
||||
editGroups: false
|
||||
mode: 'default' // default, editGroups, editSizes
|
||||
};
|
||||
}
|
||||
|
||||
@@ -256,6 +256,49 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onSizeChange = ({ id, minSize, maxSize, preferredSize }) => {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
const items = qualityProfile.items.value;
|
||||
let quality = null;
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const index in items) {
|
||||
const item = items[index];
|
||||
|
||||
if (item.quality?.id === id) {
|
||||
quality = item;
|
||||
break;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line guard-for-in
|
||||
for (const i in item.items) {
|
||||
const nestedItem = items[i];
|
||||
|
||||
if (nestedItem.quality?.id === id) {
|
||||
quality = nestedItem;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (quality) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
return;
|
||||
}
|
||||
|
||||
quality.minSize = minSize;
|
||||
quality.maxSize = maxSize;
|
||||
quality.preferredSize = preferredSize;
|
||||
|
||||
this.props.setQualityProfileValue({
|
||||
name: 'items',
|
||||
value: items
|
||||
});
|
||||
};
|
||||
|
||||
onCreateGroupPress = (id) => {
|
||||
const qualityProfile = _.cloneDeep(this.props.item);
|
||||
const items = qualityProfile.items.value;
|
||||
@@ -439,8 +482,8 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
onToggleEditGroupsMode = () => {
|
||||
this.setState({ editGroups: !this.state.editGroups });
|
||||
onChangeMode = (mode) => {
|
||||
this.setState({ mode });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -466,7 +509,8 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
||||
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
||||
onChangeMode={this.onChangeMode}
|
||||
onSizeChange={this.onSizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
&.isInGroup {
|
||||
border-style: dashed;
|
||||
}
|
||||
|
||||
&.editSizes {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.checkInputContainer {
|
||||
@@ -32,6 +37,10 @@
|
||||
font-weight: normal;
|
||||
line-height: $qualityProfileItemHeight;
|
||||
cursor: pointer;
|
||||
|
||||
&.editSizes {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.qualityName {
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'createGroupButton': string;
|
||||
'dragHandle': string;
|
||||
'dragIcon': string;
|
||||
'editSizes': string;
|
||||
'isDragging': string;
|
||||
'isInGroup': string;
|
||||
'isPreview': string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityProfileItemSize from './QualityProfileItemSize';
|
||||
import styles from './QualityProfileItem.css';
|
||||
|
||||
class QualityProfileItem extends Component {
|
||||
@@ -36,20 +37,26 @@ class QualityProfileItem extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
isPreview,
|
||||
qualityId,
|
||||
groupId,
|
||||
name,
|
||||
allowed,
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize,
|
||||
isDragging,
|
||||
isOverCurrent,
|
||||
connectDragSource
|
||||
connectDragSource,
|
||||
onSizeChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItem,
|
||||
mode === 'editSizes' && styles.editSizes,
|
||||
isDragging && styles.isDragging,
|
||||
isPreview && styles.isPreview,
|
||||
isOverCurrent && styles.isOverCurrent,
|
||||
@@ -57,10 +64,13 @@ class QualityProfileItem extends Component {
|
||||
)}
|
||||
>
|
||||
<label
|
||||
className={styles.qualityNameContainer}
|
||||
className={classNames(
|
||||
styles.qualityNameContainer,
|
||||
mode === 'editSizes' && styles.editSizes
|
||||
)}
|
||||
>
|
||||
{
|
||||
editGroups && !groupId && !isPreview &&
|
||||
mode === 'editGroups' && !groupId && !isPreview &&
|
||||
<IconButton
|
||||
className={styles.createGroupButton}
|
||||
name={icons.GROUP}
|
||||
@@ -70,7 +80,7 @@ class QualityProfileItem extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!editGroups &&
|
||||
mode === 'default' &&
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
containerClassName={styles.checkInputContainer}
|
||||
@@ -83,7 +93,7 @@ class QualityProfileItem extends Component {
|
||||
|
||||
<div className={classNames(
|
||||
styles.qualityName,
|
||||
groupId && styles.isInGroup,
|
||||
groupId && mode !== 'editSizes' && styles.isInGroup,
|
||||
!allowed && styles.notAllowed
|
||||
)}
|
||||
>
|
||||
@@ -92,15 +102,30 @@ class QualityProfileItem extends Component {
|
||||
</label>
|
||||
|
||||
{
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
title={translate('CreateGroup')}
|
||||
name={icons.REORDER}
|
||||
mode === 'editSizes' && qualityId != null ?
|
||||
<div>
|
||||
<QualityProfileItemSize
|
||||
id={qualityId}
|
||||
minSize={minSize}
|
||||
maxSize={maxSize}
|
||||
preferredSize={preferredSize}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
mode === 'editSizes' ? null :
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
title={translate('CreateGroup')}
|
||||
name={icons.REORDER}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
@@ -108,21 +133,26 @@ class QualityProfileItem extends Component {
|
||||
}
|
||||
|
||||
QualityProfileItem.propTypes = {
|
||||
editGroups: PropTypes.bool,
|
||||
mode: PropTypes.string.isRequired,
|
||||
isPreview: PropTypes.bool,
|
||||
groupId: PropTypes.number,
|
||||
qualityId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
isDragging: PropTypes.bool.isRequired,
|
||||
isOverCurrent: PropTypes.bool.isRequired,
|
||||
isInGroup: PropTypes.bool,
|
||||
connectDragSource: PropTypes.func,
|
||||
onCreateGroupPress: PropTypes.func,
|
||||
onQualityProfileItemAllowedChange: PropTypes.func
|
||||
onQualityProfileItemAllowedChange: PropTypes.func,
|
||||
onSizeChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileItem.defaultProps = {
|
||||
mode: 'default',
|
||||
isPreview: false,
|
||||
isOverCurrent: false,
|
||||
// The drag preview will not connect the drag handle.
|
||||
|
||||
@@ -11,7 +11,7 @@ import styles from './QualityProfileItemDragSource.css';
|
||||
const qualityProfileItemDragSource = {
|
||||
beginDrag(props) {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
qualityIndex,
|
||||
groupId,
|
||||
qualityId,
|
||||
@@ -20,7 +20,7 @@ const qualityProfileItemDragSource = {
|
||||
} = props;
|
||||
|
||||
return {
|
||||
editGroups,
|
||||
mode,
|
||||
qualityIndex,
|
||||
groupId,
|
||||
qualityId,
|
||||
@@ -110,12 +110,15 @@ class QualityProfileItemDragSource extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
groupId,
|
||||
qualityId,
|
||||
name,
|
||||
allowed,
|
||||
items,
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize,
|
||||
qualityIndex,
|
||||
isDragging,
|
||||
isDraggingUp,
|
||||
@@ -129,7 +132,8 @@ class QualityProfileItemDragSource extends Component {
|
||||
onItemGroupAllowedChange,
|
||||
onItemGroupNameChange,
|
||||
onQualityProfileItemDragMove,
|
||||
onQualityProfileItemDragEnd
|
||||
onQualityProfileItemDragEnd,
|
||||
onSizeChange
|
||||
} = this.props;
|
||||
|
||||
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||
@@ -156,7 +160,7 @@ class QualityProfileItemDragSource extends Component {
|
||||
{
|
||||
!!groupId && qualityId == null &&
|
||||
<QualityProfileItemGroup
|
||||
editGroups={editGroups}
|
||||
mode={mode}
|
||||
groupId={groupId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
@@ -172,23 +176,28 @@ class QualityProfileItemDragSource extends Component {
|
||||
onItemGroupNameChange={onItemGroupNameChange}
|
||||
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
qualityId != null &&
|
||||
<QualityProfileItem
|
||||
editGroups={editGroups}
|
||||
mode={mode}
|
||||
groupId={groupId}
|
||||
qualityId={qualityId}
|
||||
name={name}
|
||||
allowed={allowed}
|
||||
minSize={minSize}
|
||||
maxSize={maxSize}
|
||||
preferredSize={preferredSize}
|
||||
qualityIndex={qualityIndex}
|
||||
isDragging={isDragging}
|
||||
isOverCurrent={isOverCurrent}
|
||||
connectDragSource={connectDragSource}
|
||||
onCreateGroupPress={onCreateGroupPress}
|
||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -207,12 +216,15 @@ class QualityProfileItemDragSource extends Component {
|
||||
}
|
||||
|
||||
QualityProfileItemDragSource.propTypes = {
|
||||
editGroups: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
groupId: PropTypes.number,
|
||||
qualityId: PropTypes.number,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
qualityIndex: PropTypes.string.isRequired,
|
||||
isDragging: PropTypes.bool,
|
||||
isDraggingUp: PropTypes.bool,
|
||||
@@ -227,7 +239,8 @@ QualityProfileItemDragSource.propTypes = {
|
||||
onItemGroupAllowedChange: PropTypes.func,
|
||||
onItemGroupNameChange: PropTypes.func,
|
||||
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired,
|
||||
onSizeChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default DropTarget(
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
&.editGroups {
|
||||
background: var(--inputBackgroundColor);
|
||||
}
|
||||
|
||||
&.editSizes {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.qualityProfileItemGroupInfo {
|
||||
@@ -70,6 +74,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editSizesQualityNameLabel {
|
||||
composes: qualityNameContainer;
|
||||
}
|
||||
|
||||
.deleteGroupButton {
|
||||
composes: buton from '~Components/Link/IconButton.css';
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ interface CssExports {
|
||||
'dragHandle': string;
|
||||
'dragIcon': string;
|
||||
'editGroups': string;
|
||||
'editSizes': string;
|
||||
'editSizesQualityNameLabel': string;
|
||||
'groupQualities': string;
|
||||
'isDragging': string;
|
||||
'items': string;
|
||||
|
||||
@@ -48,7 +48,7 @@ class QualityProfileItemGroup extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
groupId,
|
||||
name,
|
||||
allowed,
|
||||
@@ -60,20 +60,22 @@ class QualityProfileItemGroup extends Component {
|
||||
connectDragSource,
|
||||
onQualityProfileItemAllowedChange,
|
||||
onQualityProfileItemDragMove,
|
||||
onQualityProfileItemDragEnd
|
||||
onQualityProfileItemDragEnd,
|
||||
onSizeChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.qualityProfileItemGroup,
|
||||
editGroups && styles.editGroups,
|
||||
mode === 'editGroups' && styles.editGroups,
|
||||
mode === 'editSizes' && styles.editSizes,
|
||||
isDragging && styles.isDragging
|
||||
)}
|
||||
>
|
||||
<div className={styles.qualityProfileItemGroupInfo}>
|
||||
{
|
||||
editGroups &&
|
||||
mode === 'editGroups' &&
|
||||
<div className={styles.qualityNameContainer}>
|
||||
<IconButton
|
||||
className={styles.deleteGroupButton}
|
||||
@@ -92,7 +94,7 @@ class QualityProfileItemGroup extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!editGroups &&
|
||||
mode === 'default' &&
|
||||
<label
|
||||
className={styles.qualityNameLabel}
|
||||
>
|
||||
@@ -129,31 +131,53 @@ class QualityProfileItemGroup extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
name={icons.REORDER}
|
||||
title={translate('Reorder')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
mode === 'editSizes' &&
|
||||
<label
|
||||
className={styles.editSizesQualityNameLabel}
|
||||
>
|
||||
<div className={styles.nameContainer}>
|
||||
<div className={classNames(
|
||||
styles.name,
|
||||
!allowed && styles.notAllowed
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
}
|
||||
|
||||
{
|
||||
mode === 'editSizes' ? null :
|
||||
connectDragSource(
|
||||
<div className={styles.dragHandle}>
|
||||
<Icon
|
||||
className={styles.dragIcon}
|
||||
name={icons.REORDER}
|
||||
title={translate('Reorder')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
editGroups &&
|
||||
<div className={styles.items}>
|
||||
mode === 'default' ?
|
||||
null :
|
||||
<div className={mode === 'editGroups' ? styles.items : undefined}>
|
||||
{
|
||||
items.map(({ quality }, index) => {
|
||||
return (
|
||||
<QualityProfileItemDragSource
|
||||
key={quality.id}
|
||||
editGroups={editGroups}
|
||||
mode={mode}
|
||||
groupId={groupId}
|
||||
qualityId={quality.id}
|
||||
name={quality.name}
|
||||
allowed={allowed}
|
||||
minSize={quality.minSize}
|
||||
maxSize={quality.maxSize}
|
||||
preferredSize={quality.preferredSize}
|
||||
items={items}
|
||||
qualityIndex={`${qualityIndex}.${index + 1}`}
|
||||
isDragging={isDragging}
|
||||
@@ -163,6 +187,7 @@ class QualityProfileItemGroup extends Component {
|
||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||
onSizeChange={onSizeChange}
|
||||
/>
|
||||
);
|
||||
}).reverse()
|
||||
@@ -175,7 +200,7 @@ class QualityProfileItemGroup extends Component {
|
||||
}
|
||||
|
||||
QualityProfileItemGroup.propTypes = {
|
||||
editGroups: PropTypes.bool,
|
||||
mode: PropTypes.string.isRequired,
|
||||
groupId: PropTypes.number.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
allowed: PropTypes.bool.isRequired,
|
||||
@@ -190,10 +215,12 @@ QualityProfileItemGroup.propTypes = {
|
||||
onItemGroupNameChange: PropTypes.func.isRequired,
|
||||
onDeleteGroupPress: PropTypes.func.isRequired,
|
||||
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired,
|
||||
onSizeChange: PropTypes.func
|
||||
};
|
||||
|
||||
QualityProfileItemGroup.defaultProps = {
|
||||
mode: 'default',
|
||||
// The drag preview will not connect the drag handle.
|
||||
connectDragSource: (node) => node
|
||||
};
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: var(--sliderAccentColor);
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(3n + 1) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid var(--sliderAccentColor);
|
||||
border-radius: 50%;
|
||||
background-color: var(--white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.megabytesPerMinuteContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 400px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.megabytesPerMinute {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sizeInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 2px;
|
||||
padding: 6px;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'megabytesPerMinute': string;
|
||||
'megabytesPerMinuteContainer': string;
|
||||
'sizeInput': string;
|
||||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,295 @@
|
||||
import React, { HTMLProps, useCallback, useState } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import Label from 'Components/Label';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityDefinitionLimits from 'Settings/Quality/Definition/QualityDefinitionLimits';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import roundNumber from 'Utilities/Number/roundNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QualityProfileItemSize.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 400;
|
||||
const STEP_SIZE = 0.1;
|
||||
const MIN_DISTANCE = 3;
|
||||
const SLIDER_MAX = roundNumber(Math.pow(MAX, 1 / 1.1));
|
||||
|
||||
interface SizeProps {
|
||||
minSize: number | null;
|
||||
preferredSize: number | null;
|
||||
maxSize: number | null;
|
||||
}
|
||||
|
||||
export interface OnSizeChangeArguments extends SizeProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileItemSizeProps extends OnSizeChangeArguments {
|
||||
onSizeChange: (props: OnSizeChangeArguments) => void;
|
||||
}
|
||||
|
||||
function trackRenderer(props: HTMLProps<HTMLDivElement>) {
|
||||
return <div {...props} className={styles.track} />;
|
||||
}
|
||||
|
||||
function thumbRenderer(props: HTMLProps<HTMLDivElement>) {
|
||||
return <div {...props} className={styles.thumb} />;
|
||||
}
|
||||
|
||||
function getSliderValue(value: number | null, defaultValue: number): number {
|
||||
const sliderValue = value ? Math.pow(value, 1 / 1.1) : defaultValue;
|
||||
|
||||
return roundNumber(sliderValue);
|
||||
}
|
||||
|
||||
export default function QualityProfileItemSize(
|
||||
props: QualityProfileItemSizeProps
|
||||
) {
|
||||
const { id, minSize, maxSize, preferredSize, onSizeChange } = props;
|
||||
const [sizes, setSizes] = useState<SizeProps>({
|
||||
minSize: getSliderValue(minSize, MIN),
|
||||
preferredSize: getSliderValue(preferredSize, SLIDER_MAX - MIN_DISTANCE),
|
||||
maxSize: getSliderValue(maxSize, SLIDER_MAX),
|
||||
});
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
([sliderMinSize, sliderPreferredSize, sliderMaxSize]: [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
]) => {
|
||||
// console.log('Sizes:', sliderMinSize, sliderPreferredSize, sliderMaxSize);
|
||||
console.log(
|
||||
'Min Sizes: ',
|
||||
sliderMinSize,
|
||||
roundNumber(Math.pow(sliderMinSize, 1.1))
|
||||
);
|
||||
|
||||
setSizes({
|
||||
minSize: sliderMinSize,
|
||||
preferredSize: sliderPreferredSize,
|
||||
maxSize: sliderMaxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||
preferredSize:
|
||||
sliderPreferredSize === MAX - MIN_DISTANCE
|
||||
? null
|
||||
: roundNumber(Math.pow(sliderPreferredSize, 1.1)),
|
||||
maxSize:
|
||||
sliderMaxSize === MAX
|
||||
? null
|
||||
: roundNumber(Math.pow(sliderMaxSize, 1.1)),
|
||||
});
|
||||
},
|
||||
[id, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleMinSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: value,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: value,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handlePreferredSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: value,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: value,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleMaxSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: value,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: value,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleAfterSliderChange = useCallback(() => {
|
||||
setSizes({
|
||||
minSize: getSliderValue(minSize, MIN),
|
||||
maxSize: getSliderValue(maxSize, MAX),
|
||||
preferredSize: getSliderValue(preferredSize, MAX - MIN_DISTANCE),
|
||||
});
|
||||
}, [minSize, maxSize, preferredSize, setSizes]);
|
||||
|
||||
const minBytes = (sizes.minSize || 0) * 1024 * 1024;
|
||||
const minSixty = `${formatBytes(minBytes * 60)}/${translate(
|
||||
'HourShorthand'
|
||||
)}`;
|
||||
|
||||
const preferredBytes = (sizes.preferredSize || 0) * 1024 * 1024;
|
||||
const preferredSixty = preferredBytes
|
||||
? `${formatBytes(preferredBytes * 60)}/${translate('HourShorthand')}`
|
||||
: translate('Unlimited');
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxSixty = maxBytes
|
||||
? `${formatBytes(maxBytes * 60)}/${translate('HourShorthand')}`
|
||||
: translate('Unlimited');
|
||||
|
||||
return (
|
||||
<div className={styles.sizeLimit}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore React version mismatch */}
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={MIN}
|
||||
max={SLIDER_MAX}
|
||||
step={STEP_SIZE}
|
||||
minDistance={3}
|
||||
value={[sizes.minSize, sizes.preferredSize, sizes.maxSize]}
|
||||
withTracks={true}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore allowCross is still available in the version currently used
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
renderThumb={thumbRenderer}
|
||||
renderTrack={trackRenderer}
|
||||
onChange={handleSliderChange}
|
||||
onAfterChange={handleAfterSliderChange}
|
||||
/>
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.INFO}>{minSixty}</Label>}
|
||||
title={translate('MinimumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={minBytes}
|
||||
message={translate('NoMinimumForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.SUCCESS}>{preferredSixty}</Label>}
|
||||
title={translate('PreferredSize')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={preferredBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.WARNING}>{maxSixty}</Label>}
|
||||
title={translate('MaximumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={maxBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinuteContainer}>
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={minSize || MIN}
|
||||
min={MIN}
|
||||
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handleMinSizeChange}
|
||||
/>
|
||||
<Label kind={kinds.INFO}>
|
||||
{translate('Minimum')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={preferredSize || MAX - 5}
|
||||
min={MIN}
|
||||
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handlePreferredSizeChange}
|
||||
/>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Preferred')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.max`}
|
||||
value={maxSize || MAX}
|
||||
min={(preferredSize || 0) + STEP_SIZE}
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handleMaxSizeChange}
|
||||
/>
|
||||
|
||||
<Label kind={kinds.WARNING}>
|
||||
{translate('Maximum')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,14 @@
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.editGroupsButtonIcon {
|
||||
.editSizesButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.editButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'editButtonIcon': string;
|
||||
'editGroupsButton': string;
|
||||
'editGroupsButtonIcon': string;
|
||||
'editSizesButton': string;
|
||||
'qualities': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -21,8 +21,9 @@ class QualityProfileItems extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
qualitiesHeight: 0,
|
||||
qualitiesHeightEditGroups: 0
|
||||
defaultHeight: 0,
|
||||
editGroupsHeight: 0,
|
||||
editSizesHeight: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -30,17 +31,23 @@ class QualityProfileItems extends Component {
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ height }) => {
|
||||
if (this.props.editGroups) {
|
||||
this.setState({
|
||||
qualitiesHeightEditGroups: height
|
||||
});
|
||||
} else {
|
||||
this.setState({ qualitiesHeight: height });
|
||||
}
|
||||
const heightKey = `${this.props.mode}Height`;
|
||||
|
||||
this.setState({
|
||||
[heightKey]: height
|
||||
});
|
||||
};
|
||||
|
||||
onToggleEditGroupsMode = () => {
|
||||
this.props.onToggleEditGroupsMode();
|
||||
onEditGroupsPress = () => {
|
||||
this.props.onChangeMode('editGroups');
|
||||
};
|
||||
|
||||
onEditSizesPress = () => {
|
||||
this.props.onChangeMode('editSizes');
|
||||
};
|
||||
|
||||
onDefaultModePress = () => {
|
||||
this.props.onChangeMode('default');
|
||||
};
|
||||
|
||||
//
|
||||
@@ -48,7 +55,7 @@ class QualityProfileItems extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
editGroups,
|
||||
mode,
|
||||
dropQualityIndex,
|
||||
dropPosition,
|
||||
qualityProfileItems,
|
||||
@@ -57,15 +64,10 @@ class QualityProfileItems extends Component {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
qualitiesHeight,
|
||||
qualitiesHeightEditGroups
|
||||
} = this.state;
|
||||
|
||||
const isDragging = dropQualityIndex !== null;
|
||||
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||
const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight;
|
||||
const height = this.state[`${mode}Height`];
|
||||
|
||||
return (
|
||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||
@@ -107,16 +109,33 @@ class QualityProfileItems extends Component {
|
||||
<Button
|
||||
className={styles.editGroupsButton}
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={this.onToggleEditGroupsMode}
|
||||
onPress={mode === 'editGroups' ? this.onDefaultModePress : this.onEditGroupsPress}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.editGroupsButtonIcon}
|
||||
name={editGroups ? icons.REORDER : icons.GROUP}
|
||||
className={styles.editButtonIcon}
|
||||
name={mode === 'editGroups' ? icons.REORDER : icons.GROUP}
|
||||
/>
|
||||
|
||||
{
|
||||
editGroups ? translate('DoneEditingGroups') : translate('EditGroups')
|
||||
mode === 'editGroups' ? translate('DoneEditingGroups') : translate('EditGroups')
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className={styles.editSizesButton}
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={mode === 'editSizes' ? this.onDefaultModePress : this.onEditSizesPress}
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.editButtonIcon}
|
||||
name={mode === 'editSizes' ? icons.REORDER : icons.FILE}
|
||||
/>
|
||||
|
||||
{
|
||||
mode === 'editSizes' ? translate('DoneEditingSizes') : translate('EditSizes')
|
||||
}
|
||||
</div>
|
||||
</Button>
|
||||
@@ -128,21 +147,24 @@ class QualityProfileItems extends Component {
|
||||
>
|
||||
<div
|
||||
className={styles.qualities}
|
||||
style={{ minHeight: `${minHeight}px` }}
|
||||
style={{ height: `${height}px` }}
|
||||
>
|
||||
{
|
||||
qualityProfileItems.map(({ id, name, allowed, quality, items }, index) => {
|
||||
qualityProfileItems.map(({ id, name, allowed, quality, items, minSize, maxSize, preferredSize }, index) => {
|
||||
const identifier = quality ? quality.id : id;
|
||||
|
||||
return (
|
||||
<QualityProfileItemDragSource
|
||||
key={identifier}
|
||||
editGroups={editGroups}
|
||||
mode={mode}
|
||||
groupId={id}
|
||||
qualityId={quality && quality.id}
|
||||
name={quality ? quality.name : name}
|
||||
allowed={allowed}
|
||||
items={items}
|
||||
minSize={minSize}
|
||||
maxSize={maxSize}
|
||||
preferredSize={preferredSize}
|
||||
qualityIndex={`${index + 1}`}
|
||||
isInGroup={false}
|
||||
isDragging={isDragging}
|
||||
@@ -164,14 +186,14 @@ class QualityProfileItems extends Component {
|
||||
}
|
||||
|
||||
QualityProfileItems.propTypes = {
|
||||
editGroups: PropTypes.bool.isRequired,
|
||||
mode: PropTypes.string.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),
|
||||
onToggleEditGroupsMode: PropTypes.func.isRequired
|
||||
onChangeMode: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QualityProfileItems.defaultProps = {
|
||||
|
||||
@@ -14,60 +14,6 @@
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: var(--sliderAccentColor);
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(3n+1) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid var(--sliderAccentColor);
|
||||
border-radius: 50%;
|
||||
background-color: var(--white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.megabytesPerMinute {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 400px;
|
||||
}
|
||||
|
||||
.sizeInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
padding: 6px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.qualityDefinition {
|
||||
flex-wrap: wrap;
|
||||
@@ -86,8 +32,4 @@
|
||||
font-weight: bold;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'megabytesPerMinute': string;
|
||||
'quality': string;
|
||||
'qualityDefinition': string;
|
||||
'sizeInput': string;
|
||||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'title': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Label from 'Components/Label';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import roundNumber from 'Utilities/Number/roundNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityDefinitionLimits from './QualityDefinitionLimits';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 1000;
|
||||
const MIN_DISTANCE = 1;
|
||||
|
||||
const slider = {
|
||||
min: MIN,
|
||||
max: roundNumber(Math.pow(MAX, 1 / 1.1)),
|
||||
step: 0.1
|
||||
};
|
||||
|
||||
function getValue(inputValue) {
|
||||
if (inputValue < MIN) {
|
||||
return MIN;
|
||||
}
|
||||
|
||||
if (inputValue > MAX) {
|
||||
return MAX;
|
||||
}
|
||||
|
||||
return roundNumber(inputValue);
|
||||
}
|
||||
|
||||
function getSliderValue(value, defaultValue) {
|
||||
const sliderValue = value ? Math.pow(value, 1 / 1.1) : defaultValue;
|
||||
|
||||
return roundNumber(sliderValue);
|
||||
}
|
||||
|
||||
class QualityDefinition extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._forceUpdateTimeout = null;
|
||||
|
||||
this.state = {
|
||||
sliderMinSize: getSliderValue(props.minSize, slider.min),
|
||||
sliderMaxSize: getSliderValue(props.maxSize, slider.max),
|
||||
sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3))
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// A hack to deal with a bug in the slider component until a fix for it
|
||||
// lands and an updated version is available.
|
||||
// See: https://github.com/mpowaga/react-slider/issues/115
|
||||
|
||||
this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._forceUpdateTimeout) {
|
||||
clearTimeout(this._forceUpdateTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
trackRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.track}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
thumbRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.thumb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSliderChange = ([sliderMinSize, sliderPreferredSize, sliderMaxSize]) => {
|
||||
this.setState({
|
||||
sliderMinSize,
|
||||
sliderMaxSize,
|
||||
sliderPreferredSize
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||
preferredSize: sliderPreferredSize === (slider.max - 3) ? null : roundNumber(Math.pow(sliderPreferredSize, 1.1)),
|
||||
maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1))
|
||||
});
|
||||
};
|
||||
|
||||
onAfterSliderChange = () => {
|
||||
const {
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
sliderMiSize: getSliderValue(minSize, slider.min),
|
||||
sliderMaxSize: getSliderValue(maxSize, slider.max),
|
||||
sliderPreferredSize: getSliderValue(preferredSize, (slider.max - 3)) // fix
|
||||
});
|
||||
};
|
||||
|
||||
onMinSizeChange = ({ value }) => {
|
||||
const minSize = getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderMinSize: getSliderValue(minSize, slider.min)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize,
|
||||
maxSize: this.props.maxSize,
|
||||
preferredSize: this.props.preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
onPreferredSizeChange = ({ value }) => {
|
||||
const preferredSize = value === (MAX - 3) ? null : getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderPreferredSize: getSliderValue(preferredSize, slider.preferred)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: this.props.minSize,
|
||||
maxSize: this.props.maxSize,
|
||||
preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
onMaxSizeChange = ({ value }) => {
|
||||
const maxSize = value === MAX ? null : getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderMaxSize: getSliderValue(maxSize, slider.max)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: this.props.minSize,
|
||||
maxSize,
|
||||
preferredSize: this.props.preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
quality,
|
||||
title,
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize,
|
||||
advancedSettings,
|
||||
onTitleChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sliderMinSize,
|
||||
sliderMaxSize,
|
||||
sliderPreferredSize
|
||||
} = this.state;
|
||||
|
||||
const minBytes = minSize * 1024 * 1024;
|
||||
const minSixty = `${formatBytes(minBytes * 60)}/${translate('HourShorthand')}`;
|
||||
|
||||
const preferredBytes = preferredSize * 1024 * 1024;
|
||||
const preferredSixty = preferredBytes ? `${formatBytes(preferredBytes * 60)}/${translate('HourShorthand')}` : translate('Unlimited');
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/${translate('HourShorthand')}` : translate('Unlimited');
|
||||
|
||||
return (
|
||||
<div className={styles.qualityDefinition}>
|
||||
<div className={styles.quality}>
|
||||
{quality.name}
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<TextInput
|
||||
name={`${id}.${title}`}
|
||||
value={title}
|
||||
onChange={onTitleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
minDistance={3}
|
||||
value={[sliderMinSize, sliderPreferredSize, sliderMaxSize]}
|
||||
withTracks={true}
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.INFO}>{minSixty}</Label>
|
||||
}
|
||||
title={translate('MinimumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={minBytes}
|
||||
message={translate('NoMinimumForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.SUCCESS}>{preferredSixty}</Label>
|
||||
}
|
||||
title={translate('PreferredSize')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={preferredBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.WARNING}>{maxSixty}</Label>
|
||||
}
|
||||
title={translate('MaximumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={maxBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
advancedSettings &&
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<div>
|
||||
{translate('Min')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={minSize || MIN}
|
||||
min={MIN}
|
||||
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onMinSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{translate('Preferred')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={preferredSize || MAX - 5}
|
||||
min={MIN}
|
||||
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onPreferredSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{translate('Max')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.max`}
|
||||
value={maxSize || MAX}
|
||||
min={minSize + MIN_DISTANCE}
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onMaxSizeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinition.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSizeChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityDefinition;
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Quality from 'Quality/Quality';
|
||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
interface QualityDefinitionProps {
|
||||
id: number;
|
||||
quality: Quality;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function QualityDefinition(props: QualityDefinitionProps) {
|
||||
const { id, quality, title } = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
dispatch(
|
||||
setQualityDefinitionValue({
|
||||
id,
|
||||
name: 'title',
|
||||
value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[id, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.qualityDefinition}>
|
||||
<div className={styles.quality}>{quality.name}</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<TextInput
|
||||
name={`${id}.${title}`}
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QualityDefinition;
|
||||
@@ -1,71 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
||||
import QualityDefinition from './QualityDefinition';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setQualityDefinitionValue,
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class QualityDefinitionConnector extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearPendingChanges({ section: 'settings.qualityDefinitions' });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTitleChange = ({ value }) => {
|
||||
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
|
||||
};
|
||||
|
||||
onSizeChange = ({ minSize, maxSize, preferredSize }) => {
|
||||
const {
|
||||
id,
|
||||
minSize: currentMinSize,
|
||||
maxSize: currentMaxSize,
|
||||
preferredSize: currentPreferredSize
|
||||
} = this.props;
|
||||
|
||||
if (minSize !== currentMinSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
|
||||
}
|
||||
|
||||
if (maxSize !== currentMaxSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
||||
}
|
||||
|
||||
if (preferredSize !== currentPreferredSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QualityDefinition
|
||||
{...this.props}
|
||||
onTitleChange={this.onTitleChange}
|
||||
onSizeChange={this.onSizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinitionConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(QualityDefinitionConnector);
|
||||
@@ -8,24 +8,11 @@
|
||||
flex: 0 1 250px;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
}
|
||||
.notice {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
.megabytesPerMinute {
|
||||
flex: 0 0 250px;
|
||||
}
|
||||
|
||||
.sizeLimitHelpTextContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.sizeLimitHelpText {
|
||||
max-width: 500px;
|
||||
color: var(--helpTextColor);
|
||||
margin: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
|
||||
@@ -3,11 +3,8 @@
|
||||
interface CssExports {
|
||||
'definitions': string;
|
||||
'header': string;
|
||||
'megabytesPerMinute': string;
|
||||
'notice': string;
|
||||
'quality': string;
|
||||
'sizeLimit': string;
|
||||
'sizeLimitHelpText': string;
|
||||
'sizeLimitHelpTextContainer': string;
|
||||
'title': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityDefinitions from './Definition/QualityDefinitions';
|
||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||
|
||||
class Quality extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isConfirmQualityDefinitionResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChildMounted = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
onResetQualityDefinitionsPress = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
||||
};
|
||||
|
||||
onCloseResetQualityDefinitionsModal = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSaving,
|
||||
isResettingQualityDefinitions,
|
||||
hasPendingChanges
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('QualitySettings')}>
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ResetDefinitions')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isResettingQualityDefinitions}
|
||||
onPress={this.onResetQualityDefinitionsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<QualityDefinitions
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
<ResetQualityDefinitionsModal
|
||||
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
||||
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Quality.propTypes = {
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Quality;
|
||||
Reference in New Issue
Block a user