parent
3eb02e0d6e
commit
46e15849b2
@ -0,0 +1,60 @@
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.track {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: var(--sliderAccentColor);
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(3n + 1) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid var(--sliderAccentColor);
|
||||
border-radius: 50%;
|
||||
background-color: var(--white);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.megabytesPerMinuteContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 400px;
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid var(--borderColor);
|
||||
}
|
||||
|
||||
.megabytesPerMinute {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sizeInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
margin-bottom: 2px;
|
||||
padding: 6px;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'megabytesPerMinute': string;
|
||||
'megabytesPerMinuteContainer': string;
|
||||
'sizeInput': string;
|
||||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,295 @@
|
||||
import React, { HTMLProps, useCallback, useState } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import Label from 'Components/Label';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import QualityDefinitionLimits from 'Settings/Quality/Definition/QualityDefinitionLimits';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import roundNumber from 'Utilities/Number/roundNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QualityProfileItemSize.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 400;
|
||||
const STEP_SIZE = 0.1;
|
||||
const MIN_DISTANCE = 3;
|
||||
const SLIDER_MAX = roundNumber(Math.pow(MAX, 1 / 1.1));
|
||||
|
||||
interface SizeProps {
|
||||
minSize: number | null;
|
||||
preferredSize: number | null;
|
||||
maxSize: number | null;
|
||||
}
|
||||
|
||||
export interface OnSizeChangeArguments extends SizeProps {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface QualityProfileItemSizeProps extends OnSizeChangeArguments {
|
||||
onSizeChange: (props: OnSizeChangeArguments) => void;
|
||||
}
|
||||
|
||||
function trackRenderer(props: HTMLProps<HTMLDivElement>) {
|
||||
return <div {...props} className={styles.track} />;
|
||||
}
|
||||
|
||||
function thumbRenderer(props: HTMLProps<HTMLDivElement>) {
|
||||
return <div {...props} className={styles.thumb} />;
|
||||
}
|
||||
|
||||
function getSliderValue(value: number | null, defaultValue: number): number {
|
||||
const sliderValue = value ? Math.pow(value, 1 / 1.1) : defaultValue;
|
||||
|
||||
return roundNumber(sliderValue);
|
||||
}
|
||||
|
||||
export default function QualityProfileItemSize(
|
||||
props: QualityProfileItemSizeProps
|
||||
) {
|
||||
const { id, minSize, maxSize, preferredSize, onSizeChange } = props;
|
||||
const [sizes, setSizes] = useState<SizeProps>({
|
||||
minSize: getSliderValue(minSize, MIN),
|
||||
preferredSize: getSliderValue(preferredSize, SLIDER_MAX - MIN_DISTANCE),
|
||||
maxSize: getSliderValue(maxSize, SLIDER_MAX),
|
||||
});
|
||||
|
||||
const handleSliderChange = useCallback(
|
||||
([sliderMinSize, sliderPreferredSize, sliderMaxSize]: [
|
||||
number,
|
||||
number,
|
||||
number
|
||||
]) => {
|
||||
// console.log('Sizes:', sliderMinSize, sliderPreferredSize, sliderMaxSize);
|
||||
console.log(
|
||||
'Min Sizes: ',
|
||||
sliderMinSize,
|
||||
roundNumber(Math.pow(sliderMinSize, 1.1))
|
||||
);
|
||||
|
||||
setSizes({
|
||||
minSize: sliderMinSize,
|
||||
preferredSize: sliderPreferredSize,
|
||||
maxSize: sliderMaxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||
preferredSize:
|
||||
sliderPreferredSize === MAX - MIN_DISTANCE
|
||||
? null
|
||||
: roundNumber(Math.pow(sliderPreferredSize, 1.1)),
|
||||
maxSize:
|
||||
sliderMaxSize === MAX
|
||||
? null
|
||||
: roundNumber(Math.pow(sliderMaxSize, 1.1)),
|
||||
});
|
||||
},
|
||||
[id, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleMinSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: value,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: value,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handlePreferredSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: value,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: value,
|
||||
maxSize: sizes.maxSize,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleMaxSizeChange = useCallback(
|
||||
({ value }: InputChanged<number>) => {
|
||||
setSizes({
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: value,
|
||||
});
|
||||
|
||||
onSizeChange({
|
||||
id,
|
||||
minSize: sizes.minSize,
|
||||
preferredSize: sizes.preferredSize,
|
||||
maxSize: value,
|
||||
});
|
||||
},
|
||||
[id, sizes, setSizes, onSizeChange]
|
||||
);
|
||||
|
||||
const handleAfterSliderChange = useCallback(() => {
|
||||
setSizes({
|
||||
minSize: getSliderValue(minSize, MIN),
|
||||
maxSize: getSliderValue(maxSize, MAX),
|
||||
preferredSize: getSliderValue(preferredSize, MAX - MIN_DISTANCE),
|
||||
});
|
||||
}, [minSize, maxSize, preferredSize, setSizes]);
|
||||
|
||||
const minBytes = (sizes.minSize || 0) * 1024 * 1024;
|
||||
const minSixty = `${formatBytes(minBytes * 60)}/${translate(
|
||||
'HourShorthand'
|
||||
)}`;
|
||||
|
||||
const preferredBytes = (sizes.preferredSize || 0) * 1024 * 1024;
|
||||
const preferredSixty = preferredBytes
|
||||
? `${formatBytes(preferredBytes * 60)}/${translate('HourShorthand')}`
|
||||
: translate('Unlimited');
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxSixty = maxBytes
|
||||
? `${formatBytes(maxBytes * 60)}/${translate('HourShorthand')}`
|
||||
: translate('Unlimited');
|
||||
|
||||
return (
|
||||
<div className={styles.sizeLimit}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
|
||||
{/* @ts-ignore React version mismatch */}
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={MIN}
|
||||
max={SLIDER_MAX}
|
||||
step={STEP_SIZE}
|
||||
minDistance={3}
|
||||
value={[sizes.minSize, sizes.preferredSize, sizes.maxSize]}
|
||||
withTracks={true}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore allowCross is still available in the version currently used
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
renderThumb={thumbRenderer}
|
||||
renderTrack={trackRenderer}
|
||||
onChange={handleSliderChange}
|
||||
onAfterChange={handleAfterSliderChange}
|
||||
/>
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.INFO}>{minSixty}</Label>}
|
||||
title={translate('MinimumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={minBytes}
|
||||
message={translate('NoMinimumForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.SUCCESS}>{preferredSixty}</Label>}
|
||||
title={translate('PreferredSize')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={preferredBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={<Label kind={kinds.WARNING}>{maxSixty}</Label>}
|
||||
title={translate('MaximumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={maxBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinuteContainer}>
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={minSize || MIN}
|
||||
min={MIN}
|
||||
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handleMinSizeChange}
|
||||
/>
|
||||
<Label kind={kinds.INFO}>
|
||||
{translate('Minimum')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={preferredSize || MAX - 5}
|
||||
min={MIN}
|
||||
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handlePreferredSizeChange}
|
||||
/>
|
||||
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
{translate('Preferred')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.max`}
|
||||
value={maxSize || MAX}
|
||||
min={(preferredSize || 0) + STEP_SIZE}
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
// @ts-expect-error - Typngs are too loose
|
||||
onChange={handleMaxSizeChange}
|
||||
/>
|
||||
|
||||
<Label kind={kinds.WARNING}>
|
||||
{translate('Maximum')} MiB/
|
||||
{translate('MinuteShorthand')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,16 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'megabytesPerMinute': string;
|
||||
'quality': string;
|
||||
'qualityDefinition': string;
|
||||
'sizeInput': string;
|
||||
'sizeLimit': string;
|
||||
'sizes': string;
|
||||
'slider': string;
|
||||
'thumb': string;
|
||||
'title': string;
|
||||
'track': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
@ -1,346 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Label from 'Components/Label';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import roundNumber from 'Utilities/Number/roundNumber';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityDefinitionLimits from './QualityDefinitionLimits';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 1000;
|
||||
const MIN_DISTANCE = 1;
|
||||
|
||||
const slider = {
|
||||
min: MIN,
|
||||
max: roundNumber(Math.pow(MAX, 1 / 1.1)),
|
||||
step: 0.1
|
||||
};
|
||||
|
||||
function getValue(inputValue) {
|
||||
if (inputValue < MIN) {
|
||||
return MIN;
|
||||
}
|
||||
|
||||
if (inputValue > MAX) {
|
||||
return MAX;
|
||||
}
|
||||
|
||||
return roundNumber(inputValue);
|
||||
}
|
||||
|
||||
function getSliderValue(value, defaultValue) {
|
||||
const sliderValue = value ? Math.pow(value, 1 / 1.1) : defaultValue;
|
||||
|
||||
return roundNumber(sliderValue);
|
||||
}
|
||||
|
||||
class QualityDefinition extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._forceUpdateTimeout = null;
|
||||
|
||||
this.state = {
|
||||
sliderMinSize: getSliderValue(props.minSize, slider.min),
|
||||
sliderMaxSize: getSliderValue(props.maxSize, slider.max),
|
||||
sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3))
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// A hack to deal with a bug in the slider component until a fix for it
|
||||
// lands and an updated version is available.
|
||||
// See: https://github.com/mpowaga/react-slider/issues/115
|
||||
|
||||
this._forceUpdateTimeout = setTimeout(() => this.forceUpdate(), 1);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._forceUpdateTimeout) {
|
||||
clearTimeout(this._forceUpdateTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
trackRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.track}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
thumbRenderer(props, state) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={styles.thumb}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSliderChange = ([sliderMinSize, sliderPreferredSize, sliderMaxSize]) => {
|
||||
this.setState({
|
||||
sliderMinSize,
|
||||
sliderMaxSize,
|
||||
sliderPreferredSize
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: roundNumber(Math.pow(sliderMinSize, 1.1)),
|
||||
preferredSize: sliderPreferredSize === (slider.max - 3) ? null : roundNumber(Math.pow(sliderPreferredSize, 1.1)),
|
||||
maxSize: sliderMaxSize === slider.max ? null : roundNumber(Math.pow(sliderMaxSize, 1.1))
|
||||
});
|
||||
};
|
||||
|
||||
onAfterSliderChange = () => {
|
||||
const {
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize
|
||||
} = this.props;
|
||||
|
||||
this.setState({
|
||||
sliderMiSize: getSliderValue(minSize, slider.min),
|
||||
sliderMaxSize: getSliderValue(maxSize, slider.max),
|
||||
sliderPreferredSize: getSliderValue(preferredSize, (slider.max - 3)) // fix
|
||||
});
|
||||
};
|
||||
|
||||
onMinSizeChange = ({ value }) => {
|
||||
const minSize = getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderMinSize: getSliderValue(minSize, slider.min)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize,
|
||||
maxSize: this.props.maxSize,
|
||||
preferredSize: this.props.preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
onPreferredSizeChange = ({ value }) => {
|
||||
const preferredSize = value === (MAX - 3) ? null : getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderPreferredSize: getSliderValue(preferredSize, slider.preferred)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: this.props.minSize,
|
||||
maxSize: this.props.maxSize,
|
||||
preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
onMaxSizeChange = ({ value }) => {
|
||||
const maxSize = value === MAX ? null : getValue(value);
|
||||
|
||||
this.setState({
|
||||
sliderMaxSize: getSliderValue(maxSize, slider.max)
|
||||
});
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: this.props.minSize,
|
||||
maxSize,
|
||||
preferredSize: this.props.preferredSize
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
quality,
|
||||
title,
|
||||
minSize,
|
||||
maxSize,
|
||||
preferredSize,
|
||||
advancedSettings,
|
||||
onTitleChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sliderMinSize,
|
||||
sliderMaxSize,
|
||||
sliderPreferredSize
|
||||
} = this.state;
|
||||
|
||||
const minBytes = minSize * 1024 * 1024;
|
||||
const minSixty = `${formatBytes(minBytes * 60)}/${translate('HourShorthand')}`;
|
||||
|
||||
const preferredBytes = preferredSize * 1024 * 1024;
|
||||
const preferredSixty = preferredBytes ? `${formatBytes(preferredBytes * 60)}/${translate('HourShorthand')}` : translate('Unlimited');
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/${translate('HourShorthand')}` : translate('Unlimited');
|
||||
|
||||
return (
|
||||
<div className={styles.qualityDefinition}>
|
||||
<div className={styles.quality}>
|
||||
{quality.name}
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<TextInput
|
||||
name={`${id}.${title}`}
|
||||
value={title}
|
||||
onChange={onTitleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.sizeLimit}>
|
||||
<ReactSlider
|
||||
className={styles.slider}
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
minDistance={3}
|
||||
value={[sliderMinSize, sliderPreferredSize, sliderMaxSize]}
|
||||
withTracks={true}
|
||||
allowCross={false}
|
||||
snapDragDisabled={true}
|
||||
renderThumb={this.thumbRenderer}
|
||||
renderTrack={this.trackRenderer}
|
||||
onChange={this.onSliderChange}
|
||||
onAfterChange={this.onAfterSliderChange}
|
||||
/>
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.INFO}>{minSixty}</Label>
|
||||
}
|
||||
title={translate('MinimumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={minBytes}
|
||||
message={translate('NoMinimumForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.SUCCESS}>{preferredSixty}</Label>
|
||||
}
|
||||
title={translate('PreferredSize')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={preferredBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Popover
|
||||
anchor={
|
||||
<Label kind={kinds.WARNING}>{maxSixty}</Label>
|
||||
}
|
||||
title={translate('MaximumLimits')}
|
||||
body={
|
||||
<QualityDefinitionLimits
|
||||
bytes={maxBytes}
|
||||
message={translate('NoLimitForAnyRuntime')}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
advancedSettings &&
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<div>
|
||||
{translate('Min')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={minSize || MIN}
|
||||
min={MIN}
|
||||
max={preferredSize ? preferredSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onMinSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{translate('Preferred')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={preferredSize || MAX - 5}
|
||||
min={MIN}
|
||||
max={maxSize ? maxSize - 5 : MAX - 5}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onPreferredSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{translate('Max')}
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.max`}
|
||||
value={maxSize || MAX}
|
||||
min={minSize + MIN_DISTANCE}
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onMaxSizeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinition.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSizeChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityDefinition;
|
@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Quality from 'Quality/Quality';
|
||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
interface QualityDefinitionProps {
|
||||
id: number;
|
||||
quality: Quality;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function QualityDefinition(props: QualityDefinitionProps) {
|
||||
const { id, quality, title } = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleTitleChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
dispatch(
|
||||
setQualityDefinitionValue({
|
||||
id,
|
||||
name: 'title',
|
||||
value,
|
||||
})
|
||||
);
|
||||
},
|
||||
[id, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.qualityDefinition}>
|
||||
<div className={styles.quality}>{quality.name}</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
<TextInput
|
||||
name={`${id}.${title}`}
|
||||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QualityDefinition;
|
@ -1,71 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
||||
import QualityDefinition from './QualityDefinition';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setQualityDefinitionValue,
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class QualityDefinitionConnector extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearPendingChanges({ section: 'settings.qualityDefinitions' });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTitleChange = ({ value }) => {
|
||||
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
|
||||
};
|
||||
|
||||
onSizeChange = ({ minSize, maxSize, preferredSize }) => {
|
||||
const {
|
||||
id,
|
||||
minSize: currentMinSize,
|
||||
maxSize: currentMaxSize,
|
||||
preferredSize: currentPreferredSize
|
||||
} = this.props;
|
||||
|
||||
if (minSize !== currentMinSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
|
||||
}
|
||||
|
||||
if (maxSize !== currentMaxSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
||||
}
|
||||
|
||||
if (preferredSize !== currentPreferredSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QualityDefinition
|
||||
{...this.props}
|
||||
onTitleChange={this.onTitleChange}
|
||||
onSizeChange={this.onSizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinitionConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
preferredSize: PropTypes.number,
|
||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, mapDispatchToProps)(QualityDefinitionConnector);
|
@ -0,0 +1,105 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QualityDefinitions from './Definition/QualityDefinitions';
|
||||
import ResetQualityDefinitionsModal from './Reset/ResetQualityDefinitionsModal';
|
||||
|
||||
class Quality extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._saveCallback = null;
|
||||
|
||||
this.state = {
|
||||
isSaving: false,
|
||||
hasPendingChanges: false,
|
||||
isConfirmQualityDefinitionResetModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChildMounted = (saveCallback) => {
|
||||
this._saveCallback = saveCallback;
|
||||
};
|
||||
|
||||
onChildStateChange = (payload) => {
|
||||
this.setState(payload);
|
||||
};
|
||||
|
||||
onResetQualityDefinitionsPress = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: true });
|
||||
};
|
||||
|
||||
onCloseResetQualityDefinitionsModal = () => {
|
||||
this.setState({ isConfirmQualityDefinitionResetModalOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
if (this._saveCallback) {
|
||||
this._saveCallback();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSaving,
|
||||
isResettingQualityDefinitions,
|
||||
hasPendingChanges
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('QualitySettings')}>
|
||||
<SettingsToolbarConnector
|
||||
isSaving={isSaving}
|
||||
hasPendingChanges={hasPendingChanges}
|
||||
additionalButtons={
|
||||
<Fragment>
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ResetDefinitions')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isResettingQualityDefinitions}
|
||||
onPress={this.onResetQualityDefinitionsPress}
|
||||
/>
|
||||
</Fragment>
|
||||
}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
|
||||
<PageContentBody>
|
||||
<QualityDefinitions
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
<ResetQualityDefinitionsModal
|
||||
isOpen={this.state.isConfirmQualityDefinitionResetModalOpen}
|
||||
onModalClose={this.onCloseResetQualityDefinitionsModal}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Quality.propTypes = {
|
||||
isResettingQualityDefinitions: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default Quality;
|
@ -0,0 +1,10 @@
|
||||
import Quality from 'Quality/Quality';
|
||||
|
||||
interface QualityDefinition {
|
||||
id: number;
|
||||
quality: Quality;
|
||||
title: string;
|
||||
weight: number;
|
||||
}
|
||||
|
||||
export default QualityDefinition;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Profiles.Qualities;
|
||||
|
||||
public class QualityProfileSizeLimit
|
||||
{
|
||||
public Quality Quality { get; set; }
|
||||
public double? MinSize { get; set; }
|
||||
public double? MaxSize { get; set; }
|
||||
public double? PreferredSize { get; set; }
|
||||
|
||||
public QualityProfileSizeLimit(QualityDefinition qualityDefinition)
|
||||
{
|
||||
Quality = qualityDefinition.Quality;
|
||||
MinSize = qualityDefinition.MinSize;
|
||||
MaxSize = qualityDefinition.MaxSize;
|
||||
PreferredSize = qualityDefinition.PreferredSize;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue