Compare commits
3 Commits
develop
...
size-limit
Author | SHA1 | Date |
---|---|---|
Mark McDowall | f296fe7c6b | |
Mark McDowall | ee84835d52 | |
Mark McDowall | 20e68c1d3c |
|
@ -43,6 +43,14 @@ export interface AppSectionItemState<T> {
|
||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionListState<T> {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
pendingChanges: Partial<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
|
AppSectionListState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
PagedAppSectionState,
|
PagedAppSectionState,
|
||||||
|
@ -13,6 +14,7 @@ import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
import IndexerFlag from 'typings/IndexerFlag';
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
|
import QualityDefinition from 'typings/QualityDefinition';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
|
|
||||||
|
@ -35,6 +37,10 @@ export interface NotificationAppState
|
||||||
extends AppSectionState<Notification>,
|
extends AppSectionState<Notification>,
|
||||||
AppSectionDeleteState {}
|
AppSectionDeleteState {}
|
||||||
|
|
||||||
|
export interface QualityDefinitionsAppState
|
||||||
|
extends AppSectionListState<QualityDefinition>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface QualityProfilesAppState
|
export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
@ -65,6 +71,7 @@ interface SettingsAppState {
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
qualityDefinitions: QualityDefinitionsAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ class EditQualityProfileModal extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onContentHeightChange = (height) => {
|
onContentHeightChange = (height) => {
|
||||||
if (this.state.height === 'auto' || height > this.state.height) {
|
if (this.state.height === 'auto' || height !== 0) {
|
||||||
this.setState({ height });
|
this.setState({ height });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,9 @@ class EditQualityProfileModalContent extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
headerHeight: 0,
|
headerHeight: 0,
|
||||||
bodyHeight: 0,
|
bodyHeight: 0,
|
||||||
|
defaultBodyHeight: 0,
|
||||||
|
editGroupsBodyHeight: 0,
|
||||||
|
editSizesBodyHeight: 0,
|
||||||
footerHeight: 0
|
footerHeight: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -51,17 +54,18 @@ class EditQualityProfileModalContent extends Component {
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const {
|
const {
|
||||||
headerHeight,
|
headerHeight,
|
||||||
bodyHeight,
|
|
||||||
footerHeight
|
footerHeight
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const bodyHeight = this.state[`${this.props.mode}BodyHeight`];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
headerHeight > 0 &&
|
headerHeight > 0 &&
|
||||||
bodyHeight > 0 &&
|
bodyHeight > 0 &&
|
||||||
footerHeight > 0 &&
|
footerHeight > 0 &&
|
||||||
(
|
(
|
||||||
headerHeight !== prevState.headerHeight ||
|
headerHeight !== prevState.headerHeight ||
|
||||||
bodyHeight !== prevState.bodyHeight ||
|
bodyHeight !== prevState[`${prevProps.mode}BodyHeight`] ||
|
||||||
footerHeight !== prevState.footerHeight
|
footerHeight !== prevState.footerHeight
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
@ -77,15 +81,16 @@ class EditQualityProfileModalContent extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onHeaderMeasure = ({ height }) => {
|
onHeaderMeasure = ({ height }) => {
|
||||||
if (height > this.state.headerHeight) {
|
if (height !== this.state.headerHeight) {
|
||||||
this.setState({ headerHeight: height });
|
this.setState({ headerHeight: height });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onBodyMeasure = ({ height }) => {
|
onBodyMeasure = ({ height }) => {
|
||||||
|
const heightKey = `${this.props.mode}BodyHeight`;
|
||||||
|
|
||||||
if (height > this.state.bodyHeight) {
|
if (height !== this.state[heightKey]) {
|
||||||
this.setState({ bodyHeight: height });
|
this.setState({ [heightKey]: height });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -100,7 +105,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
isFetching,
|
isFetching,
|
||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
@ -251,7 +256,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
|
|
||||||
<div className={styles.formGroupWrapper}>
|
<div className={styles.formGroupWrapper}>
|
||||||
<QualityProfileItems
|
<QualityProfileItems
|
||||||
editGroups={editGroups}
|
mode={mode}
|
||||||
qualityProfileItems={items.value}
|
qualityProfileItems={items.value}
|
||||||
errors={items.errors}
|
errors={items.errors}
|
||||||
warnings={items.warnings}
|
warnings={items.warnings}
|
||||||
|
@ -318,7 +323,7 @@ class EditQualityProfileModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
EditQualityProfileModalContent.propTypes = {
|
EditQualityProfileModalContent.propTypes = {
|
||||||
editGroups: PropTypes.bool.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -126,7 +126,7 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
dragQualityIndex: null,
|
dragQualityIndex: null,
|
||||||
dropQualityIndex: null,
|
dropQualityIndex: null,
|
||||||
dropPosition: 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) => {
|
onCreateGroupPress = (id) => {
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
const items = qualityProfile.items.value;
|
const items = qualityProfile.items.value;
|
||||||
|
@ -439,8 +482,8 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onToggleEditGroupsMode = () => {
|
onChangeMode = (mode) => {
|
||||||
this.setState({ editGroups: !this.state.editGroups });
|
this.setState({ mode });
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -466,7 +509,8 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||||
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange}
|
||||||
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
onChangeMode={this.onChangeMode}
|
||||||
|
onSizeChange={this.onSizeChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
&.isInGroup {
|
&.isInGroup {
|
||||||
border-style: dashed;
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.editSizes {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkInputContainer {
|
.checkInputContainer {
|
||||||
|
@ -32,6 +37,10 @@
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: $qualityProfileItemHeight;
|
line-height: $qualityProfileItemHeight;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.editSizes {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qualityName {
|
.qualityName {
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface CssExports {
|
||||||
'createGroupButton': string;
|
'createGroupButton': string;
|
||||||
'dragHandle': string;
|
'dragHandle': string;
|
||||||
'dragIcon': string;
|
'dragIcon': string;
|
||||||
|
'editSizes': string;
|
||||||
'isDragging': string;
|
'isDragging': string;
|
||||||
'isInGroup': string;
|
'isInGroup': string;
|
||||||
'isPreview': string;
|
'isPreview': string;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityProfileItemSize from './QualityProfileItemSize';
|
||||||
import styles from './QualityProfileItem.css';
|
import styles from './QualityProfileItem.css';
|
||||||
|
|
||||||
class QualityProfileItem extends Component {
|
class QualityProfileItem extends Component {
|
||||||
|
@ -36,20 +37,26 @@ class QualityProfileItem extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
isPreview,
|
isPreview,
|
||||||
|
qualityId,
|
||||||
groupId,
|
groupId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
|
minSize,
|
||||||
|
maxSize,
|
||||||
|
preferredSize,
|
||||||
isDragging,
|
isDragging,
|
||||||
isOverCurrent,
|
isOverCurrent,
|
||||||
connectDragSource
|
connectDragSource,
|
||||||
|
onSizeChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.qualityProfileItem,
|
styles.qualityProfileItem,
|
||||||
|
mode === 'editSizes' && styles.editSizes,
|
||||||
isDragging && styles.isDragging,
|
isDragging && styles.isDragging,
|
||||||
isPreview && styles.isPreview,
|
isPreview && styles.isPreview,
|
||||||
isOverCurrent && styles.isOverCurrent,
|
isOverCurrent && styles.isOverCurrent,
|
||||||
|
@ -57,10 +64,13 @@ class QualityProfileItem extends Component {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className={styles.qualityNameContainer}
|
className={classNames(
|
||||||
|
styles.qualityNameContainer,
|
||||||
|
mode === 'editSizes' && styles.editSizes
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
editGroups && !groupId && !isPreview &&
|
mode === 'editGroups' && !groupId && !isPreview &&
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.createGroupButton}
|
className={styles.createGroupButton}
|
||||||
name={icons.GROUP}
|
name={icons.GROUP}
|
||||||
|
@ -70,7 +80,7 @@ class QualityProfileItem extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!editGroups &&
|
mode === 'default' &&
|
||||||
<CheckInput
|
<CheckInput
|
||||||
className={styles.checkInput}
|
className={styles.checkInput}
|
||||||
containerClassName={styles.checkInputContainer}
|
containerClassName={styles.checkInputContainer}
|
||||||
|
@ -83,7 +93,7 @@ class QualityProfileItem extends Component {
|
||||||
|
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
styles.qualityName,
|
styles.qualityName,
|
||||||
groupId && styles.isInGroup,
|
groupId && mode !== 'editSizes' && styles.isInGroup,
|
||||||
!allowed && styles.notAllowed
|
!allowed && styles.notAllowed
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -92,15 +102,30 @@ class QualityProfileItem extends Component {
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{
|
{
|
||||||
connectDragSource(
|
mode === 'editSizes' && qualityId != null ?
|
||||||
<div className={styles.dragHandle}>
|
<div>
|
||||||
<Icon
|
<QualityProfileItemSize
|
||||||
className={styles.dragIcon}
|
id={qualityId}
|
||||||
title={translate('CreateGroup')}
|
minSize={minSize}
|
||||||
name={icons.REORDER}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -108,21 +133,26 @@ class QualityProfileItem extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItem.propTypes = {
|
QualityProfileItem.propTypes = {
|
||||||
editGroups: PropTypes.bool,
|
mode: PropTypes.string.isRequired,
|
||||||
isPreview: PropTypes.bool,
|
isPreview: PropTypes.bool,
|
||||||
groupId: PropTypes.number,
|
groupId: PropTypes.number,
|
||||||
qualityId: PropTypes.number.isRequired,
|
qualityId: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
allowed: PropTypes.bool.isRequired,
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
minSize: PropTypes.number,
|
||||||
|
maxSize: PropTypes.number,
|
||||||
|
preferredSize: PropTypes.number,
|
||||||
isDragging: PropTypes.bool.isRequired,
|
isDragging: PropTypes.bool.isRequired,
|
||||||
isOverCurrent: PropTypes.bool.isRequired,
|
isOverCurrent: PropTypes.bool.isRequired,
|
||||||
isInGroup: PropTypes.bool,
|
isInGroup: PropTypes.bool,
|
||||||
connectDragSource: PropTypes.func,
|
connectDragSource: PropTypes.func,
|
||||||
onCreateGroupPress: PropTypes.func,
|
onCreateGroupPress: PropTypes.func,
|
||||||
onQualityProfileItemAllowedChange: PropTypes.func
|
onQualityProfileItemAllowedChange: PropTypes.func,
|
||||||
|
onSizeChange: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
QualityProfileItem.defaultProps = {
|
QualityProfileItem.defaultProps = {
|
||||||
|
mode: 'default',
|
||||||
isPreview: false,
|
isPreview: false,
|
||||||
isOverCurrent: false,
|
isOverCurrent: false,
|
||||||
// The drag preview will not connect the drag handle.
|
// The drag preview will not connect the drag handle.
|
||||||
|
|
|
@ -11,7 +11,7 @@ import styles from './QualityProfileItemDragSource.css';
|
||||||
const qualityProfileItemDragSource = {
|
const qualityProfileItemDragSource = {
|
||||||
beginDrag(props) {
|
beginDrag(props) {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
qualityIndex,
|
qualityIndex,
|
||||||
groupId,
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
|
@ -20,7 +20,7 @@ const qualityProfileItemDragSource = {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
editGroups,
|
mode,
|
||||||
qualityIndex,
|
qualityIndex,
|
||||||
groupId,
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
|
@ -110,12 +110,15 @@ class QualityProfileItemDragSource extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
groupId,
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
items,
|
items,
|
||||||
|
minSize,
|
||||||
|
maxSize,
|
||||||
|
preferredSize,
|
||||||
qualityIndex,
|
qualityIndex,
|
||||||
isDragging,
|
isDragging,
|
||||||
isDraggingUp,
|
isDraggingUp,
|
||||||
|
@ -129,7 +132,8 @@ class QualityProfileItemDragSource extends Component {
|
||||||
onItemGroupAllowedChange,
|
onItemGroupAllowedChange,
|
||||||
onItemGroupNameChange,
|
onItemGroupNameChange,
|
||||||
onQualityProfileItemDragMove,
|
onQualityProfileItemDragMove,
|
||||||
onQualityProfileItemDragEnd
|
onQualityProfileItemDragEnd,
|
||||||
|
onSizeChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||||
|
@ -156,7 +160,7 @@ class QualityProfileItemDragSource extends Component {
|
||||||
{
|
{
|
||||||
!!groupId && qualityId == null &&
|
!!groupId && qualityId == null &&
|
||||||
<QualityProfileItemGroup
|
<QualityProfileItemGroup
|
||||||
editGroups={editGroups}
|
mode={mode}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
name={name}
|
name={name}
|
||||||
allowed={allowed}
|
allowed={allowed}
|
||||||
|
@ -172,23 +176,28 @@ class QualityProfileItemDragSource extends Component {
|
||||||
onItemGroupNameChange={onItemGroupNameChange}
|
onItemGroupNameChange={onItemGroupNameChange}
|
||||||
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||||
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||||
|
onSizeChange={onSizeChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
qualityId != null &&
|
qualityId != null &&
|
||||||
<QualityProfileItem
|
<QualityProfileItem
|
||||||
editGroups={editGroups}
|
mode={mode}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
qualityId={qualityId}
|
qualityId={qualityId}
|
||||||
name={name}
|
name={name}
|
||||||
allowed={allowed}
|
allowed={allowed}
|
||||||
|
minSize={minSize}
|
||||||
|
maxSize={maxSize}
|
||||||
|
preferredSize={preferredSize}
|
||||||
qualityIndex={qualityIndex}
|
qualityIndex={qualityIndex}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
isOverCurrent={isOverCurrent}
|
isOverCurrent={isOverCurrent}
|
||||||
connectDragSource={connectDragSource}
|
connectDragSource={connectDragSource}
|
||||||
onCreateGroupPress={onCreateGroupPress}
|
onCreateGroupPress={onCreateGroupPress}
|
||||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
|
onSizeChange={onSizeChange}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,12 +216,15 @@ class QualityProfileItemDragSource extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItemDragSource.propTypes = {
|
QualityProfileItemDragSource.propTypes = {
|
||||||
editGroups: PropTypes.bool.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
groupId: PropTypes.number,
|
groupId: PropTypes.number,
|
||||||
qualityId: PropTypes.number,
|
qualityId: PropTypes.number,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
allowed: PropTypes.bool.isRequired,
|
allowed: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
minSize: PropTypes.number,
|
||||||
|
maxSize: PropTypes.number,
|
||||||
|
preferredSize: PropTypes.number,
|
||||||
qualityIndex: PropTypes.string.isRequired,
|
qualityIndex: PropTypes.string.isRequired,
|
||||||
isDragging: PropTypes.bool,
|
isDragging: PropTypes.bool,
|
||||||
isDraggingUp: PropTypes.bool,
|
isDraggingUp: PropTypes.bool,
|
||||||
|
@ -227,7 +239,8 @@ QualityProfileItemDragSource.propTypes = {
|
||||||
onItemGroupAllowedChange: PropTypes.func,
|
onItemGroupAllowedChange: PropTypes.func,
|
||||||
onItemGroupNameChange: PropTypes.func,
|
onItemGroupNameChange: PropTypes.func,
|
||||||
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
onQualityProfileItemDragEnd: PropTypes.func.isRequired,
|
||||||
|
onSizeChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DropTarget(
|
export default DropTarget(
|
||||||
|
|
|
@ -7,6 +7,10 @@
|
||||||
&.editGroups {
|
&.editGroups {
|
||||||
background: var(--inputBackgroundColor);
|
background: var(--inputBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.editSizes {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qualityProfileItemGroupInfo {
|
.qualityProfileItemGroupInfo {
|
||||||
|
@ -70,6 +74,10 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editSizesQualityNameLabel {
|
||||||
|
composes: qualityNameContainer;
|
||||||
|
}
|
||||||
|
|
||||||
.deleteGroupButton {
|
.deleteGroupButton {
|
||||||
composes: buton from '~Components/Link/IconButton.css';
|
composes: buton from '~Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ interface CssExports {
|
||||||
'dragHandle': string;
|
'dragHandle': string;
|
||||||
'dragIcon': string;
|
'dragIcon': string;
|
||||||
'editGroups': string;
|
'editGroups': string;
|
||||||
|
'editSizes': string;
|
||||||
|
'editSizesQualityNameLabel': string;
|
||||||
'groupQualities': string;
|
'groupQualities': string;
|
||||||
'isDragging': string;
|
'isDragging': string;
|
||||||
'items': string;
|
'items': string;
|
||||||
|
|
|
@ -48,7 +48,7 @@ class QualityProfileItemGroup extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
groupId,
|
groupId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
|
@ -60,20 +60,22 @@ class QualityProfileItemGroup extends Component {
|
||||||
connectDragSource,
|
connectDragSource,
|
||||||
onQualityProfileItemAllowedChange,
|
onQualityProfileItemAllowedChange,
|
||||||
onQualityProfileItemDragMove,
|
onQualityProfileItemDragMove,
|
||||||
onQualityProfileItemDragEnd
|
onQualityProfileItemDragEnd,
|
||||||
|
onSizeChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.qualityProfileItemGroup,
|
styles.qualityProfileItemGroup,
|
||||||
editGroups && styles.editGroups,
|
mode === 'editGroups' && styles.editGroups,
|
||||||
|
mode === 'editSizes' && styles.editSizes,
|
||||||
isDragging && styles.isDragging
|
isDragging && styles.isDragging
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.qualityProfileItemGroupInfo}>
|
<div className={styles.qualityProfileItemGroupInfo}>
|
||||||
{
|
{
|
||||||
editGroups &&
|
mode === 'editGroups' &&
|
||||||
<div className={styles.qualityNameContainer}>
|
<div className={styles.qualityNameContainer}>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.deleteGroupButton}
|
className={styles.deleteGroupButton}
|
||||||
|
@ -92,7 +94,7 @@ class QualityProfileItemGroup extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!editGroups &&
|
mode === 'default' &&
|
||||||
<label
|
<label
|
||||||
className={styles.qualityNameLabel}
|
className={styles.qualityNameLabel}
|
||||||
>
|
>
|
||||||
|
@ -129,31 +131,53 @@ class QualityProfileItemGroup extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
connectDragSource(
|
mode === 'editSizes' &&
|
||||||
<div className={styles.dragHandle}>
|
<label
|
||||||
<Icon
|
className={styles.editSizesQualityNameLabel}
|
||||||
className={styles.dragIcon}
|
>
|
||||||
name={icons.REORDER}
|
<div className={styles.nameContainer}>
|
||||||
title={translate('Reorder')}
|
<div className={classNames(
|
||||||
/>
|
styles.name,
|
||||||
</div>
|
!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>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
editGroups &&
|
mode === 'default' ?
|
||||||
<div className={styles.items}>
|
null :
|
||||||
|
<div className={mode === 'editGroups' ? styles.items : undefined}>
|
||||||
{
|
{
|
||||||
items.map(({ quality }, index) => {
|
items.map(({ quality }, index) => {
|
||||||
return (
|
return (
|
||||||
<QualityProfileItemDragSource
|
<QualityProfileItemDragSource
|
||||||
key={quality.id}
|
key={quality.id}
|
||||||
editGroups={editGroups}
|
mode={mode}
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
qualityId={quality.id}
|
qualityId={quality.id}
|
||||||
name={quality.name}
|
name={quality.name}
|
||||||
allowed={allowed}
|
allowed={allowed}
|
||||||
|
minSize={quality.minSize}
|
||||||
|
maxSize={quality.maxSize}
|
||||||
|
preferredSize={quality.preferredSize}
|
||||||
items={items}
|
items={items}
|
||||||
qualityIndex={`${qualityIndex}.${index + 1}`}
|
qualityIndex={`${qualityIndex}.${index + 1}`}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
|
@ -163,6 +187,7 @@ class QualityProfileItemGroup extends Component {
|
||||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||||
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||||
|
onSizeChange={onSizeChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}).reverse()
|
}).reverse()
|
||||||
|
@ -175,7 +200,7 @@ class QualityProfileItemGroup extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItemGroup.propTypes = {
|
QualityProfileItemGroup.propTypes = {
|
||||||
editGroups: PropTypes.bool,
|
mode: PropTypes.string.isRequired,
|
||||||
groupId: PropTypes.number.isRequired,
|
groupId: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
allowed: PropTypes.bool.isRequired,
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
@ -190,10 +215,12 @@ QualityProfileItemGroup.propTypes = {
|
||||||
onItemGroupNameChange: PropTypes.func.isRequired,
|
onItemGroupNameChange: PropTypes.func.isRequired,
|
||||||
onDeleteGroupPress: PropTypes.func.isRequired,
|
onDeleteGroupPress: PropTypes.func.isRequired,
|
||||||
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
onQualityProfileItemDragEnd: PropTypes.func.isRequired,
|
||||||
|
onSizeChange: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
QualityProfileItemGroup.defaultProps = {
|
QualityProfileItemGroup.defaultProps = {
|
||||||
|
mode: 'default',
|
||||||
// The drag preview will not connect the drag handle.
|
// The drag preview will not connect the drag handle.
|
||||||
connectDragSource: (node) => node
|
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,291 @@
|
||||||
|
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 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 }: { value: 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 }: { value: 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 }: { value: 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}
|
||||||
|
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}
|
||||||
|
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}
|
||||||
|
onChange={handleMaxSizeChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Label kind={kinds.WARNING}>
|
||||||
|
{translate('Maximum')} MiB/
|
||||||
|
{translate('MinuteShorthand')}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,14 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editGroupsButtonIcon {
|
.editSizesButton {
|
||||||
|
composes: button from '~Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editButtonIcon {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'editButtonIcon': string;
|
||||||
'editGroupsButton': string;
|
'editGroupsButton': string;
|
||||||
'editGroupsButtonIcon': string;
|
'editSizesButton': string;
|
||||||
'qualities': string;
|
'qualities': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|
|
@ -21,8 +21,9 @@ class QualityProfileItems extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
qualitiesHeight: 0,
|
defaultHeight: 0,
|
||||||
qualitiesHeightEditGroups: 0
|
editGroupsHeight: 0,
|
||||||
|
editSizesHeight: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,17 +31,23 @@ class QualityProfileItems extends Component {
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onMeasure = ({ height }) => {
|
onMeasure = ({ height }) => {
|
||||||
if (this.props.editGroups) {
|
const heightKey = `${this.props.mode}Height`;
|
||||||
this.setState({
|
|
||||||
qualitiesHeightEditGroups: height
|
this.setState({
|
||||||
});
|
[heightKey]: height
|
||||||
} else {
|
});
|
||||||
this.setState({ qualitiesHeight: height });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onToggleEditGroupsMode = () => {
|
onEditGroupsPress = () => {
|
||||||
this.props.onToggleEditGroupsMode();
|
this.props.onChangeMode('editGroups');
|
||||||
|
};
|
||||||
|
|
||||||
|
onEditSizesPress = () => {
|
||||||
|
this.props.onChangeMode('editSizes');
|
||||||
|
};
|
||||||
|
|
||||||
|
onDefaultModePress = () => {
|
||||||
|
this.props.onChangeMode('default');
|
||||||
};
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -48,7 +55,7 @@ class QualityProfileItems extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
editGroups,
|
mode,
|
||||||
dropQualityIndex,
|
dropQualityIndex,
|
||||||
dropPosition,
|
dropPosition,
|
||||||
qualityProfileItems,
|
qualityProfileItems,
|
||||||
|
@ -57,15 +64,10 @@ class QualityProfileItems extends Component {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
|
||||||
qualitiesHeight,
|
|
||||||
qualitiesHeightEditGroups
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isDragging = dropQualityIndex !== null;
|
const isDragging = dropQualityIndex !== null;
|
||||||
const isDraggingUp = isDragging && dropPosition === 'above';
|
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||||
const isDraggingDown = isDragging && dropPosition === 'below';
|
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||||
const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight;
|
const height = this.state[`${mode}Height`];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup size={sizes.EXTRA_SMALL}>
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
@ -107,16 +109,33 @@ class QualityProfileItems extends Component {
|
||||||
<Button
|
<Button
|
||||||
className={styles.editGroupsButton}
|
className={styles.editGroupsButton}
|
||||||
kind={kinds.PRIMARY}
|
kind={kinds.PRIMARY}
|
||||||
onPress={this.onToggleEditGroupsMode}
|
onPress={mode === 'editGroups' ? this.onDefaultModePress : this.onEditGroupsPress}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.editGroupsButtonIcon}
|
className={styles.editButtonIcon}
|
||||||
name={editGroups ? icons.REORDER : icons.GROUP}
|
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>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -128,21 +147,24 @@ class QualityProfileItems extends Component {
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.qualities}
|
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;
|
const identifier = quality ? quality.id : id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QualityProfileItemDragSource
|
<QualityProfileItemDragSource
|
||||||
key={identifier}
|
key={identifier}
|
||||||
editGroups={editGroups}
|
mode={mode}
|
||||||
groupId={id}
|
groupId={id}
|
||||||
qualityId={quality && quality.id}
|
qualityId={quality && quality.id}
|
||||||
name={quality ? quality.name : name}
|
name={quality ? quality.name : name}
|
||||||
allowed={allowed}
|
allowed={allowed}
|
||||||
items={items}
|
items={items}
|
||||||
|
minSize={minSize}
|
||||||
|
maxSize={maxSize}
|
||||||
|
preferredSize={preferredSize}
|
||||||
qualityIndex={`${index + 1}`}
|
qualityIndex={`${index + 1}`}
|
||||||
isInGroup={false}
|
isInGroup={false}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
|
@ -164,14 +186,14 @@ class QualityProfileItems extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItems.propTypes = {
|
QualityProfileItems.propTypes = {
|
||||||
editGroups: PropTypes.bool.isRequired,
|
mode: PropTypes.string.isRequired,
|
||||||
dragQualityIndex: PropTypes.string,
|
dragQualityIndex: PropTypes.string,
|
||||||
dropQualityIndex: PropTypes.string,
|
dropQualityIndex: PropTypes.string,
|
||||||
dropPosition: PropTypes.string,
|
dropPosition: PropTypes.string,
|
||||||
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object),
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
onToggleEditGroupsMode: PropTypes.func.isRequired
|
onChangeMode: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
QualityProfileItems.defaultProps = {
|
QualityProfileItems.defaultProps = {
|
||||||
|
|
|
@ -14,60 +14,6 @@
|
||||||
line-height: 40px;
|
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) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.qualityDefinition {
|
.qualityDefinition {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -86,8 +32,4 @@
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: inherit;
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sizeLimit {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,9 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'megabytesPerMinute': string;
|
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'qualityDefinition': string;
|
'qualityDefinition': string;
|
||||||
'sizeInput': string;
|
|
||||||
'sizeLimit': string;
|
|
||||||
'sizes': string;
|
|
||||||
'slider': string;
|
|
||||||
'thumb': string;
|
|
||||||
'title': string;
|
'title': string;
|
||||||
'track': string;
|
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default 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 = 400;
|
|
||||||
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,70 +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,
|
|
||||||
minSize: PropTypes.number,
|
|
||||||
maxSize: PropTypes.number,
|
|
||||||
preferredSize: PropTypes.number,
|
|
||||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(QualityDefinitionConnector);
|
|
|
@ -1,40 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function QualityDefinitionLimits(props) {
|
|
||||||
const {
|
|
||||||
bytes,
|
|
||||||
message
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!bytes) {
|
|
||||||
return <div>{message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const thirty = formatBytes(bytes * 30);
|
|
||||||
const fortyFive = formatBytes(bytes * 45);
|
|
||||||
const sixty = formatBytes(bytes * 60);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesThirty', { thirty })}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesFortyFive', { fortyFive })}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesSixty', { sixty })}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitionLimits.propTypes = {
|
|
||||||
bytes: PropTypes.number,
|
|
||||||
message: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityDefinitionLimits;
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface QualityDefinitionLimitsProps {
|
||||||
|
bytes: number | null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityDefinitionLimits(props: QualityDefinitionLimitsProps) {
|
||||||
|
const { bytes, message } = props;
|
||||||
|
|
||||||
|
if (!bytes) {
|
||||||
|
return <div>{message}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thirty = formatBytes(bytes * 30);
|
||||||
|
const fortyFive = formatBytes(bytes * 45);
|
||||||
|
const sixty = formatBytes(bytes * 60);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{translate('MinutesThirty', { thirty })}</div>
|
||||||
|
<div>{translate('MinutesFortyFive', { fortyFive })}</div>
|
||||||
|
<div>{translate('MinutesSixty', { sixty })}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityDefinitionLimits;
|
|
@ -8,24 +8,11 @@
|
||||||
flex: 0 1 250px;
|
flex: 0 1 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sizeLimit {
|
.notice {
|
||||||
flex: 0 1 500px;
|
composes: alert from '~Components/Alert.css';
|
||||||
}
|
|
||||||
|
|
||||||
.megabytesPerMinute {
|
margin: 0;
|
||||||
flex: 0 0 250px;
|
margin-bottom: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.sizeLimitHelpTextContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 20px;
|
|
||||||
max-width: 1000px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sizeLimitHelpText {
|
|
||||||
max-width: 500px;
|
|
||||||
color: var(--helpTextColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
|
|
@ -3,11 +3,8 @@
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'definitions': string;
|
'definitions': string;
|
||||||
'header': string;
|
'header': string;
|
||||||
'megabytesPerMinute': string;
|
'notice': string;
|
||||||
'quality': string;
|
'quality': string;
|
||||||
'sizeLimit': string;
|
|
||||||
'sizeLimitHelpText': string;
|
|
||||||
'sizeLimitHelpTextContainer': string;
|
|
||||||
'title': string;
|
'title': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityDefinitionConnector from './QualityDefinitionConnector';
|
|
||||||
import styles from './QualityDefinitions.css';
|
|
||||||
|
|
||||||
class QualityDefinitions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
advancedSettings,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('QualityDefinitions')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('QualityDefinitionsLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.quality}>
|
|
||||||
{translate('Quality')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.title}>
|
|
||||||
{translate('Title')}
|
|
||||||
</div>
|
|
||||||
<div className={styles.sizeLimit}>
|
|
||||||
{translate('SizeLimit')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
advancedSettings ?
|
|
||||||
<div className={styles.megabytesPerMinute}>
|
|
||||||
{translate('MegabytesPerMinute')}
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.definitions}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QualityDefinitionConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.sizeLimitHelpTextContainer}>
|
|
||||||
<div className={styles.sizeLimitHelpText}>
|
|
||||||
{translate('QualityLimitsSeriesRuntimeHelpText')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitions.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
defaultProfile: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
advancedSettings: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityDefinitions;
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import {
|
||||||
|
fetchQualityDefinitions,
|
||||||
|
saveQualityDefinitions,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QualityDefinition from './QualityDefinition';
|
||||||
|
import styles from './QualityDefinitions.css';
|
||||||
|
|
||||||
|
function qualityDefinitionsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.qualityDefinitions,
|
||||||
|
(qualityDefinitions) => {
|
||||||
|
const items = qualityDefinitions.items.map((item) => {
|
||||||
|
const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {};
|
||||||
|
|
||||||
|
return Object.assign({}, item, pendingChanges);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...qualityDefinitions,
|
||||||
|
items,
|
||||||
|
hasPendingChanges: !!Object.keys(qualityDefinitions.pendingChanges)
|
||||||
|
.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QualityDefinitionsProps {
|
||||||
|
isSaving: boolean;
|
||||||
|
onChildMounted: (saveCallback: () => void) => void;
|
||||||
|
onChildStateChange: ({
|
||||||
|
isSaving,
|
||||||
|
hasPendingChanges,
|
||||||
|
}: {
|
||||||
|
isSaving: boolean;
|
||||||
|
hasPendingChanges: boolean;
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function QualityDefinitions(props: QualityDefinitionsProps) {
|
||||||
|
const { isSaving, onChildMounted, onChildStateChange } = props;
|
||||||
|
const { isFetching, isPopulated, error, items, hasPendingChanges } =
|
||||||
|
useSelector(qualityDefinitionsSelector());
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(saveQualityDefinitions());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchQualityDefinitions());
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(
|
||||||
|
clearPendingChanges({
|
||||||
|
section: 'settings.qualityDefinitions',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChildMounted(handleSavePress);
|
||||||
|
}, [handleSavePress, onChildMounted]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChildStateChange({ isSaving, hasPendingChanges });
|
||||||
|
}, [isSaving, hasPendingChanges, onChildStateChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('QualityDefinitions')}>
|
||||||
|
<Alert className={styles.notice}>
|
||||||
|
{translate('QualityDefinitionsSizeNotice')}
|
||||||
|
</Alert>
|
||||||
|
<PageSectionContent
|
||||||
|
error={error}
|
||||||
|
errorMessage={translate('QualityDefinitionsLoadError')}
|
||||||
|
isFetching={isFetching}
|
||||||
|
isPopulated={isPopulated}
|
||||||
|
>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.quality}>{translate('Quality')}</div>
|
||||||
|
|
||||||
|
<div className={styles.title}>{translate('Title')}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.definitions}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <QualityDefinition key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityDefinitions;
|
|
@ -1,90 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchQualityDefinitions, saveQualityDefinitions } from 'Store/Actions/settingsActions';
|
|
||||||
import QualityDefinitions from './QualityDefinitions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.qualityDefinitions,
|
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
(qualityDefinitions, advancedSettings) => {
|
|
||||||
const items = qualityDefinitions.items.map((item) => {
|
|
||||||
const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {};
|
|
||||||
|
|
||||||
return Object.assign({}, item, pendingChanges);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...qualityDefinitions,
|
|
||||||
items,
|
|
||||||
hasPendingChanges: !_.isEmpty(qualityDefinitions.pendingChanges),
|
|
||||||
advancedSettings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
|
||||||
dispatchSaveQualityDefinitions: saveQualityDefinitions
|
|
||||||
};
|
|
||||||
|
|
||||||
class QualityDefinitionsConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
dispatchFetchQualityDefinitions,
|
|
||||||
dispatchSaveQualityDefinitions,
|
|
||||||
onChildMounted
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchQualityDefinitions();
|
|
||||||
onChildMounted(dispatchSaveQualityDefinitions);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
hasPendingChanges,
|
|
||||||
isSaving,
|
|
||||||
onChildStateChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.isSaving !== isSaving ||
|
|
||||||
prevProps.hasPendingChanges !== hasPendingChanges
|
|
||||||
) {
|
|
||||||
onChildStateChange({
|
|
||||||
isSaving,
|
|
||||||
hasPendingChanges
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QualityDefinitions
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitionsConnector.propTypes = {
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
hasPendingChanges: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveQualityDefinitions: PropTypes.func.isRequired,
|
|
||||||
onChildMounted: PropTypes.func.isRequired,
|
|
||||||
onChildStateChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps, null)(QualityDefinitionsConnector);
|
|
|
@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
import QualityDefinitions from './Definition/QualityDefinitions';
|
||||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||||
|
|
||||||
class Quality extends Component {
|
class Quality extends Component {
|
||||||
|
@ -83,7 +83,7 @@ class Quality extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
<QualityDefinitionsConnector
|
<QualityDefinitions
|
||||||
onChildMounted={this.onChildMounted}
|
onChildMounted={this.onChildMounted}
|
||||||
onChildStateChange={this.onChildStateChange}
|
onChildStateChange={this.onChildStateChange}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Quality from 'Quality/Quality';
|
||||||
|
|
||||||
|
interface QualityDefinition {
|
||||||
|
id: number;
|
||||||
|
quality: Quality;
|
||||||
|
title: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QualityDefinition;
|
|
@ -94,6 +94,7 @@
|
||||||
"@types/lodash": "4.14.194",
|
"@types/lodash": "4.14.194",
|
||||||
"@types/react-lazyload": "3.2.0",
|
"@types/react-lazyload": "3.2.0",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
|
"@types/react-slider": "1.3.6",
|
||||||
"@types/react-text-truncate": "0.14.1",
|
"@types/react-text-truncate": "0.14.1",
|
||||||
"@types/react-window": "1.8.5",
|
"@types/react-window": "1.8.5",
|
||||||
"@types/redux-actions": "2.6.2",
|
"@types/redux-actions": "2.6.2",
|
||||||
|
|
|
@ -4,8 +4,10 @@ using FizzWare.NBuilder;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using NzbDrone.Core.Test.Framework;
|
using NzbDrone.Core.Test.Framework;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
@ -21,13 +23,49 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
private RemoteEpisode _parseResultSingle;
|
private RemoteEpisode _parseResultSingle;
|
||||||
private Series _series;
|
private Series _series;
|
||||||
private List<Episode> _episodes;
|
private List<Episode> _episodes;
|
||||||
private QualityDefinition _qualityType;
|
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_series = Builder<Series>.CreateNew()
|
_series = Builder<Series>.CreateNew()
|
||||||
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(2).Build().ToList())
|
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(2).Build().ToList())
|
||||||
|
.With(c => c.QualityProfile = (LazyLoaded<QualityProfile>)new QualityProfile
|
||||||
|
{
|
||||||
|
Cutoff = Quality.SDTV.Id, Items = new List<QualityProfileQualityItem>
|
||||||
|
{
|
||||||
|
new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = Quality.SDTV,
|
||||||
|
MinSize = 2,
|
||||||
|
MaxSize = 10
|
||||||
|
},
|
||||||
|
new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = Quality.RAWHD,
|
||||||
|
MinSize = 2,
|
||||||
|
MaxSize = null
|
||||||
|
},
|
||||||
|
new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Name = "WEB 720p",
|
||||||
|
Items = new List<QualityProfileQualityItem>
|
||||||
|
{
|
||||||
|
new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = Quality.WEBDL720p,
|
||||||
|
MinSize = 2,
|
||||||
|
MaxSize = 20,
|
||||||
|
},
|
||||||
|
new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = Quality.WEBRip720p,
|
||||||
|
MinSize = 2,
|
||||||
|
MaxSize = 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
_episodes = Builder<Episode>.CreateListOfSize(10)
|
_episodes = Builder<Episode>.CreateListOfSize(10)
|
||||||
|
@ -79,19 +117,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
.Setup(v => v.Get(It.IsAny<Quality>()))
|
.Setup(v => v.Get(It.IsAny<Quality>()))
|
||||||
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
.Returns<Quality>(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v));
|
||||||
|
|
||||||
_qualityType = Builder<QualityDefinition>.CreateNew()
|
|
||||||
.With(q => q.MinSize = 2)
|
|
||||||
.With(q => q.MaxSize = 10)
|
|
||||||
.With(q => q.Quality = Quality.SDTV)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
Mocker.GetMock<IQualityDefinitionService>().Setup(s => s.Get(Quality.SDTV)).Returns(_qualityType);
|
|
||||||
|
|
||||||
Mocker.GetMock<IEpisodeService>().Setup(
|
Mocker.GetMock<IEpisodeService>().Setup(
|
||||||
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
s => s.GetEpisodesBySeason(It.IsAny<int>(), It.IsAny<int>()))
|
||||||
.Returns(_episodes);
|
.Returns(_episodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WithSize(int? minSize, int? maxSize)
|
||||||
|
{
|
||||||
|
_series.QualityProfile.Value.Items[0].MinSize = minSize;
|
||||||
|
_series.QualityProfile.Value.Items[0].MaxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(30, 50, false)]
|
[TestCase(30, 50, false)]
|
||||||
[TestCase(30, 250, true)]
|
[TestCase(30, 250, true)]
|
||||||
[TestCase(30, 500, false)]
|
[TestCase(30, 500, false)]
|
||||||
|
@ -174,12 +210,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_size_is_zero()
|
public void should_return_true_if_size_is_zero()
|
||||||
{
|
{
|
||||||
|
WithSize(10, 20);
|
||||||
|
|
||||||
_series.Runtime = 30;
|
_series.Runtime = 30;
|
||||||
_parseResultSingle.Series = _series;
|
_parseResultSingle.Series = _series;
|
||||||
_parseResultSingle.Release.Size = 0;
|
_parseResultSingle.Release.Size = 0;
|
||||||
_parseResultSingle.Episodes.First().Runtime = 30;
|
_parseResultSingle.Episodes.First().Runtime = 30;
|
||||||
_qualityType.MinSize = 10;
|
|
||||||
_qualityType.MaxSize = 20;
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
@ -187,11 +223,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_unlimited_30_minute()
|
public void should_return_true_if_unlimited_30_minute()
|
||||||
{
|
{
|
||||||
|
WithSize(2, null);
|
||||||
|
|
||||||
_series.Runtime = 30;
|
_series.Runtime = 30;
|
||||||
_parseResultSingle.Series = _series;
|
_parseResultSingle.Series = _series;
|
||||||
_parseResultSingle.Release.Size = 18457280000;
|
_parseResultSingle.Release.Size = 18457280000;
|
||||||
_parseResultSingle.Episodes.First().Runtime = 30;
|
_parseResultSingle.Episodes.First().Runtime = 30;
|
||||||
_qualityType.MaxSize = null;
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
@ -199,11 +236,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_if_unlimited_60_minute()
|
public void should_return_true_if_unlimited_60_minute()
|
||||||
{
|
{
|
||||||
|
WithSize(2, null);
|
||||||
|
|
||||||
_series.Runtime = 60;
|
_series.Runtime = 60;
|
||||||
_parseResultSingle.Series = _series;
|
_parseResultSingle.Series = _series;
|
||||||
_parseResultSingle.Release.Size = 36857280000;
|
_parseResultSingle.Release.Size = 36857280000;
|
||||||
_parseResultSingle.Episodes.First().Runtime = 60;
|
_parseResultSingle.Episodes.First().Runtime = 60;
|
||||||
_qualityType.MaxSize = null;
|
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
@ -217,7 +255,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
_parseResultSingle.Release.Size = 300.Megabytes();
|
_parseResultSingle.Release.Size = 300.Megabytes();
|
||||||
_parseResultSingle.Episodes.First().Runtime = 60;
|
_parseResultSingle.Episodes.First().Runtime = 60;
|
||||||
|
|
||||||
_qualityType.MaxSize = 10;
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_use_grouped_quality_limits()
|
||||||
|
{
|
||||||
|
_parseResultSingle.ParsedEpisodeInfo.Quality = new QualityModel(Quality.WEBDL720p);
|
||||||
|
|
||||||
|
_series.Runtime = 30;
|
||||||
|
_parseResultSingle.Series = _series;
|
||||||
|
_parseResultSingle.Series.SeriesType = SeriesTypes.Daily;
|
||||||
|
_parseResultSingle.Release.Size = 500.Megabytes();
|
||||||
|
_parseResultSingle.Episodes.First().Runtime = 30;
|
||||||
|
|
||||||
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue();
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,21 +22,35 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class PrioritizeDownloadDecisionFixture : CoreTest<DownloadDecisionPriorizationService>
|
public class PrioritizeDownloadDecisionFixture : CoreTest<DownloadDecisionPriorizationService>
|
||||||
{
|
{
|
||||||
|
private Series _series;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
GivenPreferredDownloadProtocol(DownloadProtocol.Usenet);
|
||||||
|
|
||||||
Mocker.GetMock<IQualityDefinitionService>()
|
_series = Builder<Series>.CreateNew()
|
||||||
.Setup(s => s.Get(It.IsAny<Quality>()))
|
.With(e => e.Runtime = 60)
|
||||||
.Returns(new QualityDefinition { PreferredSize = null });
|
.With(e => e.QualityProfile = new QualityProfile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GivenPreferredSize(double? size)
|
private void GivenPreferredSize(QualityProfile qualityProfile, double? size)
|
||||||
{
|
{
|
||||||
Mocker.GetMock<IQualityDefinitionService>()
|
foreach (var qualityOrGroup in qualityProfile.Items)
|
||||||
.Setup(s => s.Get(It.IsAny<Quality>()))
|
{
|
||||||
.Returns(new QualityDefinition { PreferredSize = size });
|
if (qualityOrGroup.Quality != null)
|
||||||
|
{
|
||||||
|
qualityOrGroup.PreferredSize = size;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qualityOrGroup.Items.ForEach(i => i.PreferredSize = size);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Episode GivenEpisode(int id)
|
private Episode GivenEpisode(int id)
|
||||||
|
@ -63,13 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
remoteEpisode.Release.DownloadProtocol = downloadProtocol;
|
remoteEpisode.Release.DownloadProtocol = downloadProtocol;
|
||||||
remoteEpisode.Release.IndexerPriority = indexerPriority;
|
remoteEpisode.Release.IndexerPriority = indexerPriority;
|
||||||
|
|
||||||
remoteEpisode.Series = Builder<Series>.CreateNew()
|
remoteEpisode.Series = _series;
|
||||||
.With(e => e.Runtime = 60)
|
|
||||||
.With(e => e.QualityProfile = new QualityProfile
|
|
||||||
{
|
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
|
||||||
})
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
return remoteEpisode;
|
return remoteEpisode;
|
||||||
}
|
}
|
||||||
|
@ -176,7 +184,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
public void should_order_by_closest_to_preferred_size_if_both_under()
|
public void should_order_by_closest_to_preferred_size_if_both_under()
|
||||||
{
|
{
|
||||||
// 200 MB/Min * 60 Min Runtime = 12000 MB
|
// 200 MB/Min * 60 Min Runtime = 12000 MB
|
||||||
GivenPreferredSize(200);
|
GivenPreferredSize(_series.QualityProfile.Value, 200);
|
||||||
|
|
||||||
var remoteEpisodeSmall = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 1200.Megabytes(), age: 1);
|
var remoteEpisodeSmall = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 1200.Megabytes(), age: 1);
|
||||||
var remoteEpisodeLarge = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 10000.Megabytes(), age: 1);
|
var remoteEpisodeLarge = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 10000.Megabytes(), age: 1);
|
||||||
|
@ -193,7 +201,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
public void should_order_by_closest_to_preferred_size_if_preferred_is_in_between()
|
public void should_order_by_closest_to_preferred_size_if_preferred_is_in_between()
|
||||||
{
|
{
|
||||||
// 46 MB/Min * 60 Min Runtime = 6900 MB
|
// 46 MB/Min * 60 Min Runtime = 6900 MB
|
||||||
GivenPreferredSize(46);
|
GivenPreferredSize(_series.QualityProfile.Value, 46);
|
||||||
|
|
||||||
var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 500.Megabytes(), age: 1);
|
var remoteEpisode1 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 500.Megabytes(), age: 1);
|
||||||
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 2000.Megabytes(), age: 1);
|
var remoteEpisode2 = GivenRemoteEpisode(new List<Episode> { GivenEpisode(1) }, new QualityModel(Quality.HDTV720p), Language.English, size: 2000.Megabytes(), age: 1);
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using Dapper;
|
||||||
|
using FluentMigrator;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using NzbDrone.Common.Serializer;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(207)]
|
||||||
|
public class add_size_to_quality_profiles : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Execute.WithConnection(ConvertProfile);
|
||||||
|
Delete.Column("MinSize").FromTable("QualityDefinitions");
|
||||||
|
Delete.Column("MaxSize").FromTable("QualityDefinitions");
|
||||||
|
Delete.Column("PreferredSize").FromTable("QualityDefinitions");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConvertProfile(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
var updater = new ProfileUpdater207(conn, tran);
|
||||||
|
|
||||||
|
updater.SetSizes();
|
||||||
|
updater.Commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Definition207
|
||||||
|
{
|
||||||
|
public double? MinSize { get; set; }
|
||||||
|
public double? MaxSize { get; set; }
|
||||||
|
public double? PreferredSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Profile207
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public List<ProfileItem207> Items { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProfileItem207
|
||||||
|
{
|
||||||
|
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int? Quality { get; set; }
|
||||||
|
public List<ProfileItem207> Items { get; set; }
|
||||||
|
public bool Allowed { get; set; }
|
||||||
|
public double? MinSize { get; set; }
|
||||||
|
public double? MaxSize { get; set; }
|
||||||
|
public double? PreferredSize { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ProfileUpdater207
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _connection;
|
||||||
|
private readonly IDbTransaction _transaction;
|
||||||
|
|
||||||
|
private List<Profile207> _profiles;
|
||||||
|
private Dictionary<int, Definition207> _sizes;
|
||||||
|
private HashSet<Profile207> _changedProfiles = new HashSet<Profile207>();
|
||||||
|
|
||||||
|
public ProfileUpdater207(IDbConnection conn, IDbTransaction tran)
|
||||||
|
{
|
||||||
|
_connection = conn;
|
||||||
|
_transaction = tran;
|
||||||
|
|
||||||
|
_profiles = GetProfiles();
|
||||||
|
_sizes = GetSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Commit()
|
||||||
|
{
|
||||||
|
var profilesToUpdate = _changedProfiles.Select(p => new
|
||||||
|
{
|
||||||
|
Id = p.Id,
|
||||||
|
Items = p.Items.ToJson()
|
||||||
|
});
|
||||||
|
|
||||||
|
var updateSql = $"UPDATE \"QualityProfiles\" SET \"Items\" = @Items WHERE \"Id\" = @Id";
|
||||||
|
_connection.Execute(updateSql, profilesToUpdate, transaction: _transaction);
|
||||||
|
|
||||||
|
_changedProfiles.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSizes()
|
||||||
|
{
|
||||||
|
foreach (var profile in _profiles)
|
||||||
|
{
|
||||||
|
foreach (var item in profile.Items)
|
||||||
|
{
|
||||||
|
if (item.Quality.HasValue)
|
||||||
|
{
|
||||||
|
if (_sizes.TryGetValue(item.Quality.Value, out var sizes))
|
||||||
|
{
|
||||||
|
item.MinSize = sizes.MinSize;
|
||||||
|
item.MaxSize = sizes.MaxSize;
|
||||||
|
item.PreferredSize = sizes.PreferredSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var groupedItem in item.Items)
|
||||||
|
{
|
||||||
|
if (groupedItem.Quality.HasValue)
|
||||||
|
{
|
||||||
|
if (_sizes.TryGetValue(groupedItem.Quality.Value, out var sizes))
|
||||||
|
{
|
||||||
|
groupedItem.MinSize = sizes.MinSize;
|
||||||
|
groupedItem.MaxSize = sizes.MaxSize;
|
||||||
|
groupedItem.PreferredSize = sizes.PreferredSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_changedProfiles.Add(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Profile207> GetProfiles()
|
||||||
|
{
|
||||||
|
var profiles = new List<Profile207>();
|
||||||
|
|
||||||
|
using (var getProfilesCmd = _connection.CreateCommand())
|
||||||
|
{
|
||||||
|
getProfilesCmd.Transaction = _transaction;
|
||||||
|
getProfilesCmd.CommandText = "SELECT \"Id\", \"Items\" FROM \"QualityProfiles\"";
|
||||||
|
|
||||||
|
using (var profileReader = getProfilesCmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (profileReader.Read())
|
||||||
|
{
|
||||||
|
profiles.Add(new Profile207
|
||||||
|
{
|
||||||
|
Id = profileReader.GetInt32(0),
|
||||||
|
Items = Json.Deserialize<List<ProfileItem207>>(profileReader.GetString(1))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Definition207> GetSizes()
|
||||||
|
{
|
||||||
|
var sizes = new Dictionary<int, Definition207>();
|
||||||
|
|
||||||
|
using (var getDefinitionsCmd = _connection.CreateCommand())
|
||||||
|
{
|
||||||
|
getDefinitionsCmd.Transaction = _transaction;
|
||||||
|
getDefinitionsCmd.CommandText = "SELECT \"Id\", \"MinSize\", \"MaxSize\", \"PreferredSize\" FROM \"QualityDefinitions\"";
|
||||||
|
|
||||||
|
using (var reader = getDefinitionsCmd.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
var id = reader.GetInt32(0);
|
||||||
|
|
||||||
|
double.TryParse(reader.GetValue(1).ToString(), out var minSize);
|
||||||
|
double.TryParse(reader.GetValue(2).ToString(), out var maxSize);
|
||||||
|
double.TryParse(reader.GetValue(3).ToString(), out var preferredSize);
|
||||||
|
|
||||||
|
sizes.Add(id, new Definition207
|
||||||
|
{
|
||||||
|
MinSize = minSize,
|
||||||
|
MaxSize = maxSize,
|
||||||
|
PreferredSize = preferredSize
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -132,7 +132,10 @@ namespace NzbDrone.Core.Datastore
|
||||||
|
|
||||||
Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel()
|
Mapper.Entity<QualityDefinition>("QualityDefinitions").RegisterModel()
|
||||||
.Ignore(d => d.GroupName)
|
.Ignore(d => d.GroupName)
|
||||||
.Ignore(d => d.Weight);
|
.Ignore(d => d.Weight)
|
||||||
|
.Ignore(d => d.MinSize)
|
||||||
|
.Ignore(d => d.MaxSize)
|
||||||
|
.Ignore(d => d.PreferredSize);
|
||||||
|
|
||||||
Mapper.Entity<CustomFormat>("CustomFormats").RegisterModel();
|
Mapper.Entity<CustomFormat>("CustomFormats").RegisterModel();
|
||||||
|
|
||||||
|
|
|
@ -14,16 +14,14 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
{
|
{
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly IDelayProfileService _delayProfileService;
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
|
||||||
|
|
||||||
public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y);
|
public delegate int CompareDelegate(DownloadDecision x, DownloadDecision y);
|
||||||
public delegate int CompareDelegate<TSubject, TValue>(DownloadDecision x, DownloadDecision y);
|
public delegate int CompareDelegate<TSubject, TValue>(DownloadDecision x, DownloadDecision y);
|
||||||
|
|
||||||
public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService, IQualityDefinitionService qualityDefinitionService)
|
public DownloadDecisionComparer(IConfigService configService, IDelayProfileService delayProfileService)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_delayProfileService = delayProfileService;
|
_delayProfileService = delayProfileService;
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Compare(DownloadDecision x, DownloadDecision y)
|
public int Compare(DownloadDecision x, DownloadDecision y)
|
||||||
|
@ -184,15 +182,19 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
{
|
{
|
||||||
var sizeCompare = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode =>
|
var sizeCompare = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode =>
|
||||||
{
|
{
|
||||||
var preferredSize = _qualityDefinitionService.Get(remoteEpisode.ParsedEpisodeInfo.Quality.Quality).PreferredSize;
|
var qualityProfile = remoteEpisode.Series.QualityProfile.Value;
|
||||||
|
var qualityIndex = qualityProfile.GetIndex(remoteEpisode.ParsedEpisodeInfo.Quality.Quality, true);
|
||||||
|
var qualityOrGroup = qualityProfile.Items[qualityIndex.Index];
|
||||||
|
var item = qualityOrGroup.Quality == null ? qualityOrGroup.Items[qualityIndex.GroupIndex] : qualityOrGroup;
|
||||||
|
var preferredSize = item.PreferredSize;
|
||||||
|
|
||||||
// If no value for preferred it means unlimited so fallback to sort largest is best
|
// If no value for preferred it means unlimited so fallback to sort largest is best
|
||||||
if (preferredSize.HasValue && remoteEpisode.Series.Runtime > 0)
|
if (preferredSize.HasValue && remoteEpisode.Series.Runtime > 0)
|
||||||
{
|
{
|
||||||
var preferredMovieSize = remoteEpisode.Series.Runtime * preferredSize.Value.Megabytes();
|
var preferredEpisodeSize = remoteEpisode.Series.Runtime * preferredSize.Value.Megabytes();
|
||||||
|
|
||||||
// Calculate closest to the preferred size
|
// Calculate closest to the preferred size
|
||||||
return Math.Abs((remoteEpisode.Release.Size - preferredMovieSize).Round(200.Megabytes())) * (-1);
|
return Math.Abs((remoteEpisode.Release.Size - preferredEpisodeSize).Round(200.Megabytes())) * (-1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,7 +2,6 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Core.Profiles.Delay;
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.DecisionEngine
|
namespace NzbDrone.Core.DecisionEngine
|
||||||
{
|
{
|
||||||
|
@ -15,13 +14,11 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
{
|
{
|
||||||
private readonly IConfigService _configService;
|
private readonly IConfigService _configService;
|
||||||
private readonly IDelayProfileService _delayProfileService;
|
private readonly IDelayProfileService _delayProfileService;
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
|
||||||
|
|
||||||
public DownloadDecisionPriorizationService(IConfigService configService, IDelayProfileService delayProfileService, IQualityDefinitionService qualityDefinitionService)
|
public DownloadDecisionPriorizationService(IConfigService configService, IDelayProfileService delayProfileService)
|
||||||
{
|
{
|
||||||
_configService = configService;
|
_configService = configService;
|
||||||
_delayProfileService = delayProfileService;
|
_delayProfileService = delayProfileService;
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisions)
|
public List<DownloadDecision> PrioritizeDecisions(List<DownloadDecision> decisions)
|
||||||
|
@ -29,7 +26,7 @@ namespace NzbDrone.Core.DecisionEngine
|
||||||
return decisions.Where(c => c.RemoteEpisode.Series != null)
|
return decisions.Where(c => c.RemoteEpisode.Series != null)
|
||||||
.GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, downloadDecisions) =>
|
.GroupBy(c => c.RemoteEpisode.Series.Id, (seriesId, downloadDecisions) =>
|
||||||
{
|
{
|
||||||
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_configService, _delayProfileService, _qualityDefinitionService));
|
return downloadDecisions.OrderByDescending(decision => decision, new DownloadDecisionComparer(_configService, _delayProfileService));
|
||||||
})
|
})
|
||||||
.SelectMany(c => c)
|
.SelectMany(c => c)
|
||||||
.Union(decisions.Where(c => c.RemoteEpisode.Series == null))
|
.Union(decisions.Where(c => c.RemoteEpisode.Series == null))
|
||||||
|
|
|
@ -3,20 +3,17 @@ using NLog;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
{
|
{
|
||||||
public class AcceptableSizeSpecification : IDecisionEngineSpecification
|
public class AcceptableSizeSpecification : IDecisionEngineSpecification
|
||||||
{
|
{
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
|
||||||
private readonly IEpisodeService _episodeService;
|
private readonly IEpisodeService _episodeService;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
public AcceptableSizeSpecification(IQualityDefinitionService qualityDefinitionService, IEpisodeService episodeService, Logger logger)
|
public AcceptableSizeSpecification(IEpisodeService episodeService, Logger logger)
|
||||||
{
|
{
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
|
||||||
_episodeService = episodeService;
|
_episodeService = episodeService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
@ -78,11 +75,14 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
return Decision.Reject("Runtime of all episodes is 0, unable to validate size until it is available");
|
return Decision.Reject("Runtime of all episodes is 0, unable to validate size until it is available");
|
||||||
}
|
}
|
||||||
|
|
||||||
var qualityDefinition = _qualityDefinitionService.Get(quality);
|
var qualityProfile = subject.Series.QualityProfile.Value;
|
||||||
|
var qualityIndex = qualityProfile.GetIndex(quality, true);
|
||||||
|
var qualityOrGroup = qualityProfile.Items[qualityIndex.Index];
|
||||||
|
var item = qualityOrGroup.Quality == null ? qualityOrGroup.Items[qualityIndex.GroupIndex] : qualityOrGroup;
|
||||||
|
|
||||||
if (qualityDefinition.MinSize.HasValue)
|
if (item.MinSize.HasValue)
|
||||||
{
|
{
|
||||||
var minSize = qualityDefinition.MinSize.Value.Megabytes();
|
var minSize = item.MinSize.Value.Megabytes();
|
||||||
|
|
||||||
// Multiply maxSize by runtime of all episodes
|
// Multiply maxSize by runtime of all episodes
|
||||||
minSize *= runtime;
|
minSize *= runtime;
|
||||||
|
@ -97,13 +97,13 @@ namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!qualityDefinition.MaxSize.HasValue || qualityDefinition.MaxSize.Value == 0)
|
if (!item.MaxSize.HasValue || item.MaxSize.Value == 0)
|
||||||
{
|
{
|
||||||
_logger.Debug("Max size is unlimited, skipping size check");
|
_logger.Debug("Max size is unlimited, skipping size check");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var maxSize = qualityDefinition.MaxSize.Value.Megabytes();
|
var maxSize = item.MaxSize.Value.Megabytes();
|
||||||
|
|
||||||
// Multiply maxSize by runtime of all episodes
|
// Multiply maxSize by runtime of all episodes
|
||||||
maxSize *= runtime;
|
maxSize *= runtime;
|
||||||
|
|
|
@ -406,6 +406,7 @@
|
||||||
"Donate": "Donate",
|
"Donate": "Donate",
|
||||||
"Donations": "Donations",
|
"Donations": "Donations",
|
||||||
"DoneEditingGroups": "Done Editing Groups",
|
"DoneEditingGroups": "Done Editing Groups",
|
||||||
|
"DoneEditingSizes": "Done Editing Sizes",
|
||||||
"DotNetVersion": ".NET",
|
"DotNetVersion": ".NET",
|
||||||
"Download": "Download",
|
"Download": "Download",
|
||||||
"DownloadClient": "Download Client",
|
"DownloadClient": "Download Client",
|
||||||
|
@ -590,6 +591,7 @@
|
||||||
"EditSelectedSeries": "Edit Selected Series",
|
"EditSelectedSeries": "Edit Selected Series",
|
||||||
"EditSeries": "Edit Series",
|
"EditSeries": "Edit Series",
|
||||||
"EditSeriesModalHeader": "Edit - {title}",
|
"EditSeriesModalHeader": "Edit - {title}",
|
||||||
|
"EditSizes": "Edit Sizes",
|
||||||
"Enable": "Enable",
|
"Enable": "Enable",
|
||||||
"EnableAutomaticAdd": "Enable Automatic Add",
|
"EnableAutomaticAdd": "Enable Automatic Add",
|
||||||
"EnableAutomaticAddSeriesHelpText": "Add series from this list to {appName} when syncs are performed via the UI or by {appName}",
|
"EnableAutomaticAddSeriesHelpText": "Add series from this list to {appName} when syncs are performed via the UI or by {appName}",
|
||||||
|
@ -1112,7 +1114,7 @@
|
||||||
"MatchedToEpisodes": "Matched to Episodes",
|
"MatchedToEpisodes": "Matched to Episodes",
|
||||||
"MatchedToSeason": "Matched to Season",
|
"MatchedToSeason": "Matched to Season",
|
||||||
"MatchedToSeries": "Matched to Series",
|
"MatchedToSeries": "Matched to Series",
|
||||||
"Max": "Max",
|
"Maximum": "Maximum",
|
||||||
"MaximumLimits": "Maximum Limits",
|
"MaximumLimits": "Maximum Limits",
|
||||||
"MaximumSingleEpisodeAge": "Maximum Single Episode Age",
|
"MaximumSingleEpisodeAge": "Maximum Single Episode Age",
|
||||||
"MaximumSingleEpisodeAgeHelpText": "During a full season search only season packs will be allowed when the season's last episode is older than this setting. Standard series only. Use 0 to disable.",
|
"MaximumSingleEpisodeAgeHelpText": "During a full season search only season packs will be allowed when the season's last episode is older than this setting. Standard series only. Use 0 to disable.",
|
||||||
|
@ -1126,7 +1128,6 @@
|
||||||
"MediaManagementSettingsLoadError": "Unable to load Media Management settings",
|
"MediaManagementSettingsLoadError": "Unable to load Media Management settings",
|
||||||
"MediaManagementSettingsSummary": "Naming, file management settings and root folders",
|
"MediaManagementSettingsSummary": "Naming, file management settings and root folders",
|
||||||
"Medium": "Medium",
|
"Medium": "Medium",
|
||||||
"MegabytesPerMinute": "Megabytes Per Minute",
|
|
||||||
"Message": "Message",
|
"Message": "Message",
|
||||||
"Metadata": "Metadata",
|
"Metadata": "Metadata",
|
||||||
"MetadataLoadError": "Unable to load Metadata",
|
"MetadataLoadError": "Unable to load Metadata",
|
||||||
|
@ -1151,7 +1152,7 @@
|
||||||
"MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo with full series metadata",
|
"MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo with full series metadata",
|
||||||
"MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Include TheTVDB show URL in tvshow.nfo (can be combined with 'Series Metadata')",
|
"MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Include TheTVDB show URL in tvshow.nfo (can be combined with 'Series Metadata')",
|
||||||
"MidseasonFinale": "Midseason Finale",
|
"MidseasonFinale": "Midseason Finale",
|
||||||
"Min": "Min",
|
"Minimum": "Minimum",
|
||||||
"MinimumAge": "Minimum Age",
|
"MinimumAge": "Minimum Age",
|
||||||
"MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.",
|
"MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.",
|
||||||
"MinimumCustomFormatScore": "Minimum Custom Format Score",
|
"MinimumCustomFormatScore": "Minimum Custom Format Score",
|
||||||
|
@ -1159,6 +1160,8 @@
|
||||||
"MinimumFreeSpace": "Minimum Free Space",
|
"MinimumFreeSpace": "Minimum Free Space",
|
||||||
"MinimumFreeSpaceHelpText": "Prevent import if it would leave less than this amount of disk space available",
|
"MinimumFreeSpaceHelpText": "Prevent import if it would leave less than this amount of disk space available",
|
||||||
"MinimumLimits": "Minimum Limits",
|
"MinimumLimits": "Minimum Limits",
|
||||||
|
"Minute": "minute",
|
||||||
|
"MinuteShorthand": "m",
|
||||||
"MinutesFortyFive": "45 Minutes: {fortyFive}",
|
"MinutesFortyFive": "45 Minutes: {fortyFive}",
|
||||||
"MinutesSixty": "60 Minutes: {sixty}",
|
"MinutesSixty": "60 Minutes: {sixty}",
|
||||||
"MinutesThirty": "30 Minutes: {thirty}",
|
"MinutesThirty": "30 Minutes: {thirty}",
|
||||||
|
@ -1564,7 +1567,7 @@
|
||||||
"QualityCutoffNotMet": "Quality cutoff has not been met",
|
"QualityCutoffNotMet": "Quality cutoff has not been met",
|
||||||
"QualityDefinitions": "Quality Definitions",
|
"QualityDefinitions": "Quality Definitions",
|
||||||
"QualityDefinitionsLoadError": "Unable to load Quality Definitions",
|
"QualityDefinitionsLoadError": "Unable to load Quality Definitions",
|
||||||
"QualityLimitsSeriesRuntimeHelpText": "Limits are automatically adjusted for the series runtime and number of episodes in the file.",
|
"QualityDefinitionsSizeNotice": "Size restrictions have been moved to Quality Profiles",
|
||||||
"QualityProfile": "Quality Profile",
|
"QualityProfile": "Quality Profile",
|
||||||
"QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection",
|
"QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection",
|
||||||
"QualityProfiles": "Quality Profiles",
|
"QualityProfiles": "Quality Profiles",
|
||||||
|
|
|
@ -16,6 +16,9 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
public Quality Quality { get; set; }
|
public Quality Quality { get; set; }
|
||||||
public List<QualityProfileQualityItem> Items { get; set; }
|
public List<QualityProfileQualityItem> Items { get; set; }
|
||||||
public bool Allowed { get; set; }
|
public bool Allowed { get; set; }
|
||||||
|
public double? MinSize { get; set; }
|
||||||
|
public double? MaxSize { get; set; }
|
||||||
|
public double? PreferredSize { get; set; }
|
||||||
|
|
||||||
public QualityProfileQualityItem()
|
public QualityProfileQualityItem()
|
||||||
{
|
{
|
||||||
|
|
|
@ -193,7 +193,14 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
{
|
{
|
||||||
var quality = group.First().Quality;
|
var quality = group.First().Quality;
|
||||||
|
|
||||||
items.Add(new QualityProfileQualityItem { Quality = group.First().Quality, Allowed = allowed.Contains(quality) });
|
items.Add(new QualityProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = group.First().Quality,
|
||||||
|
Allowed = allowed.Contains(quality),
|
||||||
|
MinSize = group.First().MinSize,
|
||||||
|
MaxSize = group.First().MaxSize,
|
||||||
|
PreferredSize = group.First().PreferredSize
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +213,10 @@ namespace NzbDrone.Core.Profiles.Qualities
|
||||||
Items = group.Select(g => new QualityProfileQualityItem
|
Items = group.Select(g => new QualityProfileQualityItem
|
||||||
{
|
{
|
||||||
Quality = g.Quality,
|
Quality = g.Quality,
|
||||||
Allowed = groupAllowed
|
Allowed = groupAllowed,
|
||||||
|
MinSize = g.MinSize,
|
||||||
|
MaxSize = g.MaxSize,
|
||||||
|
PreferredSize = g.PreferredSize
|
||||||
}).ToList(),
|
}).ToList(),
|
||||||
Allowed = groupAllowed
|
Allowed = groupAllowed
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Qualities.Commands
|
namespace NzbDrone.Core.Qualities.Commands
|
||||||
{
|
{
|
||||||
|
|
|
@ -47,6 +47,7 @@ namespace NzbDrone.Core.Qualities
|
||||||
public void UpdateMany(List<QualityDefinition> qualityDefinitions)
|
public void UpdateMany(List<QualityDefinition> qualityDefinitions)
|
||||||
{
|
{
|
||||||
_repo.UpdateMany(qualityDefinitions);
|
_repo.UpdateMany(qualityDefinitions);
|
||||||
|
_cache.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<QualityDefinition> All()
|
public List<QualityDefinition> All()
|
||||||
|
@ -119,9 +120,6 @@ namespace NzbDrone.Core.Qualities
|
||||||
{
|
{
|
||||||
var existing = existingDefinitions.SingleOrDefault(d => d.Quality == definition.Quality);
|
var existing = existingDefinitions.SingleOrDefault(d => d.Quality == definition.Quality);
|
||||||
|
|
||||||
existing.MinSize = definition.MinSize;
|
|
||||||
existing.MaxSize = definition.MaxSize;
|
|
||||||
existing.PreferredSize = definition.PreferredSize;
|
|
||||||
existing.Title = message.ResetTitles ? definition.Title : existing.Title;
|
existing.Title = message.ResetTitles ? definition.Title : existing.Title;
|
||||||
|
|
||||||
updateList.Add(existing);
|
updateList.Add(existing);
|
||||||
|
|
|
@ -24,6 +24,9 @@ namespace Sonarr.Api.V3.Profiles.Quality
|
||||||
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
||||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||||
public bool Allowed { get; set; }
|
public bool Allowed { get; set; }
|
||||||
|
public double? MinSize { get; set; }
|
||||||
|
public double? MaxSize { get; set; }
|
||||||
|
public double? PreferredSize { get; set; }
|
||||||
|
|
||||||
public QualityProfileQualityItemResource()
|
public QualityProfileQualityItemResource()
|
||||||
{
|
{
|
||||||
|
@ -73,7 +76,10 @@ namespace Sonarr.Api.V3.Profiles.Quality
|
||||||
Name = model.Name,
|
Name = model.Name,
|
||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
Items = model.Items.ConvertAll(ToResource),
|
Items = model.Items.ConvertAll(ToResource),
|
||||||
Allowed = model.Allowed
|
Allowed = model.Allowed,
|
||||||
|
MinSize = model.MinSize,
|
||||||
|
MaxSize = model.MaxSize,
|
||||||
|
PreferredSize = model.PreferredSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +126,10 @@ namespace Sonarr.Api.V3.Profiles.Quality
|
||||||
Name = resource.Name,
|
Name = resource.Name,
|
||||||
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
||||||
Items = resource.Items.ConvertAll(ToModel),
|
Items = resource.Items.ConvertAll(ToModel),
|
||||||
Allowed = resource.Allowed
|
Allowed = resource.Allowed,
|
||||||
|
MinSize = resource.MinSize,
|
||||||
|
MaxSize = resource.MaxSize,
|
||||||
|
PreferredSize = resource.PreferredSize
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
using Sonarr.Http.REST;
|
using Sonarr.Http.REST;
|
||||||
|
@ -10,12 +10,6 @@ namespace Sonarr.Api.V3.Qualities
|
||||||
public Quality Quality { get; set; }
|
public Quality Quality { get; set; }
|
||||||
|
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
|
|
||||||
public int Weight { get; set; }
|
|
||||||
|
|
||||||
public double? MinSize { get; set; }
|
|
||||||
public double? MaxSize { get; set; }
|
|
||||||
public double? PreferredSize { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class QualityDefinitionResourceMapper
|
public static class QualityDefinitionResourceMapper
|
||||||
|
@ -31,11 +25,7 @@ namespace Sonarr.Api.V3.Qualities
|
||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
Title = model.Title,
|
Title = model.Title
|
||||||
Weight = model.Weight,
|
|
||||||
MinSize = model.MinSize,
|
|
||||||
MaxSize = model.MaxSize,
|
|
||||||
PreferredSize = model.PreferredSize
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,11 +40,7 @@ namespace Sonarr.Api.V3.Qualities
|
||||||
{
|
{
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
Quality = resource.Quality,
|
Quality = resource.Quality,
|
||||||
Title = resource.Title,
|
Title = resource.Title
|
||||||
Weight = resource.Weight,
|
|
||||||
MinSize = resource.MinSize,
|
|
||||||
MaxSize = resource.MaxSize,
|
|
||||||
PreferredSize = resource.PreferredSize
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1499,6 +1499,13 @@
|
||||||
"@types/history" "^4.7.11"
|
"@types/history" "^4.7.11"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
|
|
||||||
|
"@types/react-slider@1.3.6":
|
||||||
|
version "1.3.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/react-slider/-/react-slider-1.3.6.tgz#6f5602be93ab1cb3d273428c87aa227ad2ff68ff"
|
||||||
|
integrity sha512-RS8XN5O159YQ6tu3tGZIQz1/9StMLTg/FCIPxwqh2gwVixJnlfIodtVx+fpXVMZHe7A58lAX1Q4XTgAGOQaCQg==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
|
||||||
"@types/react-text-truncate@0.14.1":
|
"@types/react-text-truncate@0.14.1":
|
||||||
version "0.14.1"
|
version "0.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"
|
resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"
|
||||||
|
|
Loading…
Reference in New Issue