mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-25 22:46:31 -04:00
v3 UI
This commit is contained in:
committed by
Taloth Saldono
parent
99feff549d
commit
5894b4fd95
@@ -0,0 +1,93 @@
|
||||
.qualityDefinition {
|
||||
display: flex;
|
||||
align-content: stretch;
|
||||
margin: 5px 0;
|
||||
padding-top: 5px;
|
||||
height: 45px;
|
||||
border-top: 1px solid $borderColor;
|
||||
}
|
||||
|
||||
.quality,
|
||||
.title {
|
||||
flex: 0 1 250px;
|
||||
padding-right: 20px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.slider {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.bar {
|
||||
top: 9px;
|
||||
margin: 0 5px;
|
||||
height: 3px;
|
||||
background-color: $sliderAccentColor;
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.handle {
|
||||
top: 1px;
|
||||
z-index: 0 !important;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 3px solid $sliderAccentColor;
|
||||
border-radius: 50%;
|
||||
background-color: $white;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sizes {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.megabytesPerMinute {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 250px;
|
||||
}
|
||||
|
||||
.sizeInput {
|
||||
composes: input from 'Components/Form/TextInput.css';
|
||||
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
padding: 6px;
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.qualityDefinition {
|
||||
flex-wrap: wrap;
|
||||
height: auto;
|
||||
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.qualityDefinition:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.quality {
|
||||
font-weight: bold;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactSlider from 'react-slider';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Label from 'Components/Label';
|
||||
import NumberInput from 'Components/Form/NumberInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import styles from './QualityDefinition.css';
|
||||
|
||||
const slider = {
|
||||
min: 0,
|
||||
max: 200,
|
||||
step: 0.1
|
||||
};
|
||||
|
||||
function getValue(value) {
|
||||
if (value < slider.min) {
|
||||
return slider.min;
|
||||
}
|
||||
|
||||
if (value > slider.max) {
|
||||
return slider.max;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
class QualityDefinition extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._forceUpdateTimeout = null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSizeChange = ([minSize, maxSize]) => {
|
||||
maxSize = maxSize === slider.max ? null : maxSize;
|
||||
|
||||
this.props.onSizeChange({ minSize, maxSize });
|
||||
}
|
||||
|
||||
onMinSizeChange = ({ value }) => {
|
||||
const minSize = getValue(value);
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize,
|
||||
maxSize: this.props.maxSize
|
||||
});
|
||||
}
|
||||
|
||||
onMaxSizeChange = ({ value }) => {
|
||||
const maxSize = value === slider.max ? null : getValue(value);
|
||||
|
||||
this.props.onSizeChange({
|
||||
minSize: this.props.minSize,
|
||||
maxSize
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
quality,
|
||||
title,
|
||||
minSize,
|
||||
maxSize,
|
||||
advancedSettings,
|
||||
onTitleChange
|
||||
} = this.props;
|
||||
|
||||
const minBytes = minSize * 1024 * 1024;
|
||||
const minThirty = formatBytes(minBytes * 30, 2);
|
||||
const minSixty = formatBytes(minBytes * 60, 2);
|
||||
|
||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
||||
const maxThirty = maxBytes ? formatBytes(maxBytes * 30, 2) : 'Unlimited';
|
||||
const maxSixty = maxBytes ? formatBytes(maxBytes * 60, 2) : '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
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
minDistance={10}
|
||||
value={[minSize || slider.min, maxSize || slider.max]}
|
||||
withBars={true}
|
||||
snapDragDisabled={true}
|
||||
className={styles.slider}
|
||||
barClassName={styles.bar}
|
||||
handleClassName={styles.handle}
|
||||
onChange={this.onSizeChange}
|
||||
/>
|
||||
|
||||
<div className={styles.sizes}>
|
||||
<div>
|
||||
<Label kind={kinds.WARNING}>{minThirty}</Label>
|
||||
<Label kind={kinds.INFO}>{minSixty}</Label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label kind={kinds.WARNING}>{maxThirty}</Label>
|
||||
<Label kind={kinds.INFO}>{maxSixty}</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
advancedSettings &&
|
||||
<div className={styles.megabytesPerMinute}>
|
||||
<div>
|
||||
Min
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={minSize || slider.min}
|
||||
min={slider.min}
|
||||
max={maxSize ? maxSize - 10 : slider.max - 10}
|
||||
isFloat={true}
|
||||
onChange={this.onMinSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Max
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.min`}
|
||||
value={maxSize || slider.max}
|
||||
min={minSize + 10}
|
||||
max={slider.max}
|
||||
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,
|
||||
advancedSettings: PropTypes.bool.isRequired,
|
||||
onTitleChange: PropTypes.func.isRequired,
|
||||
onSizeChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QualityDefinition;
|
||||
@@ -0,0 +1,70 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import QualityDefinition from './QualityDefinition';
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
advancedSettings: state.settings.advancedSettings
|
||||
};
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
const {
|
||||
id,
|
||||
minSize: currentMinSize,
|
||||
maxSize: currentMaxSize
|
||||
} = this.props;
|
||||
|
||||
if (minSize !== currentMinSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
|
||||
}
|
||||
|
||||
if (minSize !== currentMaxSize) {
|
||||
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QualityDefinition
|
||||
{...this.props}
|
||||
onTitleChange={this.onTitleChange}
|
||||
onSizeChange={this.onSizeChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinitionConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
minSize: PropTypes.number,
|
||||
maxSize: PropTypes.number,
|
||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(QualityDefinitionConnector);
|
||||
@@ -0,0 +1,41 @@
|
||||
.header {
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.quality,
|
||||
.title {
|
||||
flex: 0 1 250px;
|
||||
}
|
||||
|
||||
.sizeLimit {
|
||||
flex: 0 1 500px;
|
||||
}
|
||||
|
||||
.megabytesPerMinute {
|
||||
flex: 0 0 250px;
|
||||
}
|
||||
|
||||
.sizeLimitHelpTextContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
max-width: 1000px;
|
||||
}
|
||||
|
||||
.sizeLimitHelpText {
|
||||
max-width: 500px;
|
||||
color: $helpTextColor;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.definitions {
|
||||
&:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import QualityDefinitionConnector from './QualityDefinitionConnector';
|
||||
import styles from './QualityDefinitions.css';
|
||||
|
||||
class QualityDefinitions extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Quality Definitions">
|
||||
<PageSectionContent
|
||||
errorMessage="Unable to load Quality Definitions"
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.quality}>Quality</div>
|
||||
<div className={styles.title}>Title</div>
|
||||
<div className={styles.sizeLimit}>Size Limit</div>
|
||||
<div className={styles.megabytesPerMinute}>Megabytes Per Minute</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.definitions}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QualityDefinitionConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.sizeLimitHelpTextContainer}>
|
||||
<div className={styles.sizeLimitHelpText}>
|
||||
Limits are automatically adjusted for the series runtime and number of episodes in the file.
|
||||
</div>
|
||||
</div>
|
||||
</PageSectionContent>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinitions.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
defaultProfile: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default QualityDefinitions;
|
||||
@@ -0,0 +1,90 @@
|
||||
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,
|
||||
(qualityDefinitions) => {
|
||||
const items = qualityDefinitions.items.map((item) => {
|
||||
const pendingChanges = qualityDefinitions.pendingChanges[item.id] || {};
|
||||
|
||||
return Object.assign({}, item, pendingChanges);
|
||||
});
|
||||
|
||||
return {
|
||||
...qualityDefinitions,
|
||||
items,
|
||||
hasPendingChanges: !_.isEmpty(qualityDefinitions.pendingChanges)
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchQualityDefinitions: fetchQualityDefinitions,
|
||||
dispatchSaveQualityDefinitions: saveQualityDefinitions
|
||||
};
|
||||
|
||||
class QualityDefinitionsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchQualityDefinitions();
|
||||
|
||||
const {
|
||||
dispatchFetchQualityDefinitions,
|
||||
dispatchSaveQualityDefinitions,
|
||||
onChildMounted
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchQualityDefinitions();
|
||||
onChildMounted(dispatchSaveQualityDefinitions);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
hasPendingChanges,
|
||||
isSaving,
|
||||
onChildStateChange
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.isSaving !== isSaving ||
|
||||
prevProps.hasPendingChanges !== hasPendingChanges
|
||||
) {
|
||||
onChildStateChange({
|
||||
isSaving,
|
||||
hasPendingChanges
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QualityDefinitions
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QualityDefinitionsConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
hasPendingChanges: PropTypes.bool.isRequired,
|
||||
dispatchFetchQualityDefinitions: PropTypes.func.isRequired,
|
||||
dispatchSaveQualityDefinitions: PropTypes.func.isRequired,
|
||||
onChildMounted: PropTypes.func.isRequired,
|
||||
onChildStateChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps, null, { withRef: true })(QualityDefinitionsConnector);
|
||||
Reference in New Issue
Block a user