mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-11 15:47:09 -07:00
Added: Device load support for Pushbullet
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
e41f884153
commit
23bc5b11cf
19 changed files with 439 additions and 27 deletions
8
frontend/src/Components/Form/DeviceInput.css
Normal file
8
frontend/src/Components/Form/DeviceInput.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.deviceInputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
composes: inputContainer from './TagInput.css';
|
||||
composes: hasButton from 'Components/Form/Input.css';
|
||||
}
|
103
frontend/src/Components/Form/DeviceInput.js
Normal file
103
frontend/src/Components/Form/DeviceInput.js
Normal file
|
@ -0,0 +1,103 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import FormInputButton from './FormInputButton';
|
||||
import TagInput, { tagShape } from './TagInput';
|
||||
import styles from './DeviceInput.css';
|
||||
|
||||
class DeviceInput extends Component {
|
||||
|
||||
onTagAdd = (device) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
// New tags won't have an ID, only a name.
|
||||
const deviceId = device.id || device.name;
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: [...value, deviceId]
|
||||
});
|
||||
}
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = value.slice();
|
||||
newValue.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
items,
|
||||
selectedDevices,
|
||||
hasError,
|
||||
hasWarning,
|
||||
isFetching,
|
||||
onRefreshPress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<TagInput
|
||||
className={styles.inputContainer}
|
||||
tags={selectedDevices}
|
||||
tagList={items}
|
||||
allowNew={true}
|
||||
minQueryLength={0}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
onPress={onRefreshPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
/>
|
||||
</FormInputButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceInput.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
selectedDevices: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeviceInput.defaultProps = {
|
||||
className: styles.deviceInputWrapper,
|
||||
inputClassName: styles.input
|
||||
};
|
||||
|
||||
export default DeviceInput;
|
99
frontend/src/Components/Form/DeviceInputConnector.js
Normal file
99
frontend/src/Components/Form/DeviceInputConnector.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
|
||||
import DeviceInput from './DeviceInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.devices,
|
||||
(value, devices) => {
|
||||
|
||||
return {
|
||||
...devices,
|
||||
selectedDevices: value.map((valueDevice) => {
|
||||
// Disable equality ESLint rule so we don't need to worry about
|
||||
// a type mismatch between the value items and the device ID.
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const device = devices.items.find((d) => d.id == valueDevice);
|
||||
|
||||
if (device) {
|
||||
return {
|
||||
id: device.id,
|
||||
name: `${device.name} (${device.id})`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: valueDevice,
|
||||
name: `Unknown (${valueDevice})`
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchDevices: fetchDevices,
|
||||
dispatchClearDevices: clearDevices
|
||||
};
|
||||
|
||||
class DeviceInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount = () => {
|
||||
this._populate();
|
||||
}
|
||||
|
||||
componentWillUnmount = () => {
|
||||
// this.props.dispatchClearDevices();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_populate() {
|
||||
const {
|
||||
provider,
|
||||
providerData,
|
||||
dispatchFetchDevices
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchDevices({ provider, providerData });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshPress = () => {
|
||||
this._populate();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<DeviceInput
|
||||
{...this.props}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeviceInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchDevices: PropTypes.func.isRequired,
|
||||
dispatchClearDevices: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
|
@ -4,6 +4,7 @@ import { inputTypes } from 'Helpers/Props';
|
|||
import Link from 'Components/Link/Link';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
import OAuthInputConnector from './OAuthInputConnector';
|
||||
|
@ -30,6 +31,9 @@ function getComponent(type) {
|
|||
case inputTypes.CHECK:
|
||||
return CheckInput;
|
||||
|
||||
case inputTypes.DEVICE:
|
||||
return DeviceInputConnector;
|
||||
|
||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||
return MonitorAlbumsSelectInput;
|
||||
|
||||
|
|
|
@ -12,6 +12,8 @@ function getType(type) {
|
|||
return inputTypes.CAPTCHA;
|
||||
case 'checkbox':
|
||||
return inputTypes.CHECK;
|
||||
case 'device':
|
||||
return inputTypes.DEVICE;
|
||||
case 'password':
|
||||
return inputTypes.PASSWORD;
|
||||
case 'number':
|
||||
|
@ -20,6 +22,8 @@ function getType(type) {
|
|||
return inputTypes.PATH;
|
||||
case 'select':
|
||||
return inputTypes.SELECT;
|
||||
case 'tag':
|
||||
return inputTypes.TEXT_TAG;
|
||||
case 'textbox':
|
||||
return inputTypes.TEXT;
|
||||
case 'oauth':
|
||||
|
|
|
@ -54,7 +54,7 @@ class TagInput extends Component {
|
|||
return value.length >= this.props.minQueryLength;
|
||||
}
|
||||
|
||||
renderSuggestion({ name }, { query }) {
|
||||
renderSuggestion({ name }) {
|
||||
return name;
|
||||
}
|
||||
|
||||
|
@ -202,6 +202,8 @@ class TagInput extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
inputClassName,
|
||||
placeholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
|
@ -214,7 +216,7 @@ class TagInput extends Component {
|
|||
} = this.state;
|
||||
|
||||
const inputProps = {
|
||||
className: styles.input,
|
||||
className: inputClassName,
|
||||
name,
|
||||
value,
|
||||
placeholder,
|
||||
|
@ -228,7 +230,7 @@ class TagInput extends Component {
|
|||
|
||||
const theme = {
|
||||
container: classNames(
|
||||
styles.inputContainer,
|
||||
className,
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
|
@ -266,6 +268,8 @@ export const tagShape = {
|
|||
};
|
||||
|
||||
TagInput.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
inputClassName: PropTypes.string.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
|
||||
allowNew: PropTypes.bool.isRequired,
|
||||
|
@ -281,6 +285,8 @@ TagInput.propTypes = {
|
|||
};
|
||||
|
||||
TagInput.defaultProps = {
|
||||
className: styles.inputContainer,
|
||||
inputClassName: styles.input,
|
||||
allowNew: true,
|
||||
kind: kinds.INFO,
|
||||
placeholder: '',
|
||||
|
|
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import isString from 'Utilities/String/isString';
|
||||
import split from 'Utilities/String/split';
|
||||
import TagInput from './TagInput';
|
||||
|
||||
|
@ -10,8 +11,11 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(tags) => {
|
||||
const isArray = !isString(tags);
|
||||
const tagsArray = isArray ? tags :split(tags);
|
||||
|
||||
return {
|
||||
tags: split(tags).reduce((result, tag) => {
|
||||
tags: tagsArray.reduce((result, tag) => {
|
||||
if (tag) {
|
||||
result.push({
|
||||
id: tag,
|
||||
|
@ -20,7 +24,8 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
return result;
|
||||
}, [])
|
||||
}, []),
|
||||
isArray
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -35,10 +40,11 @@ class TextTagInputConnector extends Component {
|
|||
const {
|
||||
name,
|
||||
value,
|
||||
isArray,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = split(value);
|
||||
const newValue = isArray ? [...value] : split(value);
|
||||
newValue.push(tag.name);
|
||||
|
||||
onChange({ name, value: newValue.join(',') });
|
||||
|
@ -48,10 +54,11 @@ class TextTagInputConnector extends Component {
|
|||
const {
|
||||
name,
|
||||
value,
|
||||
isArray,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = split(value);
|
||||
const newValue = isArray ? [...value] : split(value);
|
||||
newValue.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
|
@ -77,7 +84,8 @@ class TextTagInputConnector extends Component {
|
|||
|
||||
TextTagInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
||||
isArray: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue