1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-27 22:57:09 -04:00

New: Browse Lists from Discover Movies Page

This commit is contained in:
Qstick
2020-09-03 22:50:56 -04:00
committed by GitHub
parent 54bcf9eed3
commit 361ffe353d
308 changed files with 4824 additions and 3169 deletions
@@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import EditImportExclusionModalContentConnector from './EditImportExclusionModalContentConnector';
function EditImportExclusionModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
size={sizes.MEDIUM}
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditImportExclusionModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditImportExclusionModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditImportExclusionModal;
@@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditImportExclusionModal from './EditImportExclusionModal';
function mapStateToProps() {
return {};
}
const mapDispatchToProps = {
clearPendingChanges
};
class EditImportExclusionModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'settings.importExclusions' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditImportExclusionModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditImportExclusionModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(mapStateToProps, mapDispatchToProps)(EditImportExclusionModalConnector);
@@ -0,0 +1,11 @@
.body {
composes: modalBody from '~Components/Modal/ModalBody.css';
flex: 1 1 430px;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
@@ -0,0 +1,144 @@
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 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 './EditImportExclusionModalContent.css';
function EditImportExclusionModalContent(props) {
const {
id,
isFetching,
error,
isSaving,
saveError,
item,
onInputChange,
onSavePress,
onModalClose,
onDeleteImportExclusionPress,
...otherProps
} = props;
const {
movieTitle = '',
tmdbId,
movieYear
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{id ? 'Edit List Exclusion' : 'Add List Exclusion'}
</ModalHeader>
<ModalBody className={styles.body}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to add a new list exclusion, please try again.</div>
}
{
!isFetching && !error &&
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('TMDBId')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="tmdbId"
helpText={translate('TmdbIdHelpText')}
{...tmdbId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MovieTitle')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="movieTitle"
helpText={translate('MovieTitleHelpText')}
{...movieTitle}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MovieYear')}</FormLabel>
<FormInputGroup
type={inputTypes.NUMBER}
name="movieYear"
helpText={translate('MovieYearHelpText')}
{...movieYear}
onChange={onInputChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
{
id &&
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteImportExclusionPress}
>
{translate('Delete')}
</Button>
}
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={onSavePress}
>
{translate('Save')}
</SpinnerErrorButton>
</ModalFooter>
</ModalContent>
);
}
EditImportExclusionModalContent.propTypes = {
id: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteImportExclusionPress: PropTypes.func
};
export default EditImportExclusionModalContent;
@@ -0,0 +1,119 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveImportExclusion, setImportExclusionValue } from 'Store/Actions/settingsActions';
import selectSettings from 'Store/Selectors/selectSettings';
import EditImportExclusionModalContent from './EditImportExclusionModalContent';
const newImportExclusion = {
movieTitle: '',
tmdbId: 0,
movieYear: 0
};
function createImportExclusionSelector() {
return createSelector(
(state, { id }) => id,
(state) => state.settings.importExclusions,
(id, importExclusions) => {
const {
isFetching,
error,
isSaving,
saveError,
pendingChanges,
items
} = importExclusions;
const mapping = id ? _.find(items, { id }) : newImportExclusion;
const settings = selectSettings(mapping, pendingChanges, saveError);
return {
id,
isFetching,
error,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
function createMapStateToProps() {
return createSelector(
createImportExclusionSelector(),
(importExclusion) => {
return {
...importExclusion
};
}
);
}
const mapDispatchToProps = {
setImportExclusionValue,
saveImportExclusion
};
class EditImportExclusionModalContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.id) {
Object.keys(newImportExclusion).forEach((name) => {
this.props.setImportExclusionValue({
name,
value: newImportExclusion[name]
});
});
}
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setImportExclusionValue({ name, value });
}
onSavePress = () => {
this.props.saveImportExclusion({ id: this.props.id });
}
//
// Render
render() {
return (
<EditImportExclusionModalContent
{...this.props}
onSavePress={this.onSavePress}
onInputChange={this.onInputChange}
/>
);
}
}
EditImportExclusionModalContentConnector.propTypes = {
id: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
item: PropTypes.object.isRequired,
setImportExclusionValue: PropTypes.func.isRequired,
saveImportExclusion: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditImportExclusionModalContentConnector);
@@ -0,0 +1,24 @@
.importExclusion {
display: flex;
align-items: stretch;
margin-bottom: 10px;
height: 30px;
border-bottom: 1px solid $borderColor;
line-height: 30px;
}
.movieTitle {
flex: 0 0 400px;
}
.tmdbId,
.movieYear {
flex: 0 0 200px;
}
.actions {
display: flex;
justify-content: flex-end;
flex: 1 0 auto;
padding-right: 10px;
}
@@ -0,0 +1,115 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditImportExclusionModalConnector from './EditImportExclusionModalConnector';
import styles from './ImportExclusion.css';
class ImportExclusion extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditImportExclusionModalOpen: false,
isDeleteImportExclusionModalOpen: false
};
}
//
// Listeners
onEditImportExclusionPress = () => {
this.setState({ isEditImportExclusionModalOpen: true });
}
onEditImportExclusionModalClose = () => {
this.setState({ isEditImportExclusionModalOpen: false });
}
onDeleteImportExclusionPress = () => {
this.setState({
isEditImportExclusionModalOpen: false,
isDeleteImportExclusionModalOpen: true
});
}
onDeleteImportExclusionModalClose = () => {
this.setState({ isDeleteImportExclusionModalOpen: false });
}
onConfirmDeleteImportExclusion = () => {
this.props.onConfirmDeleteImportExclusion(this.props.id);
}
//
// Render
render() {
const {
id,
movieTitle,
tmdbId,
movieYear
} = this.props;
return (
<div
className={classNames(
styles.importExclusion
)}
>
<div className={styles.tmdbId}>{tmdbId}</div>
<div className={styles.movieTitle}>{movieTitle}</div>
<div className={styles.movieYear}>{movieYear}</div>
<div className={styles.actions}>
<Link
onPress={this.onEditImportExclusionPress}
>
<Icon name={icons.EDIT} />
</Link>
</div>
<EditImportExclusionModalConnector
id={id}
isOpen={this.state.isEditImportExclusionModalOpen}
onModalClose={this.onEditImportExclusionModalClose}
onDeleteImportExclusionPress={this.onDeleteImportExclusionPress}
/>
<ConfirmModal
isOpen={this.state.isDeleteImportExclusionModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportListExclusion')}
message={translate('AreYouSureYouWantToDeleteThisImportListExclusion')}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteImportExclusion}
onCancel={this.onDeleteImportExclusionModalClose}
/>
</div>
);
}
}
ImportExclusion.propTypes = {
id: PropTypes.number.isRequired,
movieTitle: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
movieYear: PropTypes.number.isRequired,
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
};
ImportExclusion.defaultProps = {
// The drag preview will not connect the drag handle.
connectDragSource: (node) => node
};
export default ImportExclusion;
@@ -0,0 +1,24 @@
.importExclusionsHeader {
display: flex;
margin-bottom: 10px;
font-weight: bold;
}
.title {
flex: 0 0 400px;
}
.tmdbId,
.movieYear {
flex: 0 0 200px;
}
.addImportExclusion {
display: flex;
justify-content: flex-end;
padding-right: 10px;
}
.addButton {
text-align: center;
}
@@ -0,0 +1,102 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import PageSectionContent from 'Components/Page/PageSectionContent';
import { icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import EditImportExclusionModalConnector from './EditImportExclusionModalConnector';
import ImportExclusion from './ImportExclusion';
import styles from './ImportExclusions.css';
class ImportExclusions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddImportExclusionModalOpen: false
};
}
//
// Listeners
onAddImportExclusionPress = () => {
this.setState({ isAddImportExclusionModalOpen: true });
}
onModalClose = () => {
this.setState({ isAddImportExclusionModalOpen: false });
}
//
// Render
render() {
const {
items,
onConfirmDeleteImportExclusion,
...otherProps
} = this.props;
return (
<FieldSet legend={translate('ListExclusions')}>
<PageSectionContent
errorMessage={translate('UnableToLoadListExclusions')}
{...otherProps}
>
<div className={styles.importExclusionsHeader}>
<div className={styles.tmdbId}>TMDB Id</div>
<div className={styles.title}>Title</div>
<div className={styles.movieYear}>Year</div>
</div>
<div>
{
items.map((item, index) => {
return (
<ImportExclusion
key={item.id}
{...item}
{...otherProps}
index={index}
onConfirmDeleteImportExclusion={onConfirmDeleteImportExclusion}
/>
);
})
}
</div>
<div className={styles.addImportExclusion}>
<Link
className={styles.addButton}
onPress={this.onAddImportExclusionPress}
>
<Icon name={icons.ADD} />
</Link>
</div>
<EditImportExclusionModalConnector
isOpen={this.state.isAddImportExclusionModalOpen}
onModalClose={this.onModalClose}
/>
</PageSectionContent>
</FieldSet>
);
}
}
ImportExclusions.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteImportExclusion: PropTypes.func.isRequired
};
export default ImportExclusions;
@@ -0,0 +1,59 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteImportExclusion, fetchImportExclusions } from 'Store/Actions/settingsActions';
import ImportExclusions from './ImportExclusions';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.importExclusions,
(importExclusions) => {
return {
...importExclusions
};
}
);
}
const mapDispatchToProps = {
fetchImportExclusions,
deleteImportExclusion
};
class ImportExclusionsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchImportExclusions();
}
//
// Listeners
onConfirmDeleteImportExclusion = (id) => {
this.props.deleteImportExclusion({ id });
}
//
// Render
render() {
return (
<ImportExclusions
{...this.state}
{...this.props}
onConfirmDeleteImportExclusion={this.onConfirmDeleteImportExclusion}
/>
);
}
}
ImportExclusionsConnector.propTypes = {
fetchImportExclusions: PropTypes.func.isRequired,
deleteImportExclusion: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportExclusionsConnector);