Medium Support (Multi-disc Albums), Quality Grouping (#121)

* Multi Disc Stage 1 - Backend Work

* Quality Group Functionality

* Fixed: Only show wanted album types on ArtistDetail page

* Add Media Count Column to ArtistDetail Page

* Parser updates for multidisc cases, other usenet release title formats

* Search for Tracks by Medium Number in Addition to Title and TrackNumber

* Medium Renaming Token for Track Naming

* fixup Codacy and Comment Cleanup

* fixup remove comments
This commit is contained in:
Qstick 2017-11-15 21:24:33 -05:00 committed by GitHub
parent e1e7cad951
commit 21428cba6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
154 changed files with 2946 additions and 701 deletions

View file

@ -27,6 +27,10 @@
background-color: #aaa;
}
.isHidden {
display: none;
}
.isMobile {
height: 50px;
border-bottom: 1px solid $borderColor;

View file

@ -28,6 +28,7 @@ class EnhancedSelectInputOption extends Component {
className,
isSelected,
isDisabled,
isHidden,
isMobile,
children
} = this.props;
@ -38,6 +39,7 @@ class EnhancedSelectInputOption extends Component {
className,
isSelected && styles.isSelected,
isDisabled && styles.isDisabled,
isHidden && styles.isHidden,
isMobile && styles.isMobile
)}
component="div"
@ -64,6 +66,7 @@ EnhancedSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isHidden: PropTypes.bool.isRequired,
isMobile: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onSelect: PropTypes.func.isRequired
@ -71,7 +74,8 @@ EnhancedSelectInputOption.propTypes = {
EnhancedSelectInputOption.defaultProps = {
className: styles.option,
isDisabled: false
isDisabled: false,
isHidden: false
};
export default EnhancedSelectInputOption;

View file

@ -5,6 +5,10 @@
/* Sizes */
.extraSmall {
max-width: $formGroupExtraSmallWidth;
}
.small {
max-width: $formGroupSmallWidth;
}

View file

@ -41,7 +41,7 @@ function FormGroup(props) {
FormGroup.propTypes = {
className: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
size: PropTypes.string.isRequired,
size: PropTypes.oneOf(sizes.all).isRequired,
advancedSettings: PropTypes.bool.isRequired,
isAdvanced: PropTypes.bool.isRequired
};

View file

@ -1,7 +1,6 @@
.label {
display: flex;
justify-content: flex-end;
flex: 0 0 $formLabelWidth;
margin-right: $formLabelRightMarginWidth;
font-weight: bold;
line-height: 35px;
@ -20,3 +19,12 @@
justify-content: flex-start;
}
}
.small {
flex: 0 0 $formLabelSmallWidth;
}
.large {
flex: 0 0 $formLabelLargeWidth;
}

View file

@ -1,12 +1,14 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import { sizes } from 'Helpers/Props';
import styles from './FormLabel.css';
function FormLabel({
children,
className,
errorClassName,
size,
name,
hasError,
isAdvanced,
@ -17,6 +19,7 @@ function FormLabel({
{...otherProps}
className={classNames(
className,
styles[size],
hasError && errorClassName,
isAdvanced && styles.isAdvanced
)}
@ -31,6 +34,7 @@ FormLabel.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
errorClassName: PropTypes.string,
size: PropTypes.oneOf(sizes.all),
name: PropTypes.string,
hasError: PropTypes.bool,
isAdvanced: PropTypes.bool.isRequired
@ -39,7 +43,8 @@ FormLabel.propTypes = {
FormLabel.defaultProps = {
className: styles.label,
errorClassName: styles.hasError,
isAdvanced: false
isAdvanced: false,
size: sizes.LARGE
};
export default FormLabel;

View file

@ -22,20 +22,19 @@ class RootFolderSelectInput extends Component {
componentDidUpdate(prevProps) {
const {
name,
values,
isSaving,
saveError,
onChange
} = this.props;
const newRootFolderPath = this.state.newRootFolderPath;
if (
prevProps.isSaving &&
!isSaving &&
!saveError &&
values.length - prevProps.values.length === 1
newRootFolderPath
) {
const newRootFolderPath = this.state.newRootFolderPath;
onChange({ name, value: newRootFolderPath });
this.setState({ newRootFolderPath: '' });
}

View file

@ -33,7 +33,8 @@ function createMapStateToProps() {
values.push({
key: '',
value: '',
isDisabled: true
isDisabled: true,
isHidden: true
});
}
@ -64,6 +65,18 @@ class RootFolderSelectInputConnector extends Component {
//
// Lifecycle
componentWillMount() {
const {
value,
values,
onChange
} = this.props;
if (value == null && values[0].key === '') {
onChange({ name, value: '' });
}
}
componentDidMount() {
const {
name,

View file

@ -12,9 +12,9 @@ const messages = [
'Hum something loud while others stare',
'Loading humorous message... Please Wait',
'I could\'ve been faster in Python',
'Don\'t forget to rewind your episodes',
'Don\'t forget to rewind your tracks',
'Congratulations! you are the 1000th visitor.',
'HELP!, I\'m being held hostage and forced to write these stupid lines!',
'HELP! I\'m being held hostage and forced to write these stupid lines!',
'RE-calibrating the internet...',
'I\'ll be here all week',
'Don\'t forget to tip your waitress',

View file

@ -51,6 +51,18 @@
width: 1080px;
}
.extraLarge {
composes: modal;
width: 1440px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.modal.extraLarge {
width: 90%;
}
}
@media only screen and (max-width: $breakpointLarge) {
.modal.large {
width: 90%;
@ -71,9 +83,10 @@
.modal.small,
.modal.medium,
.modal.large {
.modal.large,
.modal.extraLarge {
max-height: 100%;
width: 100%;
height: 100%;
height: 100% !important;
}
}

View file

@ -139,6 +139,7 @@ class Modal extends Component {
render() {
const {
className,
style,
backdropClassName,
size,
children,
@ -166,6 +167,7 @@ class Modal extends Component {
className,
styles[size]
)}
style={style}
>
{children}
</div>
@ -180,6 +182,7 @@ class Modal extends Component {
Modal.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
backdropClassName: PropTypes.string,
size: PropTypes.oneOf(sizes.all),
children: PropTypes.node,

View file

@ -1,5 +1,3 @@
$modalBodyPadding: 30px;
.modalBody {
flex: 1 0 1px;
padding: $modalBodyPadding;

View file

@ -23,13 +23,13 @@ class PageHeader extends Component {
}
componentDidMount() {
this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.openKeyboardShortcutsModal);
this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.onOpenKeyboardShortcutsModal);
}
//
// Control
openKeyboardShortcutsModal = () => {
onOpenKeyboardShortcutsModal = () => {
this.setState({ isKeyboardShortcutsModalOpen: true });
}
@ -76,7 +76,9 @@ class PageHeader extends Component {
name={icons.HEART}
to="https://lidarr.audio/donate.html"
/>
<PageHeaderActionsMenuConnector />
<PageHeaderActionsMenuConnector
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
/>
</div>
<KeyboardShortcutsModal

View file

@ -11,6 +11,7 @@ import styles from './PageHeaderActionsMenu.css';
function PageHeaderActionsMenu(props) {
const {
formsAuth,
onKeyboardShortcutsPress,
onRestartPress,
onShutdownPress
} = props;
@ -25,6 +26,16 @@ function PageHeaderActionsMenu(props) {
</MenuButton>
<MenuContent>
<MenuItem onPress={onKeyboardShortcutsPress}>
<Icon
className={styles.itemIcon}
name={icons.KEYBOARD}
/>
Keyboard Shortcuts
</MenuItem>
<div className={styles.separator} />
<MenuItem onPress={onRestartPress}>
<Icon
className={styles.itemIcon}
@ -68,6 +79,7 @@ function PageHeaderActionsMenu(props) {
PageHeaderActionsMenu.propTypes = {
formsAuth: PropTypes.bool.isRequired,
onKeyboardShortcutsPress: PropTypes.func.isRequired,
onRestartPress: PropTypes.func.isRequired,
onShutdownPress: PropTypes.func.isRequired
};

View file

@ -26,6 +26,14 @@ function getState(status) {
}
}
function isAppDisconnected(disconnectedTime) {
if (!disconnectedTime) {
return false;
}
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
}
function createMapStateToProps() {
return createSelector(
(state) => state.app.isReconnecting,
@ -66,6 +74,7 @@ class SignalRConnector extends Component {
this.signalRconnection = null;
this.retryInterval = 5;
this.retryTimeoutId = null;
this.disconnectedTime = null;
}
componentDidMount() {
@ -90,7 +99,7 @@ class SignalRConnector extends Component {
// Control
retryConnection = () => {
if (this.retryInterval >= 30) {
if (isAppDisconnected(this.disconnectedTime)) {
this.setState({
isDisconnected: true
});
@ -290,6 +299,9 @@ class SignalRConnector extends Component {
console.log(`SignalR: ${state}`);
if (state === 'connected') {
// Clear disconnected time
this.disconnectedTime = null;
// Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting.
@ -322,6 +334,10 @@ class SignalRConnector extends Component {
return;
}
if (!this.disconnectedTime) {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
this.props.setAppValue({
isReconnecting: true
});
@ -332,11 +348,14 @@ class SignalRConnector extends Component {
return;
}
if (!this.disconnectedTime) {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
this.props.setAppValue({
isConnected: false,
isReconnecting: true
// Don't set isDisconnected yet, it'll be set it if it's disconnected
// for ~105 seconds (retry interval reaches 30 seconds)
isReconnecting: true,
isDisconnected: isAppDisconnected(this.disconnectedTime)
});
this.retryConnection();

View file

@ -8,7 +8,7 @@ import TableOptionsColumn from './TableOptionsColumn';
import styles from './TableOptionsColumnDragPreview.css';
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
const formLabelWidth = parseInt(dimensions.formLabelWidth);
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
@ -40,7 +40,7 @@ class TableOptionsColumnDragPreview extends Component {
// list item and the preview is wider than the drag handle.
const { x, y } = currentOffset;
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
const style = {

View file

@ -53,6 +53,14 @@ class Popover extends Component {
this.state = {
isOpen: false
};
this._closeTimeout = null;
}
componentWillUnmount() {
if (this._closeTimeout) {
this._closeTimeout = clearTimeout(this._closeTimeout);
}
}
//
@ -63,11 +71,17 @@ class Popover extends Component {
}
onMouseEnter = () => {
if (this._closeTimeout) {
this._closeTimeout = clearTimeout(this._closeTimeout);
}
this.setState({ isOpen: true });
}
onMouseLeave = () => {
this.setState({ isOpen: false });
this._closeTimeout = setTimeout(() => {
this.setState({ isOpen: false });
}, 100);
}
//
@ -98,24 +112,28 @@ class Popover extends Component {
{
this.state.isOpen &&
<div className={styles.popoverContainer}>
<div className={styles.popover}>
<div
className={classNames(
styles.arrow,
styles[position]
)}
/>
<div
className={styles.popoverContainer}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
>
<div className={styles.popover}>
<div
className={classNames(
styles.arrow,
styles[position]
)}
/>
<div className={styles.title}>
{title}
</div>
<div className={styles.title}>
{title}
</div>
<div className={styles.body}>
{body}
</div>
<div className={styles.body}>
{body}
</div>
</div>
</div>
}
</TetherComponent>
);

View file

@ -50,11 +50,17 @@ class Tooltip extends Component {
constructor(props, context) {
super(props, context);
this._closeTimeout = null;
this.state = {
isOpen: false
};
this._closeTimeout = null;
}
componentWillUnmount() {
if (this._closeTimeout) {
this._closeTimeout = clearTimeout(this._closeTimeout);
}
}
//
@ -83,6 +89,7 @@ class Tooltip extends Component {
render() {
const {
className,
anchor,
tooltip,
kind,
@ -97,6 +104,7 @@ class Tooltip extends Component {
{...tetherOptions[position]}
>
<span
className={className}
// onClick={this.onClick}
onMouseEnter={this.onMouseEnter}
onMouseLeave={this.onMouseLeave}
@ -137,6 +145,7 @@ class Tooltip extends Component {
}
Tooltip.propTypes = {
className: PropTypes.string,
anchor: PropTypes.node.isRequired,
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]),