mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-08 05:51:47 -07:00
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:
parent
e1e7cad951
commit
21428cba6f
154 changed files with 2946 additions and 701 deletions
|
@ -27,6 +27,10 @@
|
|||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.isHidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.isMobile {
|
||||
height: 50px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
|
||||
/* Sizes */
|
||||
|
||||
.extraSmall {
|
||||
max-width: $formGroupExtraSmallWidth;
|
||||
}
|
||||
|
||||
.small {
|
||||
max-width: $formGroupSmallWidth;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: '' });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
$modalBodyPadding: 30px;
|
||||
|
||||
.modalBody {
|
||||
flex: 1 0 1px;
|
||||
padding: $modalBodyPadding;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue