parent
cfb517a90f
commit
9531bec8c2
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
|
||||||
|
|
||||||
const protocols = [
|
|
||||||
{ id: 'torrent', name: 'Torrent' },
|
|
||||||
{ id: 'usenet', name: 'Usenet' }
|
|
||||||
];
|
|
||||||
|
|
||||||
function ProtocolFilterBuilderRowValue(props) {
|
|
||||||
return (
|
|
||||||
<FilterBuilderRowValue
|
|
||||||
tagList={protocols}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ProtocolFilterBuilderRowValue;
|
|
@ -0,0 +1,30 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.downloadClients,
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(downloadClients, indexers) => {
|
||||||
|
const protocols = Array.from(new Set([
|
||||||
|
...downloadClients.items.map((i) => i.protocol),
|
||||||
|
...indexers.items.map((i) => i.protocol)
|
||||||
|
]));
|
||||||
|
|
||||||
|
console.log(protocols);
|
||||||
|
const tagList = protocols.map((protocol) => {
|
||||||
|
return {
|
||||||
|
id: protocol,
|
||||||
|
name: protocol.replace('DownloadProtocol', '')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagList
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps)(FilterBuilderRowValue);
|
@ -1,3 +1,4 @@
|
|||||||
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
export const QUALITY_PROFILE_ITEM = 'qualityProfileItem';
|
||||||
export const DELAY_PROFILE = 'delayProfile';
|
export const DELAY_PROFILE = 'delayProfile';
|
||||||
|
export const DOWNLOAD_PROTOCOL_ITEM = 'downloadProtocolItem';
|
||||||
export const TABLE_COLUMN = 'tableColumn';
|
export const TABLE_COLUMN = 'tableColumn';
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
|
||||||
|
function getDelay(item) {
|
||||||
|
if (!item.allowed) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.delay) {
|
||||||
|
return 'No Delay';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.delay === 1) {
|
||||||
|
return '1 Minute';
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: use better units of time than just minutes
|
||||||
|
return `${item.delay} Minutes`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DelayProfileItem(props) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
allowed
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
kind={allowed ? kinds.INFO : kinds.DANGER}
|
||||||
|
>
|
||||||
|
{name}: {getDelay(props)}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
DelayProfileItem.propTypes = {
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DelayProfileItem;
|
@ -1,13 +1,14 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
'addButton': string;
|
'addButton': string;
|
||||||
'addDelayProfile': string;
|
'addDelayProfile': string;
|
||||||
'column': string;
|
|
||||||
'delayProfiles': string;
|
'delayProfiles': string;
|
||||||
'delayProfilesHeader': string;
|
'delayProfilesHeader': string;
|
||||||
|
'fillcolumn': string;
|
||||||
'horizontalScroll': string;
|
'horizontalScroll': string;
|
||||||
'tags': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
@ -0,0 +1,82 @@
|
|||||||
|
.qualityProfileItem {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
&.isInGroup {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInputContainer {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInput {
|
||||||
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delayContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delayInput {
|
||||||
|
composes: input from '~Components/Form/Input.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
height: 30px;
|
||||||
|
border: unset;
|
||||||
|
border-radius: unset;
|
||||||
|
background-color: unset;
|
||||||
|
box-shadow: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: $qualityProfileItemHeight;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityName {
|
||||||
|
&.notAllowed {
|
||||||
|
color: #c6c6c6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
width: $dragHandleWidth;
|
||||||
|
text-align: center;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragIcon {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDragging {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isPreview {
|
||||||
|
.qualityName {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'checkInput': string;
|
||||||
|
'checkInputContainer': string;
|
||||||
|
'delayContainer': string;
|
||||||
|
'delayInput': string;
|
||||||
|
'dragHandle': string;
|
||||||
|
'dragIcon': string;
|
||||||
|
'isDragging': string;
|
||||||
|
'isInGroup': string;
|
||||||
|
'isPreview': string;
|
||||||
|
'notAllowed': string;
|
||||||
|
'qualityName': string;
|
||||||
|
'qualityNameContainer': string;
|
||||||
|
'qualityProfileItem': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,113 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import NumberInput from 'Components/Form/NumberInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './DownloadProtocolItem.css';
|
||||||
|
|
||||||
|
class DownloadProtocolItem extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChange = ({ name, value }) => {
|
||||||
|
const {
|
||||||
|
protocol,
|
||||||
|
onDownloadProtocolItemFieldChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onDownloadProtocolItemFieldChange(protocol, name, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isPreview,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
delay,
|
||||||
|
isDragging,
|
||||||
|
isOverCurrent,
|
||||||
|
connectDragSource
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItem,
|
||||||
|
isDragging && styles.isDragging,
|
||||||
|
isPreview && styles.isPreview,
|
||||||
|
isOverCurrent && styles.isOverCurrent
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
className={styles.qualityNameContainer}
|
||||||
|
>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name={'allowed'}
|
||||||
|
value={allowed}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={classNames(
|
||||||
|
styles.qualityName,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<NumberInput
|
||||||
|
containerClassName={styles.delayContainer}
|
||||||
|
className={styles.delayInput}
|
||||||
|
name={'delay'}
|
||||||
|
value={delay}
|
||||||
|
min={0}
|
||||||
|
max={9999999}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<div className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
title="Create group"
|
||||||
|
name={icons.REORDER}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProtocolItem.propTypes = {
|
||||||
|
isPreview: PropTypes.bool,
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
delay: PropTypes.number.isRequired,
|
||||||
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
isOverCurrent: PropTypes.bool.isRequired,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
onDownloadProtocolItemFieldChange: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadProtocolItem.defaultProps = {
|
||||||
|
isPreview: false,
|
||||||
|
isOverCurrent: false,
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadProtocolItem;
|
@ -0,0 +1,4 @@
|
|||||||
|
.dragPreview {
|
||||||
|
width: 480px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'dragPreview': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,89 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { DragLayer } from 'react-dnd';
|
||||||
|
import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||||
|
import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions.js';
|
||||||
|
import DownloadProtocolItem from './DownloadProtocolItem';
|
||||||
|
import styles from './DownloadProtocolItemDragPreview.css';
|
||||||
|
|
||||||
|
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||||
|
const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
|
||||||
|
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||||
|
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||||
|
|
||||||
|
function collectDragLayer(monitor) {
|
||||||
|
return {
|
||||||
|
item: monitor.getItem(),
|
||||||
|
itemType: monitor.getItemType(),
|
||||||
|
currentOffset: monitor.getSourceClientOffset()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadProtocolItemDragPreview extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
item,
|
||||||
|
itemType,
|
||||||
|
currentOffset
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!currentOffset || itemType !== DOWNLOAD_PROTOCOL_ITEM) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The offset is shifted because the drag handle is on the right edge of the
|
||||||
|
// list item and the preview is wider than the drag handle.
|
||||||
|
|
||||||
|
const { x, y } = currentOffset;
|
||||||
|
const handleOffset = formGroupSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||||
|
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
position: 'absolute',
|
||||||
|
WebkitTransform: transform,
|
||||||
|
msTransform: transform,
|
||||||
|
transform
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
delay
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DragPreviewLayer>
|
||||||
|
<div
|
||||||
|
className={styles.dragPreview}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<DownloadProtocolItem
|
||||||
|
isPreview={true}
|
||||||
|
id={id}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
delay={delay}
|
||||||
|
isDragging={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DragPreviewLayer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProtocolItemDragPreview.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
itemType: PropTypes.string,
|
||||||
|
currentOffset: PropTypes.shape({
|
||||||
|
x: PropTypes.number.isRequired,
|
||||||
|
y: PropTypes.number.isRequired
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DragLayer(collectDragLayer)(DownloadProtocolItemDragPreview);
|
@ -0,0 +1,18 @@
|
|||||||
|
.downloadProtocolItemDragSource {
|
||||||
|
padding: $qualityProfileItemDragSourcePadding 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadProtocolItemPlaceholder {
|
||||||
|
width: 100%;
|
||||||
|
height: $qualityProfileItemHeight;
|
||||||
|
border: 1px dotted #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadProtocolItemPlaceholderBefore {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.downloadProtocolItemPlaceholderAfter {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'downloadProtocolItemDragSource': string;
|
||||||
|
'downloadProtocolItemPlaceholder': string;
|
||||||
|
'downloadProtocolItemPlaceholderAfter': string;
|
||||||
|
'downloadProtocolItemPlaceholderBefore': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,188 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { DragSource, DropTarget } from 'react-dnd';
|
||||||
|
import { findDOMNode } from 'react-dom';
|
||||||
|
import { DOWNLOAD_PROTOCOL_ITEM } from 'Helpers/dragTypes';
|
||||||
|
import DownloadProtocolItem from './DownloadProtocolItem';
|
||||||
|
import styles from './DownloadProtocolItemDragSource.css';
|
||||||
|
|
||||||
|
const downloadProtocolItemDragSource = {
|
||||||
|
beginDrag(props) {
|
||||||
|
const {
|
||||||
|
index,
|
||||||
|
protocol,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
delay
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
index,
|
||||||
|
protocol,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
delay
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
endDrag(props, monitor, component) {
|
||||||
|
props.onDownloadProtocolItemDragEnd(monitor.didDrop());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadProtocolItemDropTarget = {
|
||||||
|
hover(props, monitor, component) {
|
||||||
|
const {
|
||||||
|
index: dragIndex
|
||||||
|
} = monitor.getItem();
|
||||||
|
|
||||||
|
const dropIndex = props.index;
|
||||||
|
|
||||||
|
// Use childNodeIndex to select the correct node to get the middle of so
|
||||||
|
// we don't bounce between above and below causing rapid setState calls.
|
||||||
|
const childNodeIndex = component.props.isOverCurrent && component.props.isDraggingUp ? 1 :0;
|
||||||
|
const componentDOMNode = findDOMNode(component).children[childNodeIndex];
|
||||||
|
const hoverBoundingRect = componentDOMNode.getBoundingClientRect();
|
||||||
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
|
const clientOffset = monitor.getClientOffset();
|
||||||
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
|
||||||
|
// If we're hovering over a child don't trigger on the parent
|
||||||
|
if (!monitor.isOver({ shallow: true })) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't show targets for dropping on self
|
||||||
|
if (dragIndex === dropIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropPosition = null;
|
||||||
|
|
||||||
|
// Determine drop position based on position over target
|
||||||
|
if (hoverClientY > hoverMiddleY) {
|
||||||
|
dropPosition = 'below';
|
||||||
|
} else if (hoverClientY < hoverMiddleY) {
|
||||||
|
dropPosition = 'above';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onDownloadProtocolItemDragMove({
|
||||||
|
dragIndex,
|
||||||
|
dropIndex,
|
||||||
|
dropPosition
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function collectDragSource(connect, monitor) {
|
||||||
|
return {
|
||||||
|
connectDragSource: connect.dragSource(),
|
||||||
|
isDragging: monitor.isDragging()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectDropTarget(connect, monitor) {
|
||||||
|
return {
|
||||||
|
connectDropTarget: connect.dropTarget(),
|
||||||
|
isOver: monitor.isOver(),
|
||||||
|
isOverCurrent: monitor.isOver({ shallow: true })
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadProtocolItemDragSource extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
protocol,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
delay,
|
||||||
|
index,
|
||||||
|
isDragging,
|
||||||
|
isDraggingUp,
|
||||||
|
isDraggingDown,
|
||||||
|
isOverCurrent,
|
||||||
|
connectDragSource,
|
||||||
|
connectDropTarget,
|
||||||
|
onDownloadProtocolItemFieldChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||||
|
const isAfter = !isDragging && isDraggingDown && isOverCurrent;
|
||||||
|
|
||||||
|
return connectDropTarget(
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.downloadProtocolItemDragSource,
|
||||||
|
isBefore && styles.isDraggingUp,
|
||||||
|
isAfter && styles.isDraggingDown
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isBefore &&
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.downloadProtocolItemPlaceholder,
|
||||||
|
styles.downloadProtocolItemPlaceholderBefore
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<DownloadProtocolItem
|
||||||
|
protocol={protocol}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
delay={delay}
|
||||||
|
index={index}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isOverCurrent={isOverCurrent}
|
||||||
|
connectDragSource={connectDragSource}
|
||||||
|
onDownloadProtocolItemFieldChange={onDownloadProtocolItemFieldChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
isAfter &&
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.downloadProtocolItemPlaceholder,
|
||||||
|
styles.downloadProtocolItemPlaceholderAfter
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProtocolItemDragSource.propTypes = {
|
||||||
|
protocol: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
delay: PropTypes.number.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
|
isDragging: PropTypes.bool,
|
||||||
|
isDraggingUp: PropTypes.bool,
|
||||||
|
isDraggingDown: PropTypes.bool,
|
||||||
|
isOverCurrent: PropTypes.bool,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
connectDropTarget: PropTypes.func,
|
||||||
|
onDownloadProtocolItemFieldChange: PropTypes.func.isRequired,
|
||||||
|
onDownloadProtocolItemDragMove: PropTypes.func.isRequired,
|
||||||
|
onDownloadProtocolItemDragEnd: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DropTarget(
|
||||||
|
DOWNLOAD_PROTOCOL_ITEM,
|
||||||
|
downloadProtocolItemDropTarget,
|
||||||
|
collectDropTarget
|
||||||
|
)(DragSource(
|
||||||
|
DOWNLOAD_PROTOCOL_ITEM,
|
||||||
|
downloadProtocolItemDragSource,
|
||||||
|
collectDragSource
|
||||||
|
)(DownloadProtocolItemDragSource));
|
@ -0,0 +1,24 @@
|
|||||||
|
.qualities {
|
||||||
|
margin-top: 10px;
|
||||||
|
transition: min-height 200ms;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerContainer {
|
||||||
|
display: flex;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerTitle {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerDelay {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 0;
|
||||||
|
margin-right: 40px;
|
||||||
|
padding-left: 16px;
|
||||||
|
width: 150px;
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'headerContainer': string;
|
||||||
|
'headerDelay': string;
|
||||||
|
'headerTitle': string;
|
||||||
|
'qualities': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,150 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Measure from 'Components/Measure';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import DownloadProtocolItemDragPreview from './DownloadProtocolItemDragPreview';
|
||||||
|
import DownloadProtocolItemDragSource from './DownloadProtocolItemDragSource';
|
||||||
|
import styles from './DownloadProtocolItems.css';
|
||||||
|
|
||||||
|
class DownloadProtocolItems extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ height }) => {
|
||||||
|
this.setState({ height });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
dropIndex,
|
||||||
|
dropPosition,
|
||||||
|
items,
|
||||||
|
errors,
|
||||||
|
warnings,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
height
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const isDragging = dropIndex !== null;
|
||||||
|
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||||
|
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup size={sizes.SMALL}>
|
||||||
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
Download Protocols
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<FormInputHelpText
|
||||||
|
text="Protocols higher in the list are more preferred. Only checked protocols are allowed"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
errors.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={error.message}
|
||||||
|
isError={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
warnings.map((warning, index) => {
|
||||||
|
return (
|
||||||
|
<FormInputHelpText
|
||||||
|
key={index}
|
||||||
|
text={warning.message}
|
||||||
|
isWarning={true}
|
||||||
|
isCheckInput={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<Measure
|
||||||
|
whitelist={['height']}
|
||||||
|
includeMargin={false}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.qualities}
|
||||||
|
style={{ minHeight: `${height}px` }}
|
||||||
|
>
|
||||||
|
<div className={styles.headerContainer}>
|
||||||
|
<div className={styles.headerTitle}>
|
||||||
|
Protocol
|
||||||
|
</div>
|
||||||
|
<div className={styles.headerDelay}>
|
||||||
|
Delay (minutes)
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
items.map(({ protocol, name, allowed, delay }, index) => {
|
||||||
|
return (
|
||||||
|
<DownloadProtocolItemDragSource
|
||||||
|
key={protocol}
|
||||||
|
protocol={protocol}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
delay={delay}
|
||||||
|
index={index}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<DownloadProtocolItemDragPreview />
|
||||||
|
</div>
|
||||||
|
</Measure>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadProtocolItems.propTypes = {
|
||||||
|
dragIndex: PropTypes.number,
|
||||||
|
dropIndex: PropTypes.number,
|
||||||
|
dropPosition: PropTypes.string,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
warnings: PropTypes.arrayOf(PropTypes.object)
|
||||||
|
};
|
||||||
|
|
||||||
|
DownloadProtocolItems.defaultProps = {
|
||||||
|
errors: [],
|
||||||
|
warnings: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DownloadProtocolItems;
|
@ -0,0 +1,15 @@
|
|||||||
|
.delayProfile {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 0 0 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'delayProfile': string;
|
||||||
|
'name': string;
|
||||||
|
'tags': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,11 @@
|
|||||||
|
.version {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 150px;
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
|
'version': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
@ -0,0 +1,79 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import styles from './PluginRow.css';
|
||||||
|
|
||||||
|
class PluginRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInstallPluginPress = () => {
|
||||||
|
this.props.onInstallPluginPress(this.props.githubUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUninstallPluginPress = () => {
|
||||||
|
this.props.onUninstallPluginPress(this.props.githubUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
installedVersion,
|
||||||
|
availableVersion,
|
||||||
|
updateAvailable,
|
||||||
|
isInstallingPlugin,
|
||||||
|
isUninstallingPlugin
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableRowCell>{name}</TableRowCell>
|
||||||
|
<TableRowCell>{owner}</TableRowCell>
|
||||||
|
<TableRowCell className={styles.version}>{installedVersion}</TableRowCell>
|
||||||
|
<TableRowCell className={styles.version}>{availableVersion}</TableRowCell>
|
||||||
|
<TableRowCell
|
||||||
|
className={styles.actions}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
updateAvailable &&
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.UPDATE}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
isSpinning={isInstallingPlugin}
|
||||||
|
onPress={this.onInstallPluginPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.DELETE}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
isSpinning={isUninstallingPlugin}
|
||||||
|
onPress={this.onUninstallPluginPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginRow.propTypes = {
|
||||||
|
githubUrl: PropTypes.string.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
owner: PropTypes.string.isRequired,
|
||||||
|
installedVersion: PropTypes.string.isRequired,
|
||||||
|
availableVersion: PropTypes.string.isRequired,
|
||||||
|
updateAvailable: PropTypes.bool.isRequired,
|
||||||
|
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||||
|
onInstallPluginPress: PropTypes.func.isRequired,
|
||||||
|
isUninstallingPlugin: PropTypes.bool.isRequired,
|
||||||
|
onUninstallPluginPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginRow;
|
@ -0,0 +1,6 @@
|
|||||||
|
.loading {
|
||||||
|
composes: loading from '~Components/Loading/LoadingIndicator.css';
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
@ -0,0 +1,168 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import { inputTypes, kinds } from 'Helpers/Props';
|
||||||
|
import PluginRow from './PluginRow';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: 'Name',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'owner',
|
||||||
|
label: 'Owner',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'installedVersion',
|
||||||
|
label: 'Installed Version',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'availableVersion',
|
||||||
|
label: 'Available Version',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
isVisible: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
class Plugins extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
repoUrl: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInputChange = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onInstallPluginPress = () => {
|
||||||
|
this.props.onInstallPluginPress(this.state.repoUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
isInstallingPlugin,
|
||||||
|
onInstallPluginPress,
|
||||||
|
isUninstallingPlugin,
|
||||||
|
onUninstallPluginPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
repoUrl
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const noPlugins = isPopulated && !error && !items.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="Plugins">
|
||||||
|
<PageContentBody>
|
||||||
|
<Form>
|
||||||
|
<FieldSet legend="Install New Plugin">
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>GitHub URL</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="repoUrl"
|
||||||
|
helpText="URL to GitHub repository containing plugin"
|
||||||
|
helpLink="https://wiki.servarr.com/Lidarr_FAQ#How_do_I_install_plugins"
|
||||||
|
value={repoUrl}
|
||||||
|
onChange={this.onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<SpinnerButton
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
isSpinning={isInstallingPlugin}
|
||||||
|
onPress={this.onInstallPluginPress}
|
||||||
|
>
|
||||||
|
Install
|
||||||
|
</SpinnerButton>
|
||||||
|
</FieldSet>
|
||||||
|
</Form>
|
||||||
|
<FieldSet legend="Installed Plugins">
|
||||||
|
{
|
||||||
|
!isPopulated && !error &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && noPlugins &&
|
||||||
|
<div>No plugins are installed</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !noPlugins &&
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((plugin) => {
|
||||||
|
return (
|
||||||
|
<PluginRow
|
||||||
|
key={plugin.githubUrl}
|
||||||
|
{...plugin}
|
||||||
|
isInstallingPlugin={isInstallingPlugin}
|
||||||
|
isUninstallingPlugin={isUninstallingPlugin}
|
||||||
|
onInstallPluginPress={onInstallPluginPress}
|
||||||
|
onUninstallPluginPress={onUninstallPluginPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
</FieldSet>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Plugins.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
isInstallingPlugin: PropTypes.bool.isRequired,
|
||||||
|
onInstallPluginPress: PropTypes.func.isRequired,
|
||||||
|
isUninstallingPlugin: PropTypes.bool.isRequired,
|
||||||
|
onUninstallPluginPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Plugins;
|
@ -0,0 +1,95 @@
|
|||||||
|
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 { fetchInstalledPlugins } from 'Store/Actions/systemActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Plugins from './Plugins';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.system.plugins,
|
||||||
|
createCommandExecutingSelector(commandNames.INSTALL_PLUGIN),
|
||||||
|
createCommandExecutingSelector(commandNames.UNINSTALL_PLUGIN),
|
||||||
|
(
|
||||||
|
plugins,
|
||||||
|
isInstallingPlugin,
|
||||||
|
isUninstallingPlugin
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
...plugins,
|
||||||
|
isInstallingPlugin,
|
||||||
|
isUninstallingPlugin
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchInstalledPlugins: fetchInstalledPlugins,
|
||||||
|
dispatchExecuteCommand: executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class PluginsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
registerPagePopulator(this.repopulate);
|
||||||
|
|
||||||
|
this.repopulate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.dispatchFetchInstalledPlugins();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onInstallPluginPress = (url) => {
|
||||||
|
this.props.dispatchExecuteCommand({
|
||||||
|
name: commandNames.INSTALL_PLUGIN,
|
||||||
|
githubUrl: url
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUninstallPluginPress = (url) => {
|
||||||
|
this.props.dispatchExecuteCommand({
|
||||||
|
name: commandNames.UNINSTALL_PLUGIN,
|
||||||
|
githubUrl: url
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Plugins
|
||||||
|
onInstallPluginPress={this.onInstallPluginPress}
|
||||||
|
onUninstallPluginPress={this.onUninstallPluginPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginsConnector.propTypes = {
|
||||||
|
dispatchFetchInstalledPlugins: PropTypes.func.isRequired,
|
||||||
|
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(PluginsConnector);
|
@ -0,0 +1,47 @@
|
|||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Profiles.Delay
|
||||||
|
{
|
||||||
|
public class DelayProfileProtocolItemResource
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Protocol { get; set; }
|
||||||
|
public bool Allowed { get; set; }
|
||||||
|
public int Delay { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ProfileItemResourceMapper
|
||||||
|
{
|
||||||
|
public static DelayProfileProtocolItemResource ToResource(this DelayProfileProtocolItem model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DelayProfileProtocolItemResource
|
||||||
|
{
|
||||||
|
Name = model.Name,
|
||||||
|
Protocol = model.Protocol,
|
||||||
|
Allowed = model.Allowed,
|
||||||
|
Delay = model.Delay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DelayProfileProtocolItem ToModel(this DelayProfileProtocolItemResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DelayProfileProtocolItem
|
||||||
|
{
|
||||||
|
Name = resource.Name,
|
||||||
|
Protocol = resource.Protocol,
|
||||||
|
Allowed = resource.Allowed,
|
||||||
|
Delay = resource.Delay
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
using Lidarr.Http;
|
||||||
|
using Lidarr.Http.REST;
|
||||||
|
using NzbDrone.Core.Profiles.Delay;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Profiles.Delay
|
||||||
|
{
|
||||||
|
[V1ApiController("delayprofile/schema")]
|
||||||
|
public class DelayProfileSchemaController : RestController<DelayProfileResource>
|
||||||
|
{
|
||||||
|
private readonly IDelayProfileService _profileService;
|
||||||
|
|
||||||
|
public DelayProfileSchemaController(IDelayProfileService profileService)
|
||||||
|
{
|
||||||
|
_profileService = profileService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DelayProfileResource GetResourceById(int id)
|
||||||
|
{
|
||||||
|
return _profileService.GetDefaultProfile().ToResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Lidarr.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Core.Plugins;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.System.Plugins
|
||||||
|
{
|
||||||
|
[V1ApiController("system/plugins")]
|
||||||
|
public class PluginController : Controller
|
||||||
|
{
|
||||||
|
private readonly IPluginService _pluginService;
|
||||||
|
|
||||||
|
public PluginController(IPluginService pluginService)
|
||||||
|
{
|
||||||
|
_pluginService = pluginService;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public List<PluginResource> GetInstalledPlugins()
|
||||||
|
{
|
||||||
|
return _pluginService.GetInstalledPlugins().ToResource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Lidarr.Http.REST;
|
||||||
|
using NzbDrone.Core.Plugins;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.System.Plugins
|
||||||
|
{
|
||||||
|
public class PluginResource : RestResource
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Owner { get; set; }
|
||||||
|
public string GithubUrl { get; set; }
|
||||||
|
public string InstalledVersion { get; set; }
|
||||||
|
public string AvailableVersion { get; set; }
|
||||||
|
public bool UpdateAvailable { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PluginResourceMapper
|
||||||
|
{
|
||||||
|
public static PluginResource ToResource(this IPlugin plugin)
|
||||||
|
{
|
||||||
|
return new PluginResource
|
||||||
|
{
|
||||||
|
Name = plugin.Name,
|
||||||
|
Owner = plugin.Owner,
|
||||||
|
GithubUrl = plugin.GithubUrl,
|
||||||
|
InstalledVersion = plugin.InstalledVersion.ToString(),
|
||||||
|
AvailableVersion = plugin.AvailableVersion.ToString(),
|
||||||
|
UpdateAvailable = plugin.AvailableVersion > plugin.InstalledVersion
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<PluginResource> ToResource(this IEnumerable<IPlugin> plugins)
|
||||||
|
{
|
||||||
|
return plugins.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.Loader;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Composition
|
||||||
|
{
|
||||||
|
public class PluginLoadContext : AssemblyLoadContext
|
||||||
|
{
|
||||||
|
private AssemblyDependencyResolver _resolver;
|
||||||
|
|
||||||
|
public PluginLoadContext(string pluginPath)
|
||||||
|
: base(isCollectible: true)
|
||||||
|
{
|
||||||
|
_resolver = new AssemblyDependencyResolver(pluginPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Assembly Load(AssemblyName assemblyName)
|
||||||
|
{
|
||||||
|
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);
|
||||||
|
if (assemblyPath != null)
|
||||||
|
{
|
||||||
|
using var fs = new FileStream(assemblyPath, FileMode.Open, FileAccess.Read);
|
||||||
|
return LoadFromStream(fs);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
|
||||||
|
{
|
||||||
|
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);
|
||||||
|
if (libraryPath != null)
|
||||||
|
{
|
||||||
|
return LoadUnmanagedDllFromPath(libraryPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Instrumentation;
|
||||||
|
|
||||||
|
namespace NzbDrone.Common.Composition
|
||||||
|
{
|
||||||
|
public static class PluginLoader
|
||||||
|
{
|
||||||
|
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(PluginLoader));
|
||||||
|
|
||||||
|
public static (List<Assembly>, List<WeakReference>) LoadPlugins(IEnumerable<string> pluginPaths)
|
||||||
|
{
|
||||||
|
var assemblies = new List<Assembly>();
|
||||||
|
var pluginRefs = new List<WeakReference>();
|
||||||
|
|
||||||
|
foreach (var pluginPath in pluginPaths)
|
||||||
|
{
|
||||||
|
(var plugin, var pluginRef) = LoadPlugin(pluginPath);
|
||||||
|
pluginRefs.Add(pluginRef);
|
||||||
|
assemblies.Add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (assemblies, pluginRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool UnloadPlugins(List<WeakReference> pluginRefs)
|
||||||
|
{
|
||||||
|
RequestPluginUnload(pluginRefs);
|
||||||
|
return AwaitPluginUnload(pluginRefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||||
|
private static (Assembly, WeakReference) LoadPlugin(string path)
|
||||||
|
{
|
||||||
|
var context = new PluginLoadContext(path);
|
||||||
|
var weakRef = new WeakReference(context, trackResurrection: true);
|
||||||
|
|
||||||
|
// load from stream to avoid locking on windows
|
||||||
|
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||||
|
var assembly = context.LoadFromStream(fs);
|
||||||
|
|
||||||
|
return (assembly, weakRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RequestPluginUnload(List<WeakReference> pluginRefs)
|
||||||
|
{
|
||||||
|
foreach (var pluginRef in pluginRefs)
|
||||||
|
{
|
||||||
|
if (pluginRef?.Target != null)
|
||||||
|
{
|
||||||
|
((PluginLoadContext)pluginRef.Target).Unload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AwaitPluginUnload(List<WeakReference> pluginRefs)
|
||||||
|
{
|
||||||
|
var i = 0;
|
||||||
|
foreach (var pluginRef in pluginRefs.Where(x => x != null))
|
||||||
|
{
|
||||||
|
while (pluginRef.IsAlive)
|
||||||
|
{
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
|
||||||
|
if (i++ >= 10)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
namespace NzbDrone.Common.Composition
|
||||||
|
{
|
||||||
|
public class PluginStatus
|
||||||
|
{
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue