Cleanup unused frontend components

This commit is contained in:
Qstick
2021-04-25 10:02:18 -04:00
parent 6dc475cf53
commit 2b6b17707d
110 changed files with 102 additions and 2602 deletions

View File

@@ -1,5 +0,0 @@
.modal {
composes: modalBody from '~Components/Modal/Modal.css';
height: 90%;
}

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
import styles from './AddIndexerModal.css';
function AddIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
className={styles.modal}
>
<AddIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModal;

View File

@@ -1,30 +0,0 @@
.indexers {
display: flex;
justify-content: center;
flex-wrap: wrap;
}
.modalBody {
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
.filterInput {
composes: input from '~Components/Form/TextInput.css';
flex: 0 0 auto;
margin-bottom: 20px;
}
.alert {
composes: alert from '~Components/Alert.css';
margin-bottom: 20px;
}
.scroller {
flex: 1 1 auto;
}

View File

@@ -1,174 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Scroller from 'Components/Scroller/Scroller';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds, scrollDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import SelectIndexerRow from './SelectIndexerRow';
import styles from './AddIndexerModalContent.css';
const columns = [
{
name: 'protocol',
label: 'Protocol',
isSortable: true,
isVisible: true
},
{
name: 'name',
label: 'Name',
isSortable: true,
isVisible: true
},
{
name: 'privacy',
label: 'Privacy',
isSortable: true,
isVisible: true
}
];
class AddIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
filter: ''
};
}
//
// Listeners
onFilterChange = ({ value }) => {
this.setState({ filter: value });
}
//
// Render
render() {
const {
indexers,
onIndexerSelect,
sortKey,
sortDirection,
isFetching,
isPopulated,
error,
onSortPress,
onModalClose
} = this.props;
const filter = this.state.filter;
const filterLower = filter.toLowerCase();
const errorMessage = getErrorMessage(error, 'Unable to load indexers');
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add Indexer
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder={translate('FilterPlaceHolder')}
name="filter"
value={filter}
autoFocus={true}
onChange={this.onFilterChange}
/>
<Alert
kind={kinds.INFO}
className={styles.alert}
>
<div>
{translate('ProwlarrSupportsAnyIndexer')}
</div>
</Alert>
<Scroller
className={styles.scroller}
autoFocus={false}
>
{
isFetching ? <LoadingIndicator /> : null
}
{
error ? <div>{errorMessage}</div> : null
}
{
isPopulated && !!indexers.length ?
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
>
<TableBody>
{
indexers.map((indexer) => {
return indexer.name.toLowerCase().includes(filterLower) ?
(
<SelectIndexerRow
key={indexer.name}
implementation={indexer.implementation}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
) :
null;
})
}
</TableBody>
</Table> :
null
}
</Scroller>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddIndexerModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
sortKey: PropTypes.string,
sortDirection: PropTypes.string,
onSortPress: PropTypes.func.isRequired,
indexers: PropTypes.arrayOf(PropTypes.object).isRequired,
onIndexerSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddIndexerModalContent;

View File

@@ -1,82 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import AddIndexerModalContent from './AddIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('indexers.schema'),
(indexers) => {
const {
isFetching,
isPopulated,
error,
items,
sortDirection,
sortKey
} = indexers;
return {
isFetching,
isPopulated,
error,
indexers: items,
sortKey,
sortDirection
};
}
);
}
const mapDispatchToProps = {
fetchIndexerSchema,
selectIndexerSchema,
setIndexerSchemaSort
};
class AddIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchIndexerSchema();
}
//
// Listeners
onIndexerSelect = ({ implementation, name }) => {
this.props.selectIndexerSchema({ implementation, name });
this.props.onModalClose({ indexerSelected: true });
}
onSortPress = (sortKey, sortDirection) => {
this.props.setIndexerSchemaSort({ sortKey, sortDirection });
}
//
// Render
render() {
return (
<AddIndexerModalContent
{...this.props}
onSortPress={this.onSortPress}
onIndexerSelect={this.onIndexerSelect}
/>
);
}
}
AddIndexerModalContentConnector.propTypes = {
fetchIndexerSchema: PropTypes.func.isRequired,
selectIndexerSchema: PropTypes.func.isRequired,
setIndexerSchemaSort: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddIndexerModalContentConnector);

View File

@@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MenuItem from 'Components/Menu/MenuItem';
class AddIndexerPresetMenuItem extends Component {
//
// Listeners
onPress = () => {
const {
name,
implementation
} = this.props;
this.props.onPress({
name,
implementation
});
}
//
// Render
render() {
const {
name,
implementation,
...otherProps
} = this.props;
return (
<MenuItem
{...otherProps}
onPress={this.onPress}
>
{name}
</MenuItem>
);
}
}
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};
export default AddIndexerPresetMenuItem;

View File

@@ -1,27 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditIndexerModalContentConnector from './EditIndexerModalContentConnector';
function EditIndexerModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditIndexerModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditIndexerModal;

View File

@@ -1,65 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { cancelSaveIndexer, cancelTestIndexer } from 'Store/Actions/indexerActions';
import EditIndexerModal from './EditIndexerModal';
function createMapDispatchToProps(dispatch, props) {
const section = 'indexers';
return {
dispatchClearPendingChanges() {
dispatch(clearPendingChanges({ section }));
},
dispatchCancelTestIndexer() {
dispatch(cancelTestIndexer({ section }));
},
dispatchCancelSaveIndexer() {
dispatch(cancelSaveIndexer({ section }));
}
};
}
class EditIndexerModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.dispatchClearPendingChanges();
this.props.dispatchCancelTestIndexer();
this.props.dispatchCancelSaveIndexer();
this.props.onModalClose();
}
//
// Render
render() {
const {
dispatchClearPendingChanges,
dispatchCancelTestIndexer,
dispatchCancelSaveIndexer,
...otherProps
} = this.props;
return (
<EditIndexerModal
{...otherProps}
onModalClose={this.onModalClose}
/>
);
}
}
EditIndexerModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
dispatchClearPendingChanges: PropTypes.func.isRequired,
dispatchCancelTestIndexer: PropTypes.func.isRequired,
dispatchCancelSaveIndexer: PropTypes.func.isRequired
};
export default connect(null, createMapDispatchToProps)(EditIndexerModalConnector);

View File

@@ -1,5 +0,0 @@
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View File

@@ -1,195 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
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 ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditIndexerModalContent.css';
function EditIndexerModalContent(props) {
const {
advancedSettings,
isFetching,
error,
isSaving,
isTesting,
saveError,
item,
onInputChange,
onFieldChange,
onModalClose,
onSavePress,
onTestPress,
onDeleteIndexerPress,
...otherProps
} = props;
const {
id,
implementationName,
name,
enable,
redirect,
supportsRss,
supportsRedirect,
fields,
priority
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${implementationName}`}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToAddANewIndexerPleaseTryAgain')}
</div>
}
{
!isFetching && !error &&
<Form {...otherProps}>
<FormGroup>
<FormLabel>{translate('Name')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="name"
{...name}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Enable')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enable"
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
isDisabled={!supportsRss.value}
{...enable}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Redirect')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="redirect"
helpText={'Redirect incoming download requests for indexer instead of Proxying using Prowlarr'}
isDisabled={!supportsRedirect.value}
{...redirect}
onChange={onInputChange}
/>
</FormGroup>
{
fields.map((field) => {
return (
<ProviderFieldFormGroup
key={field.name}
advancedSettings={advancedSettings}
provider="indexer"
providerData={item}
{...field}
onChange={onFieldChange}
/>
);
})
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('IndexerPriority')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="priority"
helpText={translate('IndexerPriorityHelpText')}
min={1}
max={50}
{...priority}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
}
<SpinnerErrorButton
isSpinning={isTesting}
error={saveError}
onPress={onTestPress}
>
{translate('Test')}
</SpinnerErrorButton>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditIndexerModalContent.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
isTesting: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onFieldChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onTestPress: PropTypes.func.isRequired,
onDeleteIndexerPress: PropTypes.func
};
export default EditIndexerModalContent;

View File

@@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveIndexer, setIndexerFieldValue, setIndexerValue, testIndexer } from 'Store/Actions/indexerActions';
import createIndexerSchemaSelector from 'Store/Selectors/createIndexerSchemaSelector';
import EditIndexerModalContent from './EditIndexerModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
createIndexerSchemaSelector(),
(advancedSettings, indexer) => {
return {
advancedSettings,
...indexer
};
}
);
}
const mapDispatchToProps = {
setIndexerValue,
setIndexerFieldValue,
saveIndexer,
testIndexer
};
class EditIndexerModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setIndexerValue({ name, value });
}
onFieldChange = ({ name, value }) => {
this.props.setIndexerFieldValue({ name, value });
}
onSavePress = () => {
this.props.saveIndexer({ id: this.props.id });
}
onTestPress = () => {
this.props.testIndexer({ id: this.props.id });
}
//
// Render
render() {
return (
<EditIndexerModalContent
{...this.props}
onSavePress={this.onSavePress}
onTestPress={this.onTestPress}
onInputChange={this.onInputChange}
onFieldChange={this.onFieldChange}
/>
);
}
}
EditIndexerModalContentConnector.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setIndexerValue: PropTypes.func.isRequired,
setIndexerFieldValue: PropTypes.func.isRequired,
saveIndexer: PropTypes.func.isRequired,
testIndexer: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditIndexerModalContentConnector);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -1,5 +0,0 @@
.protocol {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 32px;
}

View File

@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
import styles from './SelectIndexerRow.css';
class SelectIndexerRow extends Component {
//
// Listeners
onPress = () => {
const {
implementation,
name
} = this.props;
this.props.onIndexerSelect({ implementation, name });
}
//
// Render
render() {
const {
protocol,
privacy,
name
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell className={styles.protocol}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
<TableRowCell>
{name}
</TableRowCell>
<TableRowCell>
{privacy}
</TableRowCell>
</TableRowButton>
);
}
}
SelectIndexerRow.propTypes = {
name: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
privacy: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired
};
export default SelectIndexerRow;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;