parent
6dc475cf53
commit
2b6b17707d
@ -1,5 +0,0 @@
|
|||||||
nzbdrone {version} {branch}; urgency=low
|
|
||||||
|
|
||||||
* Automatic Release.
|
|
||||||
|
|
||||||
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700
|
|
@ -1 +0,0 @@
|
|||||||
8
|
|
@ -1,12 +0,0 @@
|
|||||||
Section: web
|
|
||||||
Priority: optional
|
|
||||||
Maintainer: Sonarr <contact@nzbdrone.com>
|
|
||||||
Source: nzbdrone
|
|
||||||
Homepage: https://sonarr.tv
|
|
||||||
Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
|
||||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
|
||||||
|
|
||||||
Package: nzbdrone
|
|
||||||
Architecture: all
|
|
||||||
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
|
||||||
Description: Sonarr is an internet PVR
|
|
@ -1,24 +0,0 @@
|
|||||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
|
||||||
Upstream-Name: nzbdrone
|
|
||||||
Source: https://github.com/Sonarr/Sonarr
|
|
||||||
|
|
||||||
Files: *
|
|
||||||
Copyright: 2010-2016 Sonarr <hello@sonarr.tv>
|
|
||||||
|
|
||||||
License: GPL-3.0+
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
.
|
|
||||||
This package is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU General Public License for more details.
|
|
||||||
.
|
|
||||||
You should have received a copy of the GNU General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
.
|
|
||||||
On Debian systems, the complete text of the GNU General
|
|
||||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
|
@ -1 +0,0 @@
|
|||||||
nzbdrone_bin/* opt/NzbDrone
|
|
@ -1,13 +0,0 @@
|
|||||||
#!/usr/bin/make -f
|
|
||||||
# -*- makefile -*-
|
|
||||||
# Sample debian/rules that uses debhelper.
|
|
||||||
# This file was originally written by Joey Hess and Craig Small.
|
|
||||||
# As a special exception, when this file is copied by dh-make into a
|
|
||||||
# dh-make output file, you may use that output file without restriction.
|
|
||||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
|
||||||
#export DH_VERBOSE=1
|
|
||||||
|
|
||||||
%:
|
|
||||||
dh $@
|
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@ -1,21 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
const protocols = [
|
|
||||||
{ id: 'tba', name: 'TBA' },
|
|
||||||
{ id: 'announced', name: 'Announced' },
|
|
||||||
{ id: 'inCinemas', name: 'In Cinemas' },
|
|
||||||
{ id: 'released', name: 'Released' },
|
|
||||||
{ id: 'deleted', name: 'Deleted' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function MovieStatusFilterBuilderRowValue(props) {
|
|
||||||
return (
|
|
||||||
<FilterBuilderRowValue
|
|
||||||
tagList={protocols}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MovieStatusFilterBuilderRowValue;
|
|
@ -1,4 +0,0 @@
|
|||||||
.heart {
|
|
||||||
margin-right: 5px;
|
|
||||||
color: $themeRed;
|
|
||||||
}
|
|
@ -1,34 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './HeartRating.css';
|
|
||||||
|
|
||||||
function HeartRating({ rating, iconSize, hideHeart }) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
!hideHeart &&
|
|
||||||
<Icon
|
|
||||||
className={styles.heart}
|
|
||||||
name={icons.HEART}
|
|
||||||
size={iconSize}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{rating * 10}%
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HeartRating.propTypes = {
|
|
||||||
rating: PropTypes.number.isRequired,
|
|
||||||
iconSize: PropTypes.number.isRequired,
|
|
||||||
hideHeart: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
HeartRating.defaultProps = {
|
|
||||||
iconSize: 14
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HeartRating;
|
|
@ -1,3 +0,0 @@
|
|||||||
.lists {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Label from './Label';
|
|
||||||
import styles from './ImportListList.css';
|
|
||||||
|
|
||||||
function ImportListList({ lists, importListList }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.lists}>
|
|
||||||
{
|
|
||||||
lists.map((t) => {
|
|
||||||
const list = _.find(importListList, { id: t });
|
|
||||||
|
|
||||||
if (!list) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={list.id}
|
|
||||||
kind={kinds.INFO}
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
>
|
|
||||||
{list.name}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportListList.propTypes = {
|
|
||||||
lists: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
importListList: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ImportListList.defaultProps = {
|
|
||||||
lists: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ImportListList;
|
|
@ -1,17 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createImportListSelector from 'Store/Selectors/createImportListSelector';
|
|
||||||
import ImportListList from './ImportListList';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createImportListSelector(),
|
|
||||||
(importListList) => {
|
|
||||||
return {
|
|
||||||
importListList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ImportListList);
|
|
@ -1,181 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
const FPS = 20;
|
|
||||||
const STEP = 1;
|
|
||||||
const TIMEOUT = 1 / FPS * 1000;
|
|
||||||
|
|
||||||
class Marquee extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
text: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
hoverToStop: PropTypes.bool,
|
|
||||||
loop: PropTypes.bool,
|
|
||||||
className: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
|
||||||
text: '',
|
|
||||||
title: '',
|
|
||||||
hoverToStop: true,
|
|
||||||
loop: false
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
animatedWidth: 0,
|
|
||||||
overflowWidth: 0,
|
|
||||||
direction: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.measureText();
|
|
||||||
|
|
||||||
if (this.props.hoverToStop) {
|
|
||||||
this.startAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillReceiveProps(nextProps) {
|
|
||||||
if (this.props.text.length !== nextProps.text.length) {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
this.setState({ animatedWidth: 0, direction: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.measureText();
|
|
||||||
|
|
||||||
if (this.props.hoverToStop) {
|
|
||||||
this.startAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
onHandleMouseEnter = () => {
|
|
||||||
if (this.props.hoverToStop) {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
} else if (this.state.overflowWidth > 0) {
|
|
||||||
this.startAnimation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onHandleMouseLeave = () => {
|
|
||||||
if (this.props.hoverToStop && this.state.overflowWidth > 0) {
|
|
||||||
this.startAnimation();
|
|
||||||
} else {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
this.setState({ animatedWidth: 0 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startAnimation = () => {
|
|
||||||
clearTimeout(this.marqueeTimer);
|
|
||||||
const isLeading = this.state.animatedWidth === 0;
|
|
||||||
const timeout = isLeading ? 0 : TIMEOUT;
|
|
||||||
|
|
||||||
const animate = () => {
|
|
||||||
const { overflowWidth } = this.state;
|
|
||||||
let animatedWidth = this.state.animatedWidth;
|
|
||||||
let direction = this.state.direction;
|
|
||||||
|
|
||||||
if (direction === 0) {
|
|
||||||
animatedWidth = this.state.animatedWidth + STEP;
|
|
||||||
} else {
|
|
||||||
animatedWidth = this.state.animatedWidth - STEP;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRoundOver = animatedWidth < 0;
|
|
||||||
const endOfText = animatedWidth > overflowWidth;
|
|
||||||
|
|
||||||
if (endOfText) {
|
|
||||||
direction = direction === 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRoundOver) {
|
|
||||||
if (this.props.loop) {
|
|
||||||
direction = direction === 0;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ animatedWidth, direction });
|
|
||||||
this.marqueeTimer = setTimeout(animate, TIMEOUT);
|
|
||||||
};
|
|
||||||
|
|
||||||
this.marqueeTimer = setTimeout(animate, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
measureText = () => {
|
|
||||||
const container = this.container;
|
|
||||||
const node = this.text;
|
|
||||||
|
|
||||||
if (container && node) {
|
|
||||||
const containerWidth = container.offsetWidth;
|
|
||||||
const textWidth = node.offsetWidth;
|
|
||||||
const overflowWidth = textWidth - containerWidth;
|
|
||||||
|
|
||||||
if (overflowWidth !== this.state.overflowWidth) {
|
|
||||||
this.setState({ overflowWidth });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const style = {
|
|
||||||
position: 'relative',
|
|
||||||
right: this.state.animatedWidth,
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.overflowWidth < 0) {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
this.container = el;
|
|
||||||
}}
|
|
||||||
className={`ui-marquee ${this.props.className}`}
|
|
||||||
style={{ overflow: 'hidden' }}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
ref={(el) => {
|
|
||||||
this.text = el;
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
|
||||||
>
|
|
||||||
{this.props.text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={(el) => {
|
|
||||||
this.container = el;
|
|
||||||
}}
|
|
||||||
className={`ui-marquee ${this.props.className}`.trim()}
|
|
||||||
style={{ overflow: 'hidden' }}
|
|
||||||
onMouseEnter={this.onHandleMouseEnter}
|
|
||||||
onMouseLeave={this.onHandleMouseLeave}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
ref={(el) => {
|
|
||||||
this.text = el;
|
|
||||||
}}
|
|
||||||
style={style}
|
|
||||||
title={(this.props.title && (this.props.text !== this.props.title)) ? `Original Title: ${this.props.title}` : this.props.text}
|
|
||||||
>
|
|
||||||
{this.props.text}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Marquee;
|
|
@ -1,11 +0,0 @@
|
|||||||
.toggleButton {
|
|
||||||
composes: button from '~Components/Link/IconButton.css';
|
|
||||||
|
|
||||||
padding: 0;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.isDisabled {
|
|
||||||
color: $disabledColor;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
@ -1,79 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './MonitorToggleButton.css';
|
|
||||||
|
|
||||||
function getTooltip(monitored, isDisabled) {
|
|
||||||
if (isDisabled) {
|
|
||||||
return 'Cannot toggle monitored state when movie is unmonitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitored) {
|
|
||||||
return 'Monitored, click to unmonitor';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Unmonitored, click to monitor';
|
|
||||||
}
|
|
||||||
|
|
||||||
class MonitorToggleButton extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = (event) => {
|
|
||||||
const shiftKey = event.nativeEvent.shiftKey;
|
|
||||||
|
|
||||||
this.props.onPress(!this.props.monitored, { shiftKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
monitored,
|
|
||||||
isDisabled,
|
|
||||||
isSaving,
|
|
||||||
size,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const iconName = monitored ? icons.MONITORED : icons.UNMONITORED;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SpinnerIconButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
isDisabled && styles.isDisabled
|
|
||||||
)}
|
|
||||||
name={iconName}
|
|
||||||
size={size}
|
|
||||||
title={getTooltip(monitored, isDisabled)}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
{...otherProps}
|
|
||||||
onPress={this.onPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MonitorToggleButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
size: PropTypes.number,
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
MonitorToggleButton.defaultProps = {
|
|
||||||
className: styles.toggleButton,
|
|
||||||
isDisabled: false,
|
|
||||||
isSaving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MonitorToggleButton;
|
|
@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import OrganizeMovieModalContentConnector from './OrganizeMovieModalContentConnector';
|
|
||||||
|
|
||||||
function OrganizeMovieModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<OrganizeMovieModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OrganizeMovieModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OrganizeMovieModal;
|
|
@ -1,8 +0,0 @@
|
|||||||
.renameIcon {
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message {
|
|
||||||
margin-top: 20px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
@ -1,75 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './OrganizeMovieModalContent.css';
|
|
||||||
|
|
||||||
function OrganizeMovieModalContent(props) {
|
|
||||||
const {
|
|
||||||
movieTitles,
|
|
||||||
onModalClose,
|
|
||||||
onOrganizeMoviePress
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
Organize Selected Movies
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Alert>
|
|
||||||
Tip: To preview a rename... select "Cancel" then click any movie title and use the
|
|
||||||
<Icon
|
|
||||||
className={styles.renameIcon}
|
|
||||||
name={icons.ORGANIZE}
|
|
||||||
/>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<div className={styles.message}>
|
|
||||||
Are you sure you want to organize all files in the {movieTitles.length} selected movie(s)?
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
movieTitles.map((title) => {
|
|
||||||
return (
|
|
||||||
<li key={title}>
|
|
||||||
{title}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
onPress={onOrganizeMoviePress}
|
|
||||||
>
|
|
||||||
Organize
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
OrganizeMovieModalContent.propTypes = {
|
|
||||||
movieTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onOrganizeMoviePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default OrganizeMovieModalContent;
|
|
@ -1,67 +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 * as commandNames from 'Commands/commandNames';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
|
||||||
import OrganizeMovieModalContent from './OrganizeMovieModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { movieIds }) => movieIds,
|
|
||||||
createAllMoviesSelector(),
|
|
||||||
(movieIds, allMovies) => {
|
|
||||||
const movies = _.intersectionWith(allMovies, movieIds, (s, id) => {
|
|
||||||
return s.id === id;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortedMovies = _.orderBy(movies, 'sortTitle');
|
|
||||||
const movieTitles = _.map(sortedMovies, 'title');
|
|
||||||
|
|
||||||
return {
|
|
||||||
movieTitles
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class OrganizeMovieModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOrganizeMoviePress = () => {
|
|
||||||
this.props.executeCommand({
|
|
||||||
name: commandNames.RENAME_MOVIE,
|
|
||||||
movieIds: this.props.movieIds
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onModalClose(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render(props) {
|
|
||||||
return (
|
|
||||||
<OrganizeMovieModalContent
|
|
||||||
{...this.props}
|
|
||||||
onOrganizeMoviePress={this.onOrganizeMoviePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
OrganizeMovieModalContentConnector.propTypes = {
|
|
||||||
movieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizeMovieModalContentConnector);
|
|
@ -1,52 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Menu from 'Components/Menu/Menu';
|
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import SearchMenuItem from 'Components/Menu/SearchMenuItem';
|
|
||||||
import ToolbarMenuButton from 'Components/Menu/ToolbarMenuButton';
|
|
||||||
import { align, icons } from 'Helpers/Props';
|
|
||||||
|
|
||||||
class MovieIndexSearchMenu extends Component {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isDisabled,
|
|
||||||
onSearchPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Menu
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
>
|
|
||||||
<ToolbarMenuButton
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
text="Search"
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
/>
|
|
||||||
<MenuContent>
|
|
||||||
<SearchMenuItem
|
|
||||||
name="missingMoviesSearch"
|
|
||||||
onPress={onSearchPress}
|
|
||||||
>
|
|
||||||
Search Missing
|
|
||||||
</SearchMenuItem>
|
|
||||||
|
|
||||||
<SearchMenuItem
|
|
||||||
name="cutoffUnmetMoviesSearch"
|
|
||||||
onPress={onSearchPress}
|
|
||||||
>
|
|
||||||
Search Cutoff Unmet
|
|
||||||
</SearchMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieIndexSearchMenu.propTypes = {
|
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
|
||||||
onSearchPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieIndexSearchMenu;
|
|
@ -1,70 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function MovieLanguage(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
languages,
|
|
||||||
isCutoffNotMet
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!languages) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (languages.length === 1) {
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
className={className}
|
|
||||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
|
||||||
>
|
|
||||||
{languages[0].name}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
className={className}
|
|
||||||
anchor={
|
|
||||||
<Label
|
|
||||||
className={className}
|
|
||||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
|
||||||
>
|
|
||||||
Multi-Language
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
title={translate('Languages')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
languages.map((language) => {
|
|
||||||
return (
|
|
||||||
<li key={language.id}>
|
|
||||||
{language.name}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieLanguage.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
isCutoffNotMet: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieLanguage.defaultProps = {
|
|
||||||
isCutoffNotMet: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieLanguage;
|
|
@ -1,77 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
|
|
||||||
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
|
||||||
const revision = quality.revision;
|
|
||||||
|
|
||||||
if (revision.real && revision.real > 0) {
|
|
||||||
title += ' [REAL]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (revision.version && revision.version > 1) {
|
|
||||||
title += ' [PROPER]';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (size) {
|
|
||||||
title += ` - ${formatBytes(size)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMonitored) {
|
|
||||||
title += ' [Not Monitored]';
|
|
||||||
} else if (isCutoffNotMet) {
|
|
||||||
title += ' [Cutoff Not Met]';
|
|
||||||
}
|
|
||||||
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function MovieQuality(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
title,
|
|
||||||
quality,
|
|
||||||
size,
|
|
||||||
isMonitored,
|
|
||||||
isCutoffNotMet
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
let kind = kinds.DEFAULT;
|
|
||||||
if (!isMonitored) {
|
|
||||||
kind = kinds.DISABLED;
|
|
||||||
} else if (isCutoffNotMet) {
|
|
||||||
kind = kinds.INVERSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!quality) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
className={className}
|
|
||||||
kind={kind}
|
|
||||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
|
||||||
>
|
|
||||||
{quality.quality.name}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieQuality.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
size: PropTypes.number,
|
|
||||||
isMonitored: PropTypes.bool,
|
|
||||||
isCutoffNotMet: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
MovieQuality.defaultProps = {
|
|
||||||
title: '',
|
|
||||||
isMonitored: true
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieQuality;
|
|
@ -1,32 +0,0 @@
|
|||||||
import { icons } from 'Helpers/Props';
|
|
||||||
|
|
||||||
export function getMovieStatusDetails(status) {
|
|
||||||
|
|
||||||
let statusDetails = {
|
|
||||||
icon: icons.ANNOUNCED,
|
|
||||||
title: 'Announced',
|
|
||||||
message: 'Movie is announced'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === 'deleted') {
|
|
||||||
statusDetails = {
|
|
||||||
icon: icons.MOVIE_DELETED,
|
|
||||||
title: 'Deleted',
|
|
||||||
message: 'Movie was deleted from TMDb'
|
|
||||||
};
|
|
||||||
} else if (status === 'inCinemas') {
|
|
||||||
statusDetails = {
|
|
||||||
icon: icons.IN_CINEMAS,
|
|
||||||
title: 'In Cinemas',
|
|
||||||
message: 'Movie is in Cinemas'
|
|
||||||
};
|
|
||||||
} else if (status === 'released') {
|
|
||||||
statusDetails = {
|
|
||||||
icon: icons.MOVIE_FILE,
|
|
||||||
title: 'Released',
|
|
||||||
message: 'Movie is released'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusDetails;
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { PureComponent } from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
|
|
||||||
class MovieTitleLink extends PureComponent {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
titleSlug,
|
|
||||||
title
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
to={link}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieTitleLink.propTypes = {
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
title: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieTitleLink;
|
|
@ -1,30 +0,0 @@
|
|||||||
.indexer {
|
|
||||||
composes: card from '~Components/Card.css';
|
|
||||||
|
|
||||||
width: 290px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nameContainer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
@add-mixin truncate;
|
|
||||||
|
|
||||||
margin-bottom: 20px;
|
|
||||||
font-weight: 300;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cloneButton {
|
|
||||||
composes: button from '~Components/Link/IconButton.css';
|
|
||||||
|
|
||||||
height: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.enabled {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
@ -1,152 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
|
||||||
import styles from './Indexer.css';
|
|
||||||
|
|
||||||
class Indexer extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditIndexerModalOpen: false,
|
|
||||||
isDeleteIndexerModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditIndexerPress = () => {
|
|
||||||
this.setState({ isEditIndexerModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditIndexerModalClose = () => {
|
|
||||||
this.setState({ isEditIndexerModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteIndexerPress = () => {
|
|
||||||
this.setState({
|
|
||||||
isEditIndexerModalOpen: false,
|
|
||||||
isDeleteIndexerModalOpen: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onDeleteIndexerModalClose= () => {
|
|
||||||
this.setState({ isDeleteIndexerModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
onConfirmDeleteIndexer = () => {
|
|
||||||
this.props.onConfirmDeleteIndexer(this.props.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloneIndexerPress = () => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
onCloneIndexerPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onCloneIndexerPress(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
enable,
|
|
||||||
supportsRss,
|
|
||||||
priority,
|
|
||||||
showPriority
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={styles.indexer}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onEditIndexerPress}
|
|
||||||
>
|
|
||||||
<div className={styles.nameContainer}>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
className={styles.cloneButton}
|
|
||||||
title={translate('CloneIndexer')}
|
|
||||||
name={icons.CLONE}
|
|
||||||
onPress={this.onCloneIndexerPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.enabled}>
|
|
||||||
|
|
||||||
{
|
|
||||||
supportsRss && enable &&
|
|
||||||
<Label kind={kinds.SUCCESS}>
|
|
||||||
RSS
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showPriority &&
|
|
||||||
<Label kind={kinds.DEFAULT}>
|
|
||||||
{translate('Priority')}: {priority}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!enable &&
|
|
||||||
<Label
|
|
||||||
kind={kinds.DISABLED}
|
|
||||||
outline={true}
|
|
||||||
>
|
|
||||||
{translate('Disabled')}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<EditIndexerModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditIndexerModalOpen}
|
|
||||||
onModalClose={this.onEditIndexerModalClose}
|
|
||||||
onDeleteIndexerPress={this.onDeleteIndexerPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={this.state.isDeleteIndexerModalOpen}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
title={translate('DeleteIndexer')}
|
|
||||||
message={translate('DeleteIndexerMessageText', [name])}
|
|
||||||
confirmLabel={translate('Delete')}
|
|
||||||
onConfirm={this.onConfirmDeleteIndexer}
|
|
||||||
onCancel={this.onDeleteIndexerModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Indexer.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
enable: PropTypes.bool.isRequired,
|
|
||||||
supportsRss: PropTypes.bool.isRequired,
|
|
||||||
supportsSearch: PropTypes.bool.isRequired,
|
|
||||||
onCloneIndexerPress: PropTypes.func.isRequired,
|
|
||||||
onConfirmDeleteIndexer: PropTypes.func.isRequired,
|
|
||||||
priority: PropTypes.number.isRequired,
|
|
||||||
showPriority: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Indexer;
|
|
@ -1,20 +0,0 @@
|
|||||||
.indexers {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addIndexer {
|
|
||||||
composes: indexer from '~./Indexer.css';
|
|
||||||
|
|
||||||
background-color: $cardAlternateBackgroundColor;
|
|
||||||
color: $gray;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 5px 20px 0;
|
|
||||||
border: 1px solid $borderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
}
|
|
@ -1,126 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AddIndexerModal from './AddIndexerModal';
|
|
||||||
import EditIndexerModalConnector from './EditIndexerModalConnector';
|
|
||||||
import Indexer from './Indexer';
|
|
||||||
import styles from './Indexers.css';
|
|
||||||
|
|
||||||
class Indexers extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onAddIndexerPress = () => {
|
|
||||||
this.setState({ isAddIndexerModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onCloneIndexerPress = (id) => {
|
|
||||||
this.props.dispatchCloneIndexer({ id });
|
|
||||||
this.setState({ isEditIndexerModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onAddIndexerModalClose = ({ indexerSelected = false } = {}) => {
|
|
||||||
this.setState({
|
|
||||||
isAddIndexerModalOpen: false,
|
|
||||||
isEditIndexerModalOpen: indexerSelected
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onEditIndexerModalClose = () => {
|
|
||||||
this.setState({ isEditIndexerModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
dispatchCloneIndexer,
|
|
||||||
onConfirmDeleteIndexer,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isAddIndexerModalOpen,
|
|
||||||
isEditIndexerModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const showPriority = items.some((index) => index.priority !== 25);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Indexers')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('UnableToLoadIndexers')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.indexers}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<Indexer
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
showPriority={showPriority}
|
|
||||||
onCloneIndexerPress={this.onCloneIndexerPress}
|
|
||||||
onConfirmDeleteIndexer={onConfirmDeleteIndexer}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<Card
|
|
||||||
className={styles.addIndexer}
|
|
||||||
onPress={this.onAddIndexerPress}
|
|
||||||
>
|
|
||||||
<div className={styles.center}>
|
|
||||||
<Icon
|
|
||||||
name={icons.ADD}
|
|
||||||
size={45}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddIndexerModal
|
|
||||||
isOpen={isAddIndexerModalOpen}
|
|
||||||
onModalClose={this.onAddIndexerModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EditIndexerModalConnector
|
|
||||||
isOpen={isEditIndexerModalOpen}
|
|
||||||
onModalClose={this.onEditIndexerModalClose}
|
|
||||||
/>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Indexers.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
dispatchCloneIndexer: PropTypes.func.isRequired,
|
|
||||||
onConfirmDeleteIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Indexers;
|
|
@ -1,58 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { cloneIndexer, deleteIndexer, fetchIndexers } from 'Store/Actions/indexerActions';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
|
||||||
import Indexers from './Indexers';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('indexers', sortByName),
|
|
||||||
(indexers) => indexers
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchIndexers: fetchIndexers,
|
|
||||||
dispatchDeleteIndexer: deleteIndexer,
|
|
||||||
dispatchCloneIndexer: cloneIndexer
|
|
||||||
};
|
|
||||||
|
|
||||||
class IndexersConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchIndexers();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onConfirmDeleteIndexer = (id) => {
|
|
||||||
this.props.dispatchDeleteIndexer({ id });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Indexers
|
|
||||||
{...this.props}
|
|
||||||
onConfirmDeleteIndexer={this.onConfirmDeleteIndexer}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IndexersConnector.propTypes = {
|
|
||||||
dispatchFetchIndexers: PropTypes.func.isRequired,
|
|
||||||
dispatchDeleteIndexer: PropTypes.func.isRequired,
|
|
||||||
dispatchCloneIndexer: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexersConnector);
|
|
@ -1,93 +0,0 @@
|
|||||||
.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(3n+1) {
|
|
||||||
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 400px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sizeInput {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 5px;
|
|
||||||
padding: 6px;
|
|
||||||
width: 75px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
|
||||||
.qualityDefinition {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.qualityDefinition:first-child {
|
|
||||||
border-top: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quality {
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sizeLimit {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,308 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ReactSlider from 'react-slider';
|
|
||||||
import NumberInput from 'Components/Form/NumberInput';
|
|
||||||
import TextInput from 'Components/Form/TextInput';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import roundNumber from 'Utilities/Number/roundNumber';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityDefinitionLimits from './QualityDefinitionLimits';
|
|
||||||
import styles from './QualityDefinition.css';
|
|
||||||
|
|
||||||
const MIN = 0;
|
|
||||||
const MAX = 400;
|
|
||||||
|
|
||||||
const 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.state = {
|
|
||||||
sliderMinSize: getSliderValue(props.minSize, slider.min),
|
|
||||||
sliderMaxSize: getSliderValue(props.maxSize, slider.max),
|
|
||||||
sliderPreferredSize: getSliderValue(props.preferredSize, (slider.max - 3))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// 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)}/h`;
|
|
||||||
|
|
||||||
const preferredBytes = preferredSize * 1024 * 1024;
|
|
||||||
const preferredSixty = preferredBytes ? `${formatBytes(preferredBytes * 60)}/h` : 'Unlimited';
|
|
||||||
|
|
||||||
const maxBytes = maxSize && maxSize * 1024 * 1024;
|
|
||||||
const maxSixty = maxBytes ? `${formatBytes(maxBytes * 60)}/h` : '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={3}
|
|
||||||
value={[sliderMinSize, sliderPreferredSize, sliderMaxSize]}
|
|
||||||
withTracks={true}
|
|
||||||
allowCross={false}
|
|
||||||
snapDragDisabled={true}
|
|
||||||
className={styles.slider}
|
|
||||||
trackClassName={styles.bar}
|
|
||||||
thumbClassName={styles.handle}
|
|
||||||
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>
|
|
||||||
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>
|
|
||||||
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>
|
|
||||||
Max
|
|
||||||
|
|
||||||
<NumberInput
|
|
||||||
className={styles.sizeInput}
|
|
||||||
name={`${id}.min`}
|
|
||||||
value={maxSize || MAX}
|
|
||||||
min={minSize + 5}
|
|
||||||
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;
|
|
@ -1,70 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import { setQualityDefinitionValue } from 'Store/Actions/settingsActions';
|
|
||||||
import QualityDefinition from './QualityDefinition';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setQualityDefinitionValue,
|
|
||||||
clearPendingChanges
|
|
||||||
};
|
|
||||||
|
|
||||||
class QualityDefinitionConnector extends Component {
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.props.clearPendingChanges({ section: 'settings.qualityDefinitions' });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTitleChange = ({ value }) => {
|
|
||||||
this.props.setQualityDefinitionValue({ id: this.props.id, name: 'title', value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onSizeChange = ({ minSize, maxSize, preferredSize }) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
minSize: currentMinSize,
|
|
||||||
maxSize: currentMaxSize,
|
|
||||||
preferredSize: currentPreferredSize
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (minSize !== currentMinSize) {
|
|
||||||
this.props.setQualityDefinitionValue({ id, name: 'minSize', value: minSize });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (maxSize !== currentMaxSize) {
|
|
||||||
this.props.setQualityDefinitionValue({ id, name: 'maxSize', value: maxSize });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preferredSize !== currentPreferredSize) {
|
|
||||||
this.props.setQualityDefinitionValue({ id, name: 'preferredSize', value: preferredSize });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<QualityDefinition
|
|
||||||
{...this.props}
|
|
||||||
onTitleChange={this.onTitleChange}
|
|
||||||
onSizeChange={this.onSizeChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitionConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
minSize: PropTypes.number,
|
|
||||||
maxSize: PropTypes.number,
|
|
||||||
preferredSize: PropTypes.number,
|
|
||||||
setQualityDefinitionValue: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, mapDispatchToProps)(QualityDefinitionConnector);
|
|
@ -1,40 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function QualityDefinitionLimits(props) {
|
|
||||||
const {
|
|
||||||
bytes,
|
|
||||||
message
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (!bytes) {
|
|
||||||
return <div>{message}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sixty = formatBytes(bytes * 60);
|
|
||||||
const ninety = formatBytes(bytes * 90);
|
|
||||||
const hundredTwenty = formatBytes(bytes * 120);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesSixty', [sixty])}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesNinety', [ninety])}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{translate('MinutesHundredTwenty', [hundredTwenty])}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitionLimits.propTypes = {
|
|
||||||
bytes: PropTypes.number,
|
|
||||||
message: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityDefinitionLimits;
|
|
@ -1,41 +0,0 @@
|
|||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,74 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityDefinitionConnector from './QualityDefinitionConnector';
|
|
||||||
import styles from './QualityDefinitions.css';
|
|
||||||
|
|
||||||
class QualityDefinitions extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
advancedSettings,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('QualityDefinitions')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('UnableToLoadQualityDefinitions')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.quality}>Quality</div>
|
|
||||||
<div className={styles.title}>Title</div>
|
|
||||||
<div className={styles.sizeLimit}>Size Limit</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
advancedSettings ?
|
|
||||||
<div className={styles.megabytesPerMinute}>
|
|
||||||
Megabytes Per Minute
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.definitions}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<QualityDefinitionConnector
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.sizeLimitHelpTextContainer}>
|
|
||||||
<div className={styles.sizeLimitHelpText}>
|
|
||||||
Limits are automatically adjusted for the movie runtime.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QualityDefinitions.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
defaultProfile: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
advancedSettings: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default QualityDefinitions;
|
|
@ -1,92 +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() {
|
|
||||||
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)(QualityDefinitionsConnector);
|
|
@ -1,69 +0,0 @@
|
|||||||
import React, { Component } from 'react';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import QualityDefinitionsConnector from './Definition/QualityDefinitionsConnector';
|
|
||||||
|
|
||||||
class Quality extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._saveCallback = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isSaving: false,
|
|
||||||
hasPendingChanges: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChildMounted = (saveCallback) => {
|
|
||||||
this._saveCallback = saveCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
onChildStateChange = (payload) => {
|
|
||||||
this.setState(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
if (this._saveCallback) {
|
|
||||||
this._saveCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
hasPendingChanges
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('QualitySettings')}>
|
|
||||||
<SettingsToolbarConnector
|
|
||||||
isSaving={isSaving}
|
|
||||||
hasPendingChanges={hasPendingChanges}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
<QualityDefinitionsConnector
|
|
||||||
onChildMounted={this.onChildMounted}
|
|
||||||
onChildStateChange={this.onChildStateChange}
|
|
||||||
/>
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Quality;
|
|
@ -1,193 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { sortDirections } from 'Helpers/Props';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
// import { batchActions } from 'redux-batched-actions';
|
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import dateFilterPredicate from 'Utilities/Date/dateFilterPredicate';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import { updateItem } from './baseActions';
|
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
|
||||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
|
||||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'movies';
|
|
||||||
|
|
||||||
export const filters = [
|
|
||||||
{
|
|
||||||
key: 'all',
|
|
||||||
label: translate('All'),
|
|
||||||
filters: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const filterPredicates = {
|
|
||||||
added: function(item, filterValue, type) {
|
|
||||||
return dateFilterPredicate(item.added, filterValue, type);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const sortPredicates = {
|
|
||||||
status: function(item) {
|
|
||||||
let result = 0;
|
|
||||||
|
|
||||||
if (item.monitored) {
|
|
||||||
result += 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.status === 'announced') {
|
|
||||||
result++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.status === 'inCinemas') {
|
|
||||||
result += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.status === 'released') {
|
|
||||||
result += 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null,
|
|
||||||
items: [],
|
|
||||||
sortKey: 'name',
|
|
||||||
sortDirection: sortDirections.ASCENDING,
|
|
||||||
pendingChanges: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_MOVIES = 'movies/fetchMovies';
|
|
||||||
export const SET_MOVIE_VALUE = 'movies/setMovieValue';
|
|
||||||
export const SAVE_MOVIE = 'movies/saveMovie';
|
|
||||||
export const DELETE_MOVIE = 'movies/deleteMovie';
|
|
||||||
|
|
||||||
export const TOGGLE_MOVIE_MONITORED = 'movies/toggleMovieMonitored';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchMovies = createThunk(FETCH_MOVIES);
|
|
||||||
export const saveMovie = createThunk(SAVE_MOVIE, (payload) => {
|
|
||||||
const newPayload = {
|
|
||||||
...payload
|
|
||||||
};
|
|
||||||
|
|
||||||
if (payload.moveFiles) {
|
|
||||||
newPayload.queryParams = {
|
|
||||||
moveFiles: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
delete newPayload.moveFiles;
|
|
||||||
|
|
||||||
return newPayload;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const deleteMovie = createThunk(DELETE_MOVIE, (payload) => {
|
|
||||||
return {
|
|
||||||
...payload,
|
|
||||||
queryParams: {
|
|
||||||
deleteFiles: payload.deleteFiles
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const toggleMovieMonitored = createThunk(TOGGLE_MOVIE_MONITORED);
|
|
||||||
|
|
||||||
export const setMovieValue = createAction(SET_MOVIE_VALUE, (payload) => {
|
|
||||||
return {
|
|
||||||
section,
|
|
||||||
...payload
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Helpers
|
|
||||||
|
|
||||||
function getSaveAjaxOptions({ ajaxOptions, payload }) {
|
|
||||||
if (payload.moveFolder) {
|
|
||||||
ajaxOptions.url = `${ajaxOptions.url}?moveFolder=true`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ajaxOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
|
|
||||||
[FETCH_MOVIES]: createFetchHandler(section, '/movie'),
|
|
||||||
[SAVE_MOVIE]: createSaveProviderHandler(section, '/movie', { getAjaxOptions: getSaveAjaxOptions }),
|
|
||||||
[DELETE_MOVIE]: createRemoveItemHandler(section, '/movie'),
|
|
||||||
|
|
||||||
[TOGGLE_MOVIE_MONITORED]: (getState, payload, dispatch) => {
|
|
||||||
const {
|
|
||||||
movieId: id,
|
|
||||||
monitored
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const movie = _.find(getState().movies.items, { id });
|
|
||||||
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section,
|
|
||||||
isSaving: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: `/movie/${id}`,
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({
|
|
||||||
...movie,
|
|
||||||
monitored
|
|
||||||
}),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section,
|
|
||||||
isSaving: false
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
|
||||||
|
|
||||||
[SET_MOVIE_VALUE]: createSetSettingValueReducer(section)
|
|
||||||
|
|
||||||
}, defaultState, section);
|
|
@ -1,15 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createAllMoviesSelector from './createAllMoviesSelector';
|
|
||||||
|
|
||||||
function createExistingMovieSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { tmdbId }) => tmdbId,
|
|
||||||
createAllMoviesSelector(),
|
|
||||||
(tmdbId, movies) => {
|
|
||||||
return _.some(movies, { tmdbId });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createExistingMovieSelector;
|
|
@ -1,26 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createAllMoviesSelector from './createAllMoviesSelector';
|
|
||||||
|
|
||||||
function createImportMovieItemSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.addMovie,
|
|
||||||
(state) => state.importMovie,
|
|
||||||
createAllMoviesSelector(),
|
|
||||||
(id, addMovie, importMovie, movies) => {
|
|
||||||
const item = _.find(importMovie.items, { id }) || {};
|
|
||||||
const selectedMovie = item && item.selectedMovie;
|
|
||||||
const isExistingMovie = !!selectedMovie && _.some(movies, { tmdbId: selectedMovie.tmdbId });
|
|
||||||
|
|
||||||
return {
|
|
||||||
defaultMonitor: addMovie.defaults.monitor,
|
|
||||||
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
|
|
||||||
...item,
|
|
||||||
isExistingMovie
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createImportMovieItemSelector;
|
|
@ -1,21 +0,0 @@
|
|||||||
import { createSelector } from 'reselect';
|
|
||||||
import createAllMoviesSelector from './createAllMoviesSelector';
|
|
||||||
|
|
||||||
function createMovieCountSelector() {
|
|
||||||
return createSelector(
|
|
||||||
createAllMoviesSelector(),
|
|
||||||
(state) => state.movies.error,
|
|
||||||
(state) => state.movies.isFetching,
|
|
||||||
(state) => state.movies.isPopulated,
|
|
||||||
(movies, error, isFetching, isPopulated) => {
|
|
||||||
return {
|
|
||||||
count: movies.length,
|
|
||||||
error,
|
|
||||||
isFetching,
|
|
||||||
isPopulated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createMovieCountSelector;
|
|
@ -1,15 +0,0 @@
|
|||||||
function formatRuntime(minutes, format) {
|
|
||||||
if (!minutes) {
|
|
||||||
return (format === 'hoursMinutes') ? '0m' : '0 mins';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format === 'minutes') {
|
|
||||||
return `${minutes} mins`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const movieHours = Math.floor(minutes / 60);
|
|
||||||
const movieMinutes = (minutes <= 59) ? minutes : minutes % 60;
|
|
||||||
return `${((movieHours > 0) ? `${movieHours}h ` : '') + movieMinutes}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default formatRuntime;
|
|
@ -1,21 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import { update } from 'Store/Actions/baseActions';
|
|
||||||
|
|
||||||
function updateEpisodes(section, episodes, episodeIds, options) {
|
|
||||||
const data = _.reduce(episodes, (result, item) => {
|
|
||||||
if (episodeIds.indexOf(item.id) > -1) {
|
|
||||||
result.push({
|
|
||||||
...item,
|
|
||||||
...options
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return update({ section, data });
|
|
||||||
}
|
|
||||||
|
|
||||||
export default updateEpisodes;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue