From 20e68c1d3c92b3a6e3e2d451cb41da75afa452ed Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 27 Apr 2024 14:49:15 -0700 Subject: [PATCH] New: Quality limits are part of Quality Profile Closes #613 --- frontend/src/App/State/AppSectionState.ts | 8 + frontend/src/App/State/SettingsAppState.ts | 7 + .../Quality/EditQualityProfileModal.js | 2 +- .../Quality/EditQualityProfileModalContent.js | 21 +- ...EditQualityProfileModalContentConnector.js | 52 ++- .../Profiles/Quality/QualityProfileItem.css | 9 + .../Quality/QualityProfileItem.css.d.ts | 1 + .../Profiles/Quality/QualityProfileItem.js | 62 +++- .../Quality/QualityProfileItemDragSource.js | 29 +- .../Quality/QualityProfileItemGroup.css | 8 + .../Quality/QualityProfileItemGroup.css.d.ts | 2 + .../Quality/QualityProfileItemGroup.js | 65 +++- .../Quality/QualityProfileItemSize.css | 60 +++ .../Quality/QualityProfileItemSize.css.d.ts | 14 + .../Quality/QualityProfileItemSize.tsx | 291 +++++++++++++++ .../Profiles/Quality/QualityProfileItems.css | 9 +- .../Quality/QualityProfileItems.css.d.ts | 3 +- .../Profiles/Quality/QualityProfileItems.js | 76 ++-- .../Quality/Definition/QualityDefinition.css | 58 --- .../Definition/QualityDefinition.css.d.ts | 7 - .../Quality/Definition/QualityDefinition.js | 346 ------------------ .../Quality/Definition/QualityDefinition.tsx | 46 +++ .../Definition/QualityDefinitionConnector.js | 70 ---- .../Definition/QualityDefinitionLimits.js | 40 -- .../Definition/QualityDefinitionLimits.tsx | 30 ++ .../Quality/Definition/QualityDefinitions.css | 21 +- .../Definition/QualityDefinitions.css.d.ts | 5 +- .../Quality/Definition/QualityDefinitions.js | 80 ---- .../Quality/Definition/QualityDefinitions.tsx | 106 ++++++ .../Definition/QualityDefinitionsConnector.js | 90 ----- frontend/src/Settings/Quality/Quality.js | 4 +- frontend/src/typings/QualityDefinition.ts | 10 + package.json | 1 + .../AcceptableSizeSpecificationFixture.cs | 78 +++- .../207_add_size_to_quality_profiles.cs | 182 +++++++++ src/NzbDrone.Core/Datastore/TableMapping.cs | 5 +- .../DownloadDecisionComparer.cs | 10 +- .../AcceptableSizeSpecification.cs | 18 +- src/NzbDrone.Core/Localization/Core/en.json | 11 +- .../Qualities/QualityProfileQualityItem.cs | 3 + .../Qualities/QualityProfileService.cs | 14 +- .../ResetQualityDefinitionsCommand.cs | 2 +- .../Qualities/QualityDefinitionService.cs | 4 +- .../Quality/QualityProfileResource.cs | 13 +- .../Qualities/QualityDefinitionResource.cs | 20 +- yarn.lock | 7 + 46 files changed, 1145 insertions(+), 855 deletions(-) create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css.d.ts create mode 100644 frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.tsx delete mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinition.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinition.tsx delete mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js delete mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.tsx delete mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitions.js create mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitions.tsx delete mode 100644 frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js create mode 100644 frontend/src/typings/QualityDefinition.ts create mode 100644 src/NzbDrone.Core/Datastore/Migration/207_add_size_to_quality_profiles.cs diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index 30af90d34..f4b8b2722 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -43,6 +43,14 @@ export interface AppSectionItemState { item: T; } +export interface AppSectionListState { + isFetching: boolean; + isPopulated: boolean; + error: Error; + items: T[]; + pendingChanges: Partial[]; +} + interface AppSectionState { isFetching: boolean; isPopulated: boolean; diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index e4322db69..a1ba6299b 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -1,6 +1,7 @@ import AppSectionState, { AppSectionDeleteState, AppSectionItemState, + AppSectionListState, AppSectionSaveState, AppSectionSchemaState, PagedAppSectionState, @@ -13,6 +14,7 @@ import ImportListOptionsSettings from 'typings/ImportListOptionsSettings'; import Indexer from 'typings/Indexer'; import IndexerFlag from 'typings/IndexerFlag'; import Notification from 'typings/Notification'; +import QualityDefinition from 'typings/QualityDefinition'; import QualityProfile from 'typings/QualityProfile'; import { UiSettings } from 'typings/UiSettings'; @@ -35,6 +37,10 @@ export interface NotificationAppState extends AppSectionState, AppSectionDeleteState {} +export interface QualityDefinitionsAppState + extends AppSectionListState, + AppSectionSaveState {} + export interface QualityProfilesAppState extends AppSectionState, AppSectionSchemaState {} @@ -65,6 +71,7 @@ interface SettingsAppState { indexers: IndexerAppState; languages: LanguageSettingsAppState; notifications: NotificationAppState; + qualityDefinitions: QualityDefinitionsAppState; qualityProfiles: QualityProfilesAppState; ui: UiSettingsAppState; } diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js index d6f2b8ed0..4b980c67c 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModal.js @@ -21,7 +21,7 @@ class EditQualityProfileModal extends Component { // Listeners onContentHeightChange = (height) => { - if (this.state.height === 'auto' || height > this.state.height) { + if (this.state.height === 'auto' || height !== 0) { this.setState({ height }); } }; diff --git a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js index ece0e8728..e0582a784 100644 --- a/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js +++ b/frontend/src/Settings/Profiles/Quality/EditQualityProfileModalContent.js @@ -44,6 +44,9 @@ class EditQualityProfileModalContent extends Component { this.state = { headerHeight: 0, bodyHeight: 0, + defaultBodyHeight: 0, + editGroupsBodyHeight: 0, + editSizesBodyHeight: 0, footerHeight: 0 }; } @@ -51,17 +54,18 @@ class EditQualityProfileModalContent extends Component { componentDidUpdate(prevProps, prevState) { const { headerHeight, - bodyHeight, footerHeight } = this.state; + const bodyHeight = this.state[`${this.props.mode}BodyHeight`]; + if ( headerHeight > 0 && bodyHeight > 0 && footerHeight > 0 && ( headerHeight !== prevState.headerHeight || - bodyHeight !== prevState.bodyHeight || + bodyHeight !== prevState[`${prevProps.mode}BodyHeight`] || footerHeight !== prevState.footerHeight ) ) { @@ -77,15 +81,16 @@ class EditQualityProfileModalContent extends Component { // Listeners onHeaderMeasure = ({ height }) => { - if (height > this.state.headerHeight) { + if (height !== this.state.headerHeight) { this.setState({ headerHeight: height }); } }; onBodyMeasure = ({ height }) => { + const heightKey = `${this.props.mode}BodyHeight`; - if (height > this.state.bodyHeight) { - this.setState({ bodyHeight: height }); + if (height !== this.state[heightKey]) { + this.setState({ [heightKey]: height }); } }; @@ -100,7 +105,7 @@ class EditQualityProfileModalContent extends Component { render() { const { - editGroups, + mode, isFetching, error, isSaving, @@ -251,7 +256,7 @@ class EditQualityProfileModalContent extends Component {
{ + const qualityProfile = _.cloneDeep(this.props.item); + const items = qualityProfile.items.value; + let quality = null; + + // eslint-disable-next-line guard-for-in + for (const index in items) { + const item = items[index]; + + if (item.quality?.id === id) { + quality = item; + break; + } + + // eslint-disable-next-line guard-for-in + for (const i in item.items) { + const nestedItem = items[i]; + + if (nestedItem.quality?.id === id) { + quality = nestedItem; + break; + } + } + + if (quality) { + break; + } + } + + if (!quality) { + return; + } + + quality.minSize = minSize; + quality.maxSize = maxSize; + quality.preferredSize = preferredSize; + + this.props.setQualityProfileValue({ + name: 'items', + value: items + }); + }; + onCreateGroupPress = (id) => { const qualityProfile = _.cloneDeep(this.props.item); const items = qualityProfile.items.value; @@ -439,8 +482,8 @@ class EditQualityProfileModalContentConnector extends Component { }); }; - onToggleEditGroupsMode = () => { - this.setState({ editGroups: !this.state.editGroups }); + onChangeMode = (mode) => { + this.setState({ mode }); }; // @@ -466,7 +509,8 @@ class EditQualityProfileModalContentConnector extends Component { onQualityProfileItemDragMove={this.onQualityProfileItemDragMove} onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd} onQualityProfileFormatItemScoreChange={this.onQualityProfileFormatItemScoreChange} - onToggleEditGroupsMode={this.onToggleEditGroupsMode} + onChangeMode={this.onChangeMode} + onSizeChange={this.onSizeChange} /> ); } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css index c10bcd200..d19139674 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css @@ -9,6 +9,11 @@ &.isInGroup { border-style: dashed; } + + &.editSizes { + flex-direction: column; + padding: 10px; + } } .checkInputContainer { @@ -32,6 +37,10 @@ font-weight: normal; line-height: $qualityProfileItemHeight; cursor: pointer; + + &.editSizes { + cursor: default; + } } .qualityName { diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css.d.ts b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css.d.ts index 2f18ba539..24b0577ce 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css.d.ts +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.css.d.ts @@ -6,6 +6,7 @@ interface CssExports { 'createGroupButton': string; 'dragHandle': string; 'dragIcon': string; + 'editSizes': string; 'isDragging': string; 'isInGroup': string; 'isPreview': string; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js index 5550464cb..ac05a9f23 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js @@ -6,6 +6,7 @@ import Icon from 'Components/Icon'; import IconButton from 'Components/Link/IconButton'; import { icons } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; +import QualityProfileItemSize from './QualityProfileItemSize'; import styles from './QualityProfileItem.css'; class QualityProfileItem extends Component { @@ -36,20 +37,26 @@ class QualityProfileItem extends Component { render() { const { - editGroups, + mode, isPreview, + qualityId, groupId, name, allowed, + minSize, + maxSize, + preferredSize, isDragging, isOverCurrent, - connectDragSource + connectDragSource, + onSizeChange } = this.props; return (
{ - connectDragSource( -
- + -
- ) +
: + null + } + + { + mode === 'editSizes' ? null : + connectDragSource( +
+ +
+ ) }
); @@ -108,21 +133,26 @@ class QualityProfileItem extends Component { } QualityProfileItem.propTypes = { - editGroups: PropTypes.bool, + mode: PropTypes.string.isRequired, isPreview: PropTypes.bool, groupId: PropTypes.number, qualityId: PropTypes.number.isRequired, name: PropTypes.string.isRequired, allowed: PropTypes.bool.isRequired, + minSize: PropTypes.number, + maxSize: PropTypes.number, + preferredSize: PropTypes.number, isDragging: PropTypes.bool.isRequired, isOverCurrent: PropTypes.bool.isRequired, isInGroup: PropTypes.bool, connectDragSource: PropTypes.func, onCreateGroupPress: PropTypes.func, - onQualityProfileItemAllowedChange: PropTypes.func + onQualityProfileItemAllowedChange: PropTypes.func, + onSizeChange: PropTypes.func }; QualityProfileItem.defaultProps = { + mode: 'default', isPreview: false, isOverCurrent: false, // The drag preview will not connect the drag handle. diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js index b47470171..4318e3489 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemDragSource.js @@ -11,7 +11,7 @@ import styles from './QualityProfileItemDragSource.css'; const qualityProfileItemDragSource = { beginDrag(props) { const { - editGroups, + mode, qualityIndex, groupId, qualityId, @@ -20,7 +20,7 @@ const qualityProfileItemDragSource = { } = props; return { - editGroups, + mode, qualityIndex, groupId, qualityId, @@ -110,12 +110,15 @@ class QualityProfileItemDragSource extends Component { render() { const { - editGroups, + mode, groupId, qualityId, name, allowed, items, + minSize, + maxSize, + preferredSize, qualityIndex, isDragging, isDraggingUp, @@ -129,7 +132,8 @@ class QualityProfileItemDragSource extends Component { onItemGroupAllowedChange, onItemGroupNameChange, onQualityProfileItemDragMove, - onQualityProfileItemDragEnd + onQualityProfileItemDragEnd, + onSizeChange } = this.props; const isBefore = !isDragging && isDraggingUp && isOverCurrent; @@ -156,7 +160,7 @@ class QualityProfileItemDragSource extends Component { { !!groupId && qualityId == null && } { qualityId != null && } @@ -207,12 +216,15 @@ class QualityProfileItemDragSource extends Component { } QualityProfileItemDragSource.propTypes = { - editGroups: PropTypes.bool.isRequired, + mode: PropTypes.string.isRequired, groupId: PropTypes.number, qualityId: PropTypes.number, name: PropTypes.string.isRequired, allowed: PropTypes.bool.isRequired, items: PropTypes.arrayOf(PropTypes.object), + minSize: PropTypes.number, + maxSize: PropTypes.number, + preferredSize: PropTypes.number, qualityIndex: PropTypes.string.isRequired, isDragging: PropTypes.bool, isDraggingUp: PropTypes.bool, @@ -227,7 +239,8 @@ QualityProfileItemDragSource.propTypes = { onItemGroupAllowedChange: PropTypes.func, onItemGroupNameChange: PropTypes.func, onQualityProfileItemDragMove: PropTypes.func.isRequired, - onQualityProfileItemDragEnd: PropTypes.func.isRequired + onQualityProfileItemDragEnd: PropTypes.func.isRequired, + onSizeChange: PropTypes.func.isRequired }; export default DropTarget( diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css index 772bd9a80..6f13db6d5 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css @@ -7,6 +7,10 @@ &.editGroups { background: var(--inputBackgroundColor); } + + &.editSizes { + padding: 10px; + } } .qualityProfileItemGroupInfo { @@ -70,6 +74,10 @@ cursor: pointer; } +.editSizesQualityNameLabel { + composes: qualityNameContainer; +} + .deleteGroupButton { composes: buton from '~Components/Link/IconButton.css'; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css.d.ts b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css.d.ts index 73ffeb3e5..1b6daf0e4 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css.d.ts +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.css.d.ts @@ -7,6 +7,8 @@ interface CssExports { 'dragHandle': string; 'dragIcon': string; 'editGroups': string; + 'editSizes': string; + 'editSizesQualityNameLabel': string; 'groupQualities': string; 'isDragging': string; 'items': string; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js index 499b62016..2a307ee3e 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js @@ -48,7 +48,7 @@ class QualityProfileItemGroup extends Component { render() { const { - editGroups, + mode, groupId, name, allowed, @@ -60,20 +60,22 @@ class QualityProfileItemGroup extends Component { connectDragSource, onQualityProfileItemAllowedChange, onQualityProfileItemDragMove, - onQualityProfileItemDragEnd + onQualityProfileItemDragEnd, + onSizeChange } = this.props; return (
{ - editGroups && + mode === 'editGroups' &&
@@ -129,31 +131,53 @@ class QualityProfileItemGroup extends Component { } { - connectDragSource( -
- -
- ) + mode === 'editSizes' && + + } + + { + mode === 'editSizes' ? null : + connectDragSource( +
+ +
+ ) }
{ - editGroups && -
+ mode === 'default' ? + null : +
{ items.map(({ quality }, index) => { return ( ); }).reverse() @@ -175,7 +200,7 @@ class QualityProfileItemGroup extends Component { } QualityProfileItemGroup.propTypes = { - editGroups: PropTypes.bool, + mode: PropTypes.string.isRequired, groupId: PropTypes.number.isRequired, name: PropTypes.string.isRequired, allowed: PropTypes.bool.isRequired, @@ -190,10 +215,12 @@ QualityProfileItemGroup.propTypes = { onItemGroupNameChange: PropTypes.func.isRequired, onDeleteGroupPress: PropTypes.func.isRequired, onQualityProfileItemDragMove: PropTypes.func.isRequired, - onQualityProfileItemDragEnd: PropTypes.func.isRequired + onQualityProfileItemDragEnd: PropTypes.func.isRequired, + onSizeChange: PropTypes.func }; QualityProfileItemGroup.defaultProps = { + mode: 'default', // The drag preview will not connect the drag handle. connectDragSource: (node) => node }; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css new file mode 100644 index 000000000..2aee4c641 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css @@ -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; +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css.d.ts b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css.d.ts new file mode 100644 index 000000000..0d1090ae3 --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.css.d.ts @@ -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; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.tsx b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.tsx new file mode 100644 index 000000000..9da92dd0f --- /dev/null +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemSize.tsx @@ -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) { + return
; +} + +function thumbRenderer(props: HTMLProps) { + return
; +} + +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({ + 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 ( +
+ {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} + {/* @ts-ignore React version mismatch */} + + +
+
+ {minSixty}} + title={translate('MinimumLimits')} + body={ + + } + position={tooltipPositions.BOTTOM} + /> +
+ +
+ {preferredSixty}} + title={translate('PreferredSize')} + body={ + + } + position={tooltipPositions.BOTTOM} + /> +
+ +
+ {maxSixty}} + title={translate('MaximumLimits')} + body={ + + } + position={tooltipPositions.BOTTOM} + /> +
+
+ +
+
+ + +
+ +
+ + + +
+ +
+ + + +
+
+
+ ); +} diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css index 002e555a7..cb442f211 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css @@ -4,7 +4,14 @@ margin-top: 10px; } -.editGroupsButtonIcon { +.editSizesButton { + composes: button from '~Components/Link/Button.css'; + + margin-top: 10px; + margin-left: 10px; +} + +.editButtonIcon { margin-right: 8px; } diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css.d.ts b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css.d.ts index 159ce7759..d306ee6c4 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css.d.ts +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.css.d.ts @@ -1,8 +1,9 @@ // This file is automatically generated. // Please do not change this file! interface CssExports { + 'editButtonIcon': string; 'editGroupsButton': string; - 'editGroupsButtonIcon': string; + 'editSizesButton': string; 'qualities': string; } export const cssExports: CssExports; diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js index 5166894da..e2b3ec71f 100644 --- a/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js +++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItems.js @@ -21,8 +21,9 @@ class QualityProfileItems extends Component { super(props, context); this.state = { - qualitiesHeight: 0, - qualitiesHeightEditGroups: 0 + defaultHeight: 0, + editGroupsHeight: 0, + editSizesHeight: 0 }; } @@ -30,17 +31,23 @@ class QualityProfileItems extends Component { // Listeners onMeasure = ({ height }) => { - if (this.props.editGroups) { - this.setState({ - qualitiesHeightEditGroups: height - }); - } else { - this.setState({ qualitiesHeight: height }); - } + const heightKey = `${this.props.mode}Height`; + + this.setState({ + [heightKey]: height + }); }; - onToggleEditGroupsMode = () => { - this.props.onToggleEditGroupsMode(); + onEditGroupsPress = () => { + this.props.onChangeMode('editGroups'); + }; + + onEditSizesPress = () => { + this.props.onChangeMode('editSizes'); + }; + + onDefaultModePress = () => { + this.props.onChangeMode('default'); }; // @@ -48,7 +55,7 @@ class QualityProfileItems extends Component { render() { const { - editGroups, + mode, dropQualityIndex, dropPosition, qualityProfileItems, @@ -57,15 +64,10 @@ class QualityProfileItems extends Component { ...otherProps } = this.props; - const { - qualitiesHeight, - qualitiesHeightEditGroups - } = this.state; - const isDragging = dropQualityIndex !== null; const isDraggingUp = isDragging && dropPosition === 'above'; const isDraggingDown = isDragging && dropPosition === 'below'; - const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight; + const height = this.state[`${mode}Height`]; return ( @@ -107,16 +109,33 @@ class QualityProfileItems extends Component { + + @@ -128,21 +147,24 @@ class QualityProfileItems extends Component { >
{ - qualityProfileItems.map(({ id, name, allowed, quality, items }, index) => { + qualityProfileItems.map(({ id, name, allowed, quality, items, minSize, maxSize, preferredSize }, index) => { const identifier = quality ? quality.id : id; return ( 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 ( -
- ); - } - - thumbRenderer(props, state) { - return ( -
- ); - } - - // - // 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 ( -
-
- {quality.name} -
- -
- -
- -
- - -
-
- {minSixty} - } - title={translate('MinimumLimits')} - body={ - - } - position={tooltipPositions.BOTTOM} - /> -
- -
- {preferredSixty} - } - title={translate('PreferredSize')} - body={ - - } - position={tooltipPositions.BOTTOM} - /> -
- -
- {maxSixty} - } - title={translate('MaximumLimits')} - body={ - - } - position={tooltipPositions.BOTTOM} - /> -
-
-
- - { - advancedSettings && -
-
- {translate('Min')} - - -
- -
- {translate('Preferred')} - - -
- -
- {translate('Max')} - - -
-
- } -
- ); - } -} - -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; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinition.tsx b/frontend/src/Settings/Quality/Definition/QualityDefinition.tsx new file mode 100644 index 000000000..f25cf0e6c --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinition.tsx @@ -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 ( +
+
{quality.name}
+ +
+ +
+
+ ); +} + +export default QualityDefinition; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js deleted file mode 100644 index eee0558f1..000000000 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitionConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js deleted file mode 100644 index 9f738edc6..000000000 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.js +++ /dev/null @@ -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
{message}
; - } - - const thirty = formatBytes(bytes * 30); - const fortyFive = formatBytes(bytes * 45); - const sixty = formatBytes(bytes * 60); - - return ( -
-
- {translate('MinutesThirty', { thirty })} -
-
- {translate('MinutesFortyFive', { fortyFive })} -
-
- {translate('MinutesSixty', { sixty })} -
-
- ); -} - -QualityDefinitionLimits.propTypes = { - bytes: PropTypes.number, - message: PropTypes.string.isRequired -}; - -export default QualityDefinitionLimits; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.tsx b/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.tsx new file mode 100644 index 000000000..121d03d3c --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitionLimits.tsx @@ -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
{message}
; + } + + const thirty = formatBytes(bytes * 30); + const fortyFive = formatBytes(bytes * 45); + const sixty = formatBytes(bytes * 60); + + return ( +
+
{translate('MinutesThirty', { thirty })}
+
{translate('MinutesFortyFive', { fortyFive })}
+
{translate('MinutesSixty', { sixty })}
+
+ ); +} + +export default QualityDefinitionLimits; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css index da5347737..496761012 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css @@ -8,24 +8,11 @@ flex: 0 1 250px; } -.sizeLimit { - flex: 0 1 500px; -} +.notice { + composes: alert from '~Components/Alert.css'; -.megabytesPerMinute { - flex: 0 0 250px; -} - -.sizeLimitHelpTextContainer { - display: flex; - justify-content: flex-end; - margin-top: 20px; - max-width: 1000px; -} - -.sizeLimitHelpText { - max-width: 500px; - color: var(--helpTextColor); + margin: 0; + margin-bottom: 20px; } @media only screen and (max-width: $breakpointSmall) { diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css.d.ts b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css.d.ts index 72ba4f3a3..6342b91dc 100644 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitions.css.d.ts +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.css.d.ts @@ -3,11 +3,8 @@ interface CssExports { 'definitions': string; 'header': string; - 'megabytesPerMinute': string; + 'notice': string; 'quality': string; - 'sizeLimit': string; - 'sizeLimitHelpText': string; - 'sizeLimitHelpTextContainer': string; 'title': string; } export const cssExports: CssExports; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js b/frontend/src/Settings/Quality/Definition/QualityDefinitions.js deleted file mode 100644 index 76b7ca383..000000000 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitions.js +++ /dev/null @@ -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 ( -
- -
-
- {translate('Quality')} -
-
- {translate('Title')} -
-
- {translate('SizeLimit')} -
- - { - advancedSettings ? -
- {translate('MegabytesPerMinute')} -
: - null - } -
- -
- { - items.map((item) => { - return ( - - ); - }) - } -
- -
-
- {translate('QualityLimitsSeriesRuntimeHelpText')} -
-
-
-
- ); - } -} - -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; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitions.tsx b/frontend/src/Settings/Quality/Definition/QualityDefinitions.tsx new file mode 100644 index 000000000..8447b432f --- /dev/null +++ b/frontend/src/Settings/Quality/Definition/QualityDefinitions.tsx @@ -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 ( +
+ + {translate('QualityDefinitionsSizeNotice')} + + +
+
{translate('Quality')}
+ +
{translate('Title')}
+
+ +
+ {items.map((item) => { + return ; + })} +
+
+
+ ); +} + +export default QualityDefinitions; diff --git a/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js b/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js deleted file mode 100644 index 4b1fc72a6..000000000 --- a/frontend/src/Settings/Quality/Definition/QualityDefinitionsConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Settings/Quality/Quality.js b/frontend/src/Settings/Quality/Quality.js index 49c9df5d0..3d458cb1c 100644 --- a/frontend/src/Settings/Quality/Quality.js +++ b/frontend/src/Settings/Quality/Quality.js @@ -7,7 +7,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import { icons } from 'Helpers/Props'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import translate from 'Utilities/String/translate'; -import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector'; +import QualityDefinitions from './Definition/QualityDefinitions'; import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal'; class Quality extends Component { @@ -83,7 +83,7 @@ class Quality extends Component { /> - diff --git a/frontend/src/typings/QualityDefinition.ts b/frontend/src/typings/QualityDefinition.ts new file mode 100644 index 000000000..373f2b181 --- /dev/null +++ b/frontend/src/typings/QualityDefinition.ts @@ -0,0 +1,10 @@ +import Quality from 'Quality/Quality'; + +interface QualityDefinition { + id: number; + quality: Quality; + title: string; + weight: number; +} + +export default QualityDefinition; diff --git a/package.json b/package.json index c86ab70c5..59eb0b99c 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "@types/lodash": "4.14.194", "@types/react-lazyload": "3.2.0", "@types/react-router-dom": "5.3.3", + "@types/react-slider": "1.3.6", "@types/react-text-truncate": "0.14.1", "@types/react-window": "1.8.5", "@types/redux-actions": "2.6.2", diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs index b11de1c93..ec073831b 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/AcceptableSizeSpecificationFixture.cs @@ -4,8 +4,10 @@ using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; +using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -21,13 +23,49 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private RemoteEpisode _parseResultSingle; private Series _series; private List _episodes; - private QualityDefinition _qualityType; [SetUp] public void Setup() { _series = Builder.CreateNew() .With(s => s.Seasons = Builder.CreateListOfSize(2).Build().ToList()) + .With(c => c.QualityProfile = (LazyLoaded)new QualityProfile + { + Cutoff = Quality.SDTV.Id, Items = new List + { + 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 + { + new QualityProfileQualityItem + { + Quality = Quality.WEBDL720p, + MinSize = 2, + MaxSize = 20, + }, + new QualityProfileQualityItem + { + Quality = Quality.WEBRip720p, + MinSize = 2, + MaxSize = 20, + } + } + } + } + }) .Build(); _episodes = Builder.CreateListOfSize(10) @@ -79,19 +117,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .Setup(v => v.Get(It.IsAny())) .Returns(v => Quality.DefaultQualityDefinitions.First(c => c.Quality == v)); - _qualityType = Builder.CreateNew() - .With(q => q.MinSize = 2) - .With(q => q.MaxSize = 10) - .With(q => q.Quality = Quality.SDTV) - .Build(); - - Mocker.GetMock().Setup(s => s.Get(Quality.SDTV)).Returns(_qualityType); - Mocker.GetMock().Setup( s => s.GetEpisodesBySeason(It.IsAny(), It.IsAny())) .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, 250, true)] [TestCase(30, 500, false)] @@ -174,12 +210,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_size_is_zero() { + WithSize(10, 20); + _series.Runtime = 30; _parseResultSingle.Series = _series; _parseResultSingle.Release.Size = 0; _parseResultSingle.Episodes.First().Runtime = 30; - _qualityType.MinSize = 10; - _qualityType.MaxSize = 20; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -187,11 +223,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_unlimited_30_minute() { + WithSize(2, null); + _series.Runtime = 30; _parseResultSingle.Series = _series; _parseResultSingle.Release.Size = 18457280000; _parseResultSingle.Episodes.First().Runtime = 30; - _qualityType.MaxSize = null; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -199,11 +236,12 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_return_true_if_unlimited_60_minute() { + WithSize(2, null); + _series.Runtime = 60; _parseResultSingle.Series = _series; _parseResultSingle.Release.Size = 36857280000; _parseResultSingle.Episodes.First().Runtime = 60; - _qualityType.MaxSize = null; Subject.IsSatisfiedBy(_parseResultSingle, null).Accepted.Should().BeTrue(); } @@ -217,7 +255,19 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _parseResultSingle.Release.Size = 300.Megabytes(); _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(); } diff --git a/src/NzbDrone.Core/Datastore/Migration/207_add_size_to_quality_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/207_add_size_to_quality_profiles.cs new file mode 100644 index 000000000..a9a3a10fc --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/207_add_size_to_quality_profiles.cs @@ -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 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 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 _profiles; + private Dictionary _sizes; + private HashSet _changedProfiles = new HashSet(); + + 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 GetProfiles() + { + var profiles = new List(); + + 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>(profileReader.GetString(1)) + }); + } + } + } + + return profiles; + } + + private Dictionary GetSizes() + { + var sizes = new Dictionary(); + + 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; + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 26fa02c4b..831eb6e37 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -132,7 +132,10 @@ namespace NzbDrone.Core.Datastore Mapper.Entity("QualityDefinitions").RegisterModel() .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("CustomFormats").RegisterModel(); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index ce29d1919..64277b34e 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -184,15 +184,19 @@ namespace NzbDrone.Core.DecisionEngine { 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 (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 - return Math.Abs((remoteEpisode.Release.Size - preferredMovieSize).Round(200.Megabytes())) * (-1); + return Math.Abs((remoteEpisode.Release.Size - preferredEpisodeSize).Round(200.Megabytes())) * (-1); } else { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs index 04688ce14..319a52273 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/AcceptableSizeSpecification.cs @@ -3,20 +3,17 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; namespace NzbDrone.Core.DecisionEngine.Specifications { public class AcceptableSizeSpecification : IDecisionEngineSpecification { - private readonly IQualityDefinitionService _qualityDefinitionService; private readonly IEpisodeService _episodeService; private readonly Logger _logger; - public AcceptableSizeSpecification(IQualityDefinitionService qualityDefinitionService, IEpisodeService episodeService, Logger logger) + public AcceptableSizeSpecification(IEpisodeService episodeService, Logger logger) { - _qualityDefinitionService = qualityDefinitionService; _episodeService = episodeService; _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"); } - 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 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"); } else { - var maxSize = qualityDefinition.MaxSize.Value.Megabytes(); + var maxSize = item.MaxSize.Value.Megabytes(); // Multiply maxSize by runtime of all episodes maxSize *= runtime; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 73a556b40..3b7f2dd3c 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -406,6 +406,7 @@ "Donate": "Donate", "Donations": "Donations", "DoneEditingGroups": "Done Editing Groups", + "DoneEditingSizes": "Done Editing Sizes", "DotNetVersion": ".NET", "Download": "Download", "DownloadClient": "Download Client", @@ -590,6 +591,7 @@ "EditSelectedSeries": "Edit Selected Series", "EditSeries": "Edit Series", "EditSeriesModalHeader": "Edit - {title}", + "EditSizes": "Edit Sizes", "Enable": "Enable", "EnableAutomaticAdd": "Enable Automatic Add", "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", "MatchedToSeason": "Matched to Season", "MatchedToSeries": "Matched to Series", - "Max": "Max", + "Maximum": "Maximum", "MaximumLimits": "Maximum Limits", "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.", @@ -1126,7 +1128,6 @@ "MediaManagementSettingsLoadError": "Unable to load Media Management settings", "MediaManagementSettingsSummary": "Naming, file management settings and root folders", "Medium": "Medium", - "MegabytesPerMinute": "Megabytes Per Minute", "Message": "Message", "Metadata": "Metadata", "MetadataLoadError": "Unable to load Metadata", @@ -1151,7 +1152,7 @@ "MetadataXmbcSettingsSeriesMetadataHelpText": "tvshow.nfo with full series metadata", "MetadataXmbcSettingsSeriesMetadataUrlHelpText": "Include TheTVDB show URL in tvshow.nfo (can be combined with 'Series Metadata')", "MidseasonFinale": "Midseason Finale", - "Min": "Min", + "Minimum": "Minimum", "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.", "MinimumCustomFormatScore": "Minimum Custom Format Score", @@ -1159,6 +1160,8 @@ "MinimumFreeSpace": "Minimum Free Space", "MinimumFreeSpaceHelpText": "Prevent import if it would leave less than this amount of disk space available", "MinimumLimits": "Minimum Limits", + "Minute": "minute", + "MinuteShorthand": "m", "MinutesFortyFive": "45 Minutes: {fortyFive}", "MinutesSixty": "60 Minutes: {sixty}", "MinutesThirty": "30 Minutes: {thirty}", @@ -1564,7 +1567,7 @@ "QualityCutoffNotMet": "Quality cutoff has not been met", "QualityDefinitions": "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", "QualityProfileInUseSeriesListCollection": "Can't delete a quality profile that is attached to a series, list, or collection", "QualityProfiles": "Quality Profiles", diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs index 4fe8dec67..9f6a19b4b 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileQualityItem.cs @@ -16,6 +16,9 @@ namespace NzbDrone.Core.Profiles.Qualities public Quality Quality { get; set; } public List Items { get; set; } public bool Allowed { get; set; } + public double? MinSize { get; set; } + public double? MaxSize { get; set; } + public double? PreferredSize { get; set; } public QualityProfileQualityItem() { diff --git a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs index 67b7dc18f..9777fcc00 100644 --- a/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs +++ b/src/NzbDrone.Core/Profiles/Qualities/QualityProfileService.cs @@ -193,7 +193,14 @@ namespace NzbDrone.Core.Profiles.Qualities { 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; } @@ -206,7 +213,10 @@ namespace NzbDrone.Core.Profiles.Qualities Items = group.Select(g => new QualityProfileQualityItem { Quality = g.Quality, - Allowed = groupAllowed + Allowed = groupAllowed, + MinSize = g.MinSize, + MaxSize = g.MaxSize, + PreferredSize = g.PreferredSize }).ToList(), Allowed = groupAllowed }); diff --git a/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs b/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs index d588ef822..cdab1ae56 100644 --- a/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs +++ b/src/NzbDrone.Core/Qualities/Commands/ResetQualityDefinitionsCommand.cs @@ -1,4 +1,4 @@ -using NzbDrone.Core.Messaging.Commands; +using NzbDrone.Core.Messaging.Commands; namespace NzbDrone.Core.Qualities.Commands { diff --git a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs index 0b5096ff9..da8654aec 100644 --- a/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs +++ b/src/NzbDrone.Core/Qualities/QualityDefinitionService.cs @@ -47,6 +47,7 @@ namespace NzbDrone.Core.Qualities public void UpdateMany(List qualityDefinitions) { _repo.UpdateMany(qualityDefinitions); + _cache.Clear(); } public List All() @@ -119,9 +120,6 @@ namespace NzbDrone.Core.Qualities { 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; updateList.Add(existing); diff --git a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs index 8f7fef948..60fb09f75 100644 --- a/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Quality/QualityProfileResource.cs @@ -24,6 +24,9 @@ namespace Sonarr.Api.V3.Profiles.Quality public NzbDrone.Core.Qualities.Quality Quality { get; set; } public List Items { get; set; } public bool Allowed { get; set; } + public double? MinSize { get; set; } + public double? MaxSize { get; set; } + public double? PreferredSize { get; set; } public QualityProfileQualityItemResource() { @@ -73,7 +76,10 @@ namespace Sonarr.Api.V3.Profiles.Quality Name = model.Name, Quality = model.Quality, 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, Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null, Items = resource.Items.ConvertAll(ToModel), - Allowed = resource.Allowed + Allowed = resource.Allowed, + MinSize = resource.MinSize, + MaxSize = resource.MaxSize, + PreferredSize = resource.PreferredSize }; } diff --git a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs index abde0ce46..05c36766f 100644 --- a/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs +++ b/src/Sonarr.Api.V3/Qualities/QualityDefinitionResource.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Qualities; using Sonarr.Http.REST; @@ -10,12 +10,6 @@ namespace Sonarr.Api.V3.Qualities public Quality Quality { 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 @@ -31,11 +25,7 @@ namespace Sonarr.Api.V3.Qualities { Id = model.Id, Quality = model.Quality, - Title = model.Title, - Weight = model.Weight, - MinSize = model.MinSize, - MaxSize = model.MaxSize, - PreferredSize = model.PreferredSize + Title = model.Title }; } @@ -50,11 +40,7 @@ namespace Sonarr.Api.V3.Qualities { Id = resource.Id, Quality = resource.Quality, - Title = resource.Title, - Weight = resource.Weight, - MinSize = resource.MinSize, - MaxSize = resource.MaxSize, - PreferredSize = resource.PreferredSize + Title = resource.Title }; } diff --git a/yarn.lock b/yarn.lock index 5cf57bfc8..66c7a344a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1499,6 +1499,13 @@ "@types/history" "^4.7.11" "@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": version "0.14.1" resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"