mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-15 09:33:52 -07:00
Initial Commit Rework
This commit is contained in:
parent
74a4cc048c
commit
95051cbd63
2483 changed files with 101351 additions and 111396 deletions
54
frontend/src/AddArtist/AddNewSeries/AddNewSeries.css
Normal file
54
frontend/src/AddArtist/AddNewSeries/AddNewSeries.css
Normal file
|
@ -0,0 +1,54 @@
|
|||
.searchContainer {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.searchIconContainer {
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: #edf1f2;
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clearLookupButton {
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
margin-top: 30px;
|
||||
}
|
184
frontend/src/AddArtist/AddNewSeries/AddNewSeries.js
Normal file
184
frontend/src/AddArtist/AddNewSeries/AddNewSeries.js
Normal file
|
@ -0,0 +1,184 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||
import styles from './AddNewSeries.css';
|
||||
|
||||
class AddNewSeries extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
term: props.term || '',
|
||||
isFetching: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const term = this.state.term;
|
||||
|
||||
if (term) {
|
||||
this.props.onSeriesLookupChange(term);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
term,
|
||||
isFetching
|
||||
} = this.props;
|
||||
|
||||
if (term && term !== prevProps.term) {
|
||||
this.setState({
|
||||
term,
|
||||
isFetching: true
|
||||
});
|
||||
this.props.onSeriesLookupChange(term);
|
||||
} else if (isFetching !== prevProps.isFetching) {
|
||||
this.setState({
|
||||
isFetching
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
const hasValue = !!value.trim();
|
||||
|
||||
this.setState({ term: value, isFetching: hasValue }, () => {
|
||||
if (hasValue) {
|
||||
this.props.onSeriesLookupChange(value);
|
||||
} else {
|
||||
this.props.onClearSeriesLookup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClearSeriesLookupPress = () => {
|
||||
this.setState({ term: '' });
|
||||
this.props.onClearSeriesLookup();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
const isFetching = this.state.isFetching;
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Artist">
|
||||
<PageContentBodyConnector>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
name={icons.SEARCH}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name="seriesLookup"
|
||||
value={term}
|
||||
placeholder="eg. Breaking Benjamin, lidarr:####"
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={styles.clearLookupButton}
|
||||
onPress={this.onClearSeriesLookupPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REMOVE}
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Failed to load search results, please try again.</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && !!items.length &&
|
||||
<div className={styles.searchResults}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AddNewSeriesSearchResultConnector
|
||||
key={item.foreignArtistId}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && !items.length && !!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
||||
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:71663</div>
|
||||
<div>
|
||||
<Link to="https://github.com/Sonarr/Sonarr/wiki/FAQ#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
||||
Why can't I find my artist?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>It's easy to add a new artist, just start typing the name the artist you want to add.</div>
|
||||
<div>You can also search using MusicBrainz ID of a show. eg. lidarr:71663</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewSeries.propTypes = {
|
||||
term: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSeriesLookupChange: PropTypes.func.isRequired,
|
||||
onClearSeriesLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewSeries;
|
102
frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js
Normal file
102
frontend/src/AddArtist/AddNewSeries/AddNewSeriesConnector.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import queryString from 'query-string';
|
||||
import { lookupSeries, clearAddSeries } from 'Store/Actions/addSeriesActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import AddNewSeries from './AddNewSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.routing.location,
|
||||
(addSeries, location) => {
|
||||
const query = queryString.parse(location.search);
|
||||
|
||||
return {
|
||||
term: query.term,
|
||||
...addSeries
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
lookupSeries,
|
||||
clearAddSeries,
|
||||
fetchRootFolders
|
||||
};
|
||||
|
||||
class AddNewSeriesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._seriesLookupTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._seriesLookupTimeout) {
|
||||
clearTimeout(this._seriesLookupTimeout);
|
||||
}
|
||||
|
||||
this.props.clearAddSeries();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSeriesLookupChange = (term) => {
|
||||
if (this._seriesLookupTimeout) {
|
||||
clearTimeout(this._seriesLookupTimeout);
|
||||
}
|
||||
|
||||
if (term.trim() === '') {
|
||||
this.props.clearAddSeries();
|
||||
} else {
|
||||
this._seriesLookupTimeout = setTimeout(() => {
|
||||
this.props.lookupSeries({ term });
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
onClearSeriesLookup = () => {
|
||||
this.props.clearAddSeries();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
term,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AddNewSeries
|
||||
term={term}
|
||||
{...otherProps}
|
||||
onSeriesLookupChange={this.onSeriesLookupChange}
|
||||
onClearSeriesLookup={this.onClearSeriesLookup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewSeriesConnector.propTypes = {
|
||||
term: PropTypes.string,
|
||||
lookupSeries: PropTypes.func.isRequired,
|
||||
clearAddSeries: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesConnector);
|
31
frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js
Normal file
31
frontend/src/AddArtist/AddNewSeries/AddNewSeriesModal.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddNewSeriesModalContentConnector from './AddNewSeriesModalContentConnector';
|
||||
|
||||
function AddNewSeriesModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddNewSeriesModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewSeriesModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewSeriesModal;
|
|
@ -0,0 +1,74 @@
|
|||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesLabel {
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesContainer {
|
||||
composes: container from 'Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForMissingEpisodesInput {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
}
|
||||
|
||||
.addButton {
|
||||
composes: button from 'Components/Link/SpinnerButton.css';
|
||||
composes: truncate from 'Styles/mixins/truncate.css';
|
||||
}
|
||||
|
||||
.hideLanguageProfile {
|
||||
composes: group from 'Components/Form/FormGroup.css';
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
265
frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js
Normal file
265
frontend/src/AddArtist/AddNewSeries/AddNewSeriesModalContent.js
Normal file
|
@ -0,0 +1,265 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import SeriesMonitoringOptionsPopoverContent from 'AddArtist/SeriesMonitoringOptionsPopoverContent';
|
||||
import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent';
|
||||
import styles from './AddNewSeriesModalContent.css';
|
||||
|
||||
class AddNewSeriesModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
searchForMissingEpisodes: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchForMissingEpisodesChange = ({ value }) => {
|
||||
this.setState({ searchForMissingEpisodes: value });
|
||||
}
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onLanguageProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'languageProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onAddSeriesPress = () => {
|
||||
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
artistName,
|
||||
year,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
albumFolder,
|
||||
tags,
|
||||
showLanguageProfile,
|
||||
isSmallScreen,
|
||||
onModalClose,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{artistName}
|
||||
|
||||
{
|
||||
!name.contains(year) &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<div className={styles.poster}>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Root Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Monitor
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Monitoring Options"
|
||||
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||
name="monitor"
|
||||
onChange={onInputChange}
|
||||
{...monitor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Quality Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup className={showLanguageProfile ? null : styles.hideLanguageProfile}>
|
||||
<FormLabel>Language Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||
name="languageProfileId"
|
||||
onChange={this.onLanguageProfileIdChange}
|
||||
{...languageProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Series Type
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Series Types"
|
||||
body={<SeriesTypePopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
onChange={onInputChange}
|
||||
{...seriesType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Album Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="albumFolder"
|
||||
onChange={onInputChange}
|
||||
{...albumFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingEpisodesLabelContainer}>
|
||||
<span className={styles.searchForMissingEpisodesLabel}>
|
||||
Start search for missing episodes
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForMissingEpisodesContainer}
|
||||
className={styles.searchForMissingEpisodesInput}
|
||||
name="searchForMissingEpisodes"
|
||||
value={this.state.searchForMissingEpisodes}
|
||||
onChange={this.onSearchForMissingEpisodesChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddSeriesPress}
|
||||
>
|
||||
Add {artistName}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewSeriesModalContent.propTypes = {
|
||||
artistName: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
languageProfileId: PropTypes.object,
|
||||
seriesType: PropTypes.object.isRequired,
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddSeriesPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewSeriesModalContent;
|
|
@ -0,0 +1,105 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.settings.languageProfiles,
|
||||
createDimensionsSelector(),
|
||||
(addSeriesState, languageProfiles, dimensions) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
defaults
|
||||
} = addSeriesState;
|
||||
|
||||
const {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings
|
||||
} = selectSettings(defaults, {}, addError);
|
||||
|
||||
return {
|
||||
isAdding,
|
||||
addError,
|
||||
showLanguageProfile: languageProfiles.length > 1,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAddSeriesDefault,
|
||||
addSeries
|
||||
};
|
||||
|
||||
class AddNewSeriesModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddSeriesDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddSeriesPress = (searchForMissingEpisodes) => {
|
||||
const {
|
||||
foreignArtistId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
albumFolder,
|
||||
tags
|
||||
} = this.props;
|
||||
|
||||
this.props.addSeries({
|
||||
foreignArtistId,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
languageProfileId: languageProfileId.value,
|
||||
albumFolder: albumFolder.value,
|
||||
tags: tags.value,
|
||||
searchForMissingEpisodes
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddNewSeriesModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onAddSeriesPress={this.onAddSeriesPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewSeriesModalContentConnector.propTypes = {
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
languageProfileId: PropTypes.object,
|
||||
albumFolder: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
setAddSeriesDefault: PropTypes.func.isRequired,
|
||||
addSeries: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewSeriesModalContentConnector);
|
|
@ -0,0 +1,40 @@
|
|||
.searchResult {
|
||||
display: flex;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
color: inherit;
|
||||
transition: background 500ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaf2ff;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 10px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-top: 20px;
|
||||
}
|
169
frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js
Normal file
169
frontend/src/AddArtist/AddNewSeries/AddNewSeriesSearchResult.js
Normal file
|
@ -0,0 +1,169 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import AddNewSeriesModal from './AddNewSeriesModal';
|
||||
import styles from './AddNewSeriesSearchResult.css';
|
||||
|
||||
class AddNewSeriesSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isNewAddSeriesModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
||||
this.onAddSerisModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onAddSerisModalClose = () => {
|
||||
this.setState({ isNewAddSeriesModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
foreignArtistId,
|
||||
artistName,
|
||||
nameSlug,
|
||||
year,
|
||||
network,
|
||||
status,
|
||||
overview,
|
||||
seasonCount,
|
||||
ratings,
|
||||
images,
|
||||
isExistingSeries,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const linkProps = isExistingSeries ? { to: `/series/${nameSlug}` } : { onPress: this.onPress };
|
||||
let seasons = '1 Season';
|
||||
|
||||
if (seasonCount > 1) {
|
||||
seasons = `${seasonCount} Seasons`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.searchResult}
|
||||
{...linkProps}
|
||||
>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
}
|
||||
|
||||
<div>
|
||||
<div className={styles.name}>
|
||||
{artistName}
|
||||
|
||||
{
|
||||
!name.contains(year) && !!year &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
artistName="Already in your library"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{network}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!!seasonCount &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{seasons}
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<Label
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Ended
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddNewSeriesModal
|
||||
isOpen={this.state.isNewAddSeriesModalOpen && !isExistingSeries}
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
year={year}
|
||||
overview={overview}
|
||||
images={images}
|
||||
onModalClose={this.onAddSerisModalClose}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewSeriesSearchResult.propTypes = {
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
nameSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number,
|
||||
network: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
seasonCount: PropTypes.number,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AddNewSeriesSearchResult;
|
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import AddNewSeriesSearchResult from './AddNewSeriesSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingSeriesSelector(),
|
||||
createDimensionsSelector(),
|
||||
(isExistingSeries, dimensions) => {
|
||||
return {
|
||||
isExistingSeries,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(AddNewSeriesSearchResult);
|
173
frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js
Normal file
173
frontend/src/AddArtist/ImportSeries/Import/ImportSeries.js
Normal file
|
@ -0,0 +1,173 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||
|
||||
class ImportSeries extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
contentBody: null,
|
||||
scrollTop: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setContentBodyRef = (ref) => {
|
||||
this.setState({ contentBody: ref });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
// Only select non-dupes
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveSelectedStateItem = (id) => {
|
||||
this.setState((state) => {
|
||||
const selectedState = Object.assign({}, state.selectedState);
|
||||
delete selectedState[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedState
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||
}
|
||||
|
||||
onImportPress = () => {
|
||||
this.props.onImportPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
this.setState({ scrollTop });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootFolderId,
|
||||
path,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
unmappedFolders,
|
||||
showLanguageProfile
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
contentBody
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector
|
||||
ref={this.setContentBodyRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
rootFoldersFetching && !rootFoldersPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError &&
|
||||
<div>Unable to load root folders</div>
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
|
||||
<div>
|
||||
All series in {path} have been imported
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
||||
<ImportSeriesTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
contentBody={contentBody}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
<ImportSeriesFooterConnector
|
||||
selectedIds={this.getSelectedIds()}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeries.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
path: PropTypes.string,
|
||||
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
rootFoldersError: PropTypes.object,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportSeries.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default ImportSeries;
|
|
@ -0,0 +1,121 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setImportSeriesValue, importSeries, clearImportSeries } from 'Store/Actions/importSeriesActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { setAddSeriesDefault } from 'Store/Actions/addSeriesActions';
|
||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||
import ImportSeries from './ImportSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.rootFolders,
|
||||
(state) => state.addSeries,
|
||||
(state) => state.importSeries,
|
||||
(state) => state.settings.languageProfiles,
|
||||
(match, rootFolders, addSeries, importSeriesState, languageProfiles) => {
|
||||
const {
|
||||
isFetching: rootFoldersFetching,
|
||||
isPopulated: rootFoldersPopulated,
|
||||
error: rootFoldersError,
|
||||
items
|
||||
} = rootFolders;
|
||||
|
||||
const rootFolderId = parseInt(match.params.rootFolderId);
|
||||
|
||||
const result = {
|
||||
rootFolderId,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
showLanguageProfile: languageProfiles.items.length > 1
|
||||
};
|
||||
|
||||
if (items.length) {
|
||||
const rootFolder = _.find(items, { id: rootFolderId });
|
||||
|
||||
return {
|
||||
...result,
|
||||
...rootFolder,
|
||||
items: importSeriesState.items
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setImportSeriesValue,
|
||||
importSeries,
|
||||
clearImportSeries,
|
||||
fetchRootFolders,
|
||||
setAddSeriesDefault
|
||||
};
|
||||
|
||||
class ImportSeriesConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.rootFoldersPopulated) {
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearImportSeries();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.setAddSeriesDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.setImportSeriesValue({
|
||||
id,
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.importSeries({ ids });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportSeries
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routeMatchShape = createRouteMatchShape({
|
||||
rootFolderId: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
ImportSeriesConnector.propTypes = {
|
||||
match: routeMatchShape.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired,
|
||||
importSeries: PropTypes.func.isRequired,
|
||||
clearImportSeries: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
setAddSeriesDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesConnector);
|
|
@ -0,0 +1,27 @@
|
|||
.inputContainer {
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.importButtonContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.importButton {
|
||||
composes: button from 'Components/Link/SpinnerButton.css';
|
||||
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin: 0 10px 0 12px;
|
||||
text-align: left;
|
||||
}
|
263
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js
Normal file
263
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesFooter.js
Normal file
|
@ -0,0 +1,263 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import styles from './ImportSeriesFooter.css';
|
||||
|
||||
const MIXED = 'mixed';
|
||||
|
||||
class ImportSeriesFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultLanguageProfileId,
|
||||
defaultSeasonFolder,
|
||||
defaultSeriesType
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
languageProfileId: defaultLanguageProfileId,
|
||||
seriesType: defaultSeriesType,
|
||||
seasonFolder: defaultSeasonFolder
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultLanguageProfileId,
|
||||
defaultSeriesType,
|
||||
defaultSeasonFolder,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isLanguageProfileIdMixed,
|
||||
isSeriesTypeMixed,
|
||||
isSeasonFolderMixed
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder
|
||||
} = this.state;
|
||||
|
||||
const newState = {};
|
||||
|
||||
if (isMonitorMixed && monitor !== MIXED) {
|
||||
newState.monitor = MIXED;
|
||||
} else if (!isMonitorMixed && monitor !== defaultMonitor) {
|
||||
newState.monitor = defaultMonitor;
|
||||
}
|
||||
|
||||
if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
|
||||
newState.qualityProfileId = MIXED;
|
||||
} else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
|
||||
newState.qualityProfileId = defaultQualityProfileId;
|
||||
}
|
||||
|
||||
if (isLanguageProfileIdMixed && languageProfileId !== MIXED) {
|
||||
newState.languageProfileId = MIXED;
|
||||
} else if (!isLanguageProfileIdMixed && languageProfileId !== defaultLanguageProfileId) {
|
||||
newState.languageProfileId = defaultLanguageProfileId;
|
||||
}
|
||||
|
||||
if (isSeriesTypeMixed && seriesType !== MIXED) {
|
||||
newState.seriesType = MIXED;
|
||||
} else if (!isSeriesTypeMixed && seriesType !== defaultSeriesType) {
|
||||
newState.seriesType = defaultSeriesType;
|
||||
}
|
||||
|
||||
if (isSeasonFolderMixed && seasonFolder != null) {
|
||||
newState.seasonFolder = null;
|
||||
} else if (!isSeasonFolderMixed && seasonFolder !== defaultSeasonFolder) {
|
||||
newState.seasonFolder = defaultSeasonFolder;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
this.props.onInputChange({ name, value });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedCount,
|
||||
isImporting,
|
||||
isLookingUpSeries,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isLanguageProfileIdMixed,
|
||||
isSeriesTypeMixed,
|
||||
showLanguageProfile,
|
||||
onImportPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isMonitorMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Quality Profile
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isQualityProfileIdMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
showLanguageProfile &&
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Language Profile
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||
name="languageProfileId"
|
||||
value={languageProfileId}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isLanguageProfileIdMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Series Type
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
value={seriesType}
|
||||
isDisabled={!selectedCount}
|
||||
includeMixed={isSeriesTypeMixed}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Season Folder
|
||||
</div>
|
||||
|
||||
<CheckInput
|
||||
name="seasonFolder"
|
||||
value={seasonFolder}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className={styles.label}>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.importButtonContainer}>
|
||||
<SpinnerButton
|
||||
className={styles.importButton}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isImporting}
|
||||
isDisabled={!selectedCount || isLookingUpSeries}
|
||||
onPress={onImportPress}
|
||||
>
|
||||
Import {selectedCount} Series
|
||||
</SpinnerButton>
|
||||
|
||||
{
|
||||
isLookingUpSeries &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={24}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpSeries &&
|
||||
'Processing Folders'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesFooter.propTypes = {
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isImporting: PropTypes.bool.isRequired,
|
||||
isLookingUpSeries: PropTypes.bool.isRequired,
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultLanguageProfileId: PropTypes.number,
|
||||
defaultSeriesType: PropTypes.string.isRequired,
|
||||
defaultSeasonFolder: PropTypes.bool.isRequired,
|
||||
isMonitorMixed: PropTypes.bool.isRequired,
|
||||
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isLanguageProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isSeriesTypeMixed: PropTypes.bool.isRequired,
|
||||
isSeasonFolderMixed: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesFooter;
|
|
@ -0,0 +1,57 @@
|
|||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ImportSeriesFooter from './ImportSeriesFooter';
|
||||
|
||||
function isMixed(items, selectedIds, defaultValue, key) {
|
||||
return _.some(items, (series) => {
|
||||
return selectedIds.indexOf(series.id) > -1 && series[key] !== defaultValue;
|
||||
});
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.importSeries,
|
||||
(state, { selectedIds }) => selectedIds,
|
||||
(addSeries, importSeries, selectedIds) => {
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
languageProfileId: defaultLanguageProfileId,
|
||||
seriesType: defaultSeriesType,
|
||||
seasonFolder: defaultSeasonFolder
|
||||
} = addSeries.defaults;
|
||||
|
||||
const items = importSeries.items;
|
||||
|
||||
const isLookingUpSeries = _.some(importSeries.items, (series) => {
|
||||
return !series.isPopulated && series.error == null;
|
||||
});
|
||||
|
||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||
const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId');
|
||||
const isSeriesTypeMixed = isMixed(items, selectedIds, defaultSeriesType, 'seriesType');
|
||||
const isSeasonFolderMixed = isMixed(items, selectedIds, defaultSeasonFolder, 'seasonFolder');
|
||||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
isImporting: importSeries.isImporting,
|
||||
isLookingUpSeries,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultLanguageProfileId,
|
||||
defaultSeriesType,
|
||||
defaultSeasonFolder,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
isLanguageProfileIdMixed,
|
||||
isSeriesTypeMixed,
|
||||
isSeasonFolderMixed
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ImportSeriesFooter);
|
|
@ -0,0 +1,45 @@
|
|||
.folder {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
}
|
||||
|
||||
.qualityProfile,
|
||||
.languageProfile {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seriesType {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.series {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.detailsIcon {
|
||||
margin-left: 8px;
|
||||
}
|
115
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js
Normal file
115
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesHeader.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import SeriesMonitoringOptionsPopoverContent from 'AddArtist/SeriesMonitoringOptionsPopoverContent';
|
||||
import SeriesTypePopoverContent from 'AddArtist/SeriesTypePopoverContent';
|
||||
import styles from './ImportSeriesHeader.css';
|
||||
|
||||
function ImportSeriesHeader(props) {
|
||||
const {
|
||||
showLanguageProfile,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.folder}
|
||||
name="folder"
|
||||
>
|
||||
Folder
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.monitor}
|
||||
name="monitor"
|
||||
>
|
||||
Monitor
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.detailsIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Monitoring Options"
|
||||
body={<SeriesMonitoringOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.qualityProfile}
|
||||
name="qualityProfileId"
|
||||
>
|
||||
Quality Profile
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
{
|
||||
showLanguageProfile &&
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.languageProfile}
|
||||
name="languageProfileId"
|
||||
>
|
||||
Language Profile
|
||||
</VirtualTableHeaderCell>
|
||||
}
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.seriesType}
|
||||
name="seriesType"
|
||||
>
|
||||
Series Type
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.detailsIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title="Series Type"
|
||||
body={<SeriesTypePopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.seasonFolder}
|
||||
name="seasonFolder"
|
||||
>
|
||||
Season Folder
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.series}
|
||||
name="series"
|
||||
>
|
||||
Series
|
||||
</VirtualTableHeaderCell>
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesHeader.propTypes = {
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesHeader;
|
|
@ -0,0 +1,52 @@
|
|||
.selectInput {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
}
|
||||
|
||||
.folder {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 1 0 200px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.monitor {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 185px;
|
||||
}
|
||||
|
||||
.qualityProfile,
|
||||
.languageProfile {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.seriesType {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 200px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.seasonFolder {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 150px;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.series {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.hideLanguageProfile {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
display: none;
|
||||
}
|
121
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js
Normal file
121
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesRow.js
Normal file
|
@ -0,0 +1,121 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
||||
import styles from './ImportSeriesRow.css';
|
||||
|
||||
function ImportSeriesRow(props) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seasonFolder,
|
||||
seriesType,
|
||||
selectedSeries,
|
||||
isExistingSeries,
|
||||
showLanguageProfile,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableRow style={style}>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={!selectedSeries || isExistingSeries}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<VirtualTableRowCell className={styles.folder}>
|
||||
{id}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.monitor}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.qualityProfile}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell
|
||||
className={showLanguageProfile ? styles.languageProfile : styles.hideLanguageProfile}
|
||||
>
|
||||
<FormInputGroup
|
||||
type={inputTypes.LANGUAGE_PROFILE_SELECT}
|
||||
name="languageProfileId"
|
||||
value={languageProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.seriesType}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SERIES_TYPE_SELECT}
|
||||
name="seriesType"
|
||||
value={seriesType}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.seasonFolder}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="seasonFolder"
|
||||
value={seasonFolder}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.series}>
|
||||
<ImportSeriesSelectSeriesConnector
|
||||
id={id}
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesRow.propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
languageProfileId: PropTypes.number.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
seasonFolder: PropTypes.bool.isRequired,
|
||||
selectedSeries: PropTypes.object,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportSeriesRow.defaultsProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
export default ImportSeriesRow;
|
|
@ -0,0 +1,91 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import ImportSeriesRow from './ImportSeriesRow';
|
||||
|
||||
function createImportSeriesItemSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.importSeries.items,
|
||||
(id, items) => {
|
||||
return _.find(items, { id }) || {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createImportSeriesItemSelector(),
|
||||
createAllSeriesSelector(),
|
||||
(item, series) => {
|
||||
const selectedSeries = item && item.selectedSeries;
|
||||
const isExistingSeries = !!selectedSeries && _.some(series, { tvdbId: selectedSeries.tvdbId });
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExistingSeries
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue
|
||||
};
|
||||
|
||||
class ImportSeriesRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setImportSeriesValue({
|
||||
id: this.props.id,
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
// Don't show the row until we have the information we require for it.
|
||||
|
||||
const {
|
||||
items,
|
||||
monitor,
|
||||
seriesType,
|
||||
seasonFolder
|
||||
} = this.props;
|
||||
|
||||
if (!items || !monitor || !seriesType || !seasonFolder == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImportSeriesRow
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSeriesSelect={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesRowConnector.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
seriesType: PropTypes.string,
|
||||
seasonFolder: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
queueLookupSeries: PropTypes.func.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRowConnector);
|
|
@ -0,0 +1,3 @@
|
|||
.input {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
}
|
213
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js
Normal file
213
frontend/src/AddArtist/ImportSeries/Import/ImportSeriesTable.js
Normal file
|
@ -0,0 +1,213 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
||||
|
||||
class ImportSeriesTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._table = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
unmappedFolders,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultLanguageProfileId,
|
||||
defaultSeriesType,
|
||||
defaultSeasonFolder,
|
||||
onSeriesLookup,
|
||||
onSetImportSeriesValue
|
||||
} = this.props;
|
||||
|
||||
const values = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
languageProfileId: defaultLanguageProfileId,
|
||||
seriesType: defaultSeriesType,
|
||||
seasonFolder: defaultSeasonFolder
|
||||
};
|
||||
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const id = unmappedFolder.name;
|
||||
|
||||
onSeriesLookup(id, unmappedFolder.path);
|
||||
|
||||
onSetImportSeriesValue({
|
||||
id,
|
||||
...values
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// This isn't great, but it's the most reliable way to ensure the items
|
||||
// are checked off even if they aren't actually visible since the cells
|
||||
// are virtualized.
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
selectedState,
|
||||
onSelectedChange,
|
||||
onRemoveSelectedStateItem
|
||||
} = this.props;
|
||||
|
||||
prevProps.items.forEach((prevItem) => {
|
||||
const {
|
||||
id
|
||||
} = prevItem;
|
||||
|
||||
const item = _.find(items, { id });
|
||||
|
||||
if (!item) {
|
||||
onRemoveSelectedStateItem(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSeries = item.selectedSeries;
|
||||
const isSelected = selectedState[id];
|
||||
|
||||
const isExistingSeries = !!selectedSeries &&
|
||||
_.some(prevProps.allSeries, { tvdbId: selectedSeries.tvdbId });
|
||||
|
||||
// Props doesn't have a selected series or
|
||||
// the selected series is an existing series.
|
||||
if ((selectedSeries && !prevItem.selectedSeries) || (isExistingSeries && !prevItem.selectedSeries)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// State is selected, but a series isn't selected or
|
||||
// the selected series is an existing series.
|
||||
if (isSelected && (!selectedSeries || isExistingSeries)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A series is being selected that wasn't previously selected.
|
||||
if (selectedSeries && selectedSeries !== prevItem.selectedSeries) {
|
||||
onSelectedChange({ id, value: true });
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Forces the table to re-render if the selected state
|
||||
// has changed otherwise it will be stale.
|
||||
|
||||
if (prevProps.selectedState !== selectedState && this._table) {
|
||||
this._table.forceUpdateGrid();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setTableRef = (ref) => {
|
||||
this._table = ref;
|
||||
}
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
rootFolderId,
|
||||
items,
|
||||
selectedState,
|
||||
showLanguageProfile,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<ImportSeriesRowConnector
|
||||
key={key}
|
||||
style={style}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
contentBody,
|
||||
showLanguageProfile,
|
||||
scrollTop,
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTable
|
||||
ref={this.setTableRef}
|
||||
items={items}
|
||||
contentBody={contentBody}
|
||||
isSmallScreen={isSmallScreen}
|
||||
rowHeight={52}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<ImportSeriesHeader
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesTable.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
defaultLanguageProfileId: PropTypes.number,
|
||||
defaultSeriesType: PropTypes.string.isRequired,
|
||||
defaultSeasonFolder: PropTypes.bool.isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||
onSeriesLookup: PropTypes.func.isRequired,
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesTable;
|
|
@ -0,0 +1,44 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import ImportSeriesTable from './ImportSeriesTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addSeries,
|
||||
(state) => state.importSeries,
|
||||
(state) => state.app.dimensions,
|
||||
createAllSeriesSelector(),
|
||||
(addSeries, importSeries, dimensions, allSeries) => {
|
||||
return {
|
||||
defaultMonitor: addSeries.defaults.monitor,
|
||||
defaultQualityProfileId: addSeries.defaults.qualityProfileId,
|
||||
defaultLanguageProfileId: addSeries.defaults.languageProfileId,
|
||||
defaultSeriesType: addSeries.defaults.seriesType,
|
||||
defaultSeasonFolder: addSeries.defaults.seasonFolder,
|
||||
items: importSeries.items,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
allSeries
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onSeriesLookup(name, path) {
|
||||
dispatch(queueLookupSeries({
|
||||
name,
|
||||
path,
|
||||
term: name
|
||||
}));
|
||||
},
|
||||
|
||||
onSetImportSeriesValue(values) {
|
||||
dispatch(setImportSeriesValue(values));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportSeriesTable);
|
|
@ -0,0 +1,8 @@
|
|||
.series {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: $menuItemHoverColor;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSearchResult.css';
|
||||
|
||||
class ImportSeriesSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tvdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.series}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportSeriesTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={network}
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesSearchResult.propTypes = {
|
||||
tvdbId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
network: PropTypes.string,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesSearchResult;
|
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingSeriesSelector from 'Store/Selectors/createExistingSeriesSelector';
|
||||
import ImportSeriesSearchResult from './ImportSeriesSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingSeriesSelector(),
|
||||
(isExistingSeries) => {
|
||||
return {
|
||||
isExistingSeries
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ImportSeriesSearchResult);
|
|
@ -0,0 +1,72 @@
|
|||
.tether {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.warningIcon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
margin-top: 4px;
|
||||
padding: 0 8px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 4px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
}
|
||||
|
||||
.searchContainer {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchIconContainer {
|
||||
width: 58px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: #edf1f2;
|
||||
text-align: center;
|
||||
line-height: 33px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
/*border-left: 0;*/
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
|
@ -0,0 +1,268 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import TetherComponent from 'react-tether';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnector';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSelectSeries.css';
|
||||
|
||||
const tetherOptions = {
|
||||
skipMoveElement: true,
|
||||
constraints: [
|
||||
{
|
||||
to: 'window',
|
||||
attachment: 'together',
|
||||
pin: true
|
||||
}
|
||||
],
|
||||
attachment: 'top center',
|
||||
targetAttachment: 'bottom center'
|
||||
};
|
||||
|
||||
class ImportSeriesSelectSeries extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._seriesLookupTimeout = null;
|
||||
|
||||
this.state = {
|
||||
term: props.id,
|
||||
isOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_setButtonRef = (ref) => {
|
||||
this._buttonRef = ref;
|
||||
}
|
||||
|
||||
_setContentRef = (ref) => {
|
||||
this._contentRef = ref;
|
||||
}
|
||||
|
||||
_addListener() {
|
||||
window.addEventListener('click', this.onWindowClick);
|
||||
}
|
||||
|
||||
_removeListener() {
|
||||
window.removeEventListener('click', this.onWindowClick);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onWindowClick = (event) => {
|
||||
const button = ReactDOM.findDOMNode(this._buttonRef);
|
||||
const content = ReactDOM.findDOMNode(this._contentRef);
|
||||
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
|
||||
this.setState({ isOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
}
|
||||
|
||||
onPress = () => {
|
||||
if (this.state.isOpen) {
|
||||
this._removeListener();
|
||||
} else {
|
||||
this._addListener();
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
if (this._seriesLookupTimeout) {
|
||||
clearTimeout(this._seriesLookupTimeout);
|
||||
}
|
||||
|
||||
this.setState({ term: value }, () => {
|
||||
this._seriesLookupTimeout = setTimeout(() => {
|
||||
this.props.onSearchInputChange(value);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
onSeriesSelect = (tvdbId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onSeriesSelect(tvdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedSeries,
|
||||
isExistingSeries,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
queued,
|
||||
onSeriesSelect
|
||||
} = this.props;
|
||||
|
||||
const errorMessage = error &&
|
||||
error.responseJSON &&
|
||||
error.responseJSON.message;
|
||||
|
||||
return (
|
||||
<TetherComponent
|
||||
classes={{
|
||||
element: styles.tether
|
||||
}}
|
||||
{...tetherOptions}
|
||||
>
|
||||
<Link
|
||||
ref={this._setButtonRef}
|
||||
className={styles.button}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
queued && !isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedSeries && isExistingSeries &&
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedSeries &&
|
||||
<ImportSeriesTitle
|
||||
title={selectedSeries.title}
|
||||
year={selectedSeries.year}
|
||||
network={selectedSeries.network}
|
||||
isExistingSeries={isExistingSeries}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !selectedSeries &&
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
No match found!
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
title={errorMessage}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
Search failed, please try again later.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.dropdownArrowContainer}>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{
|
||||
this.state.isOpen &&
|
||||
<div
|
||||
ref={this._setContentRef}
|
||||
className={styles.contentContainer}
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<SpinnerIcon
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isFetching}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name={`${name}_textInput`}
|
||||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportSeriesSearchResultConnector
|
||||
key={item.tvdbId}
|
||||
tvdbId={item.tvdbId}
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
network={item.network}
|
||||
onPress={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</TetherComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesSelectSeries.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
selectedSeries: PropTypes.object,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
onSearchInputChange: PropTypes.func.isRequired,
|
||||
onSeriesSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportSeriesSelectSeries.defaultProps = {
|
||||
isFetching: true,
|
||||
isPopulated: false,
|
||||
items: [],
|
||||
queued: true
|
||||
};
|
||||
|
||||
export default ImportSeriesSelectSeries;
|
|
@ -0,0 +1,71 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createImportSeriesItemSelector(),
|
||||
(item) => {
|
||||
return item;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue
|
||||
};
|
||||
|
||||
class ImportSeriesSelectSeriesConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchInputChange = (term) => {
|
||||
this.props.queueLookupSeries({
|
||||
name: this.props.id,
|
||||
term
|
||||
});
|
||||
}
|
||||
|
||||
onSeriesSelect = (tvdbId) => {
|
||||
const {
|
||||
id,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
this.props.setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries: _.find(items, { tvdbId })
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportSeriesSelectSeries
|
||||
{...this.props}
|
||||
onSearchInputChange={this.onSearchInputChange}
|
||||
onSeriesSelect={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesSelectSeriesConnector.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedSeries: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
queueLookupSeries: PropTypes.func.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesSelectSeriesConnector);
|
|
@ -0,0 +1,17 @@
|
|||
.titleContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 5px;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Label from 'Components/Label';
|
||||
import styles from './ImportSeriesTitle.css';
|
||||
|
||||
function ImportSeriesTitle(props) {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
network,
|
||||
isExistingSeries
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!network &&
|
||||
<Label>{network}</Label>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingSeries &&
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesTitle.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
network: PropTypes.string,
|
||||
isExistingSeries: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesTitle;
|
30
frontend/src/AddArtist/ImportSeries/ImportSeries.js
Normal file
30
frontend/src/AddArtist/ImportSeries/ImportSeries.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import ImportSeriesSelectFolderConnector from 'AddArtist/ImportSeries/SelectFolder/ImportSeriesSelectFolderConnector';
|
||||
import ImportSeriesConnector from 'AddArtist/ImportSeries/Import/ImportSeriesConnector';
|
||||
|
||||
class ImportSeries extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path="/add/import"
|
||||
component={ImportSeriesSelectFolderConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/import/:rootFolderId"
|
||||
component={ImportSeriesConnector}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ImportSeries;
|
|
@ -0,0 +1,18 @@
|
|||
.link {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
.freeSpace,
|
||||
.unmappedFolders {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 45px;
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import styles from './ImportSeriesRootFolderRow.css';
|
||||
|
||||
function ImportSeriesRootFolderRow(props) {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
freeSpace,
|
||||
unmappedFolders,
|
||||
onDeletePress
|
||||
} = props;
|
||||
|
||||
const unmappedFoldersCount = unmappedFolders.length || '-';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/add/import/${id}`}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace) || '-'}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.unmappedFolders}>
|
||||
{unmappedFoldersCount}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
onPress={onDeletePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesRootFolderRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number.isRequired,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportSeriesRootFolderRow.defaultProps = {
|
||||
freeSpace: 0,
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default ImportSeriesRootFolderRow;
|
|
@ -0,0 +1,48 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportSeriesRootFolderRow from './ImportSeriesRootFolderRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
() => {
|
||||
return {
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteRootFolder
|
||||
};
|
||||
|
||||
class ImportSeriesRootFolderRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteRootFolder({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportSeriesRootFolderRow
|
||||
{...this.props}
|
||||
onDeletePress={this.onDeletePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesRootFolderRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRootFolderRowConnector);
|
|
@ -0,0 +1,32 @@
|
|||
.header {
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.tips {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 12px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.recentFolders {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.startImport {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.importButtonIcon {
|
||||
margin-right: 8px;
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import ImportSeriesRootFolderRowConnector from './ImportSeriesRootFolderRowConnector';
|
||||
import styles from './ImportSeriesSelectFolder.css';
|
||||
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: 'Free Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
label: 'Unmapped Folders',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class ImportSeriesSelectFolder extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddNewRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
}
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
}
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load root folders</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated &&
|
||||
<div>
|
||||
<div className={styles.header}>
|
||||
Import series you already have
|
||||
</div>
|
||||
|
||||
<div className={styles.tips}>
|
||||
Some tips to ensure the import goes smoothly:
|
||||
<ul>
|
||||
<li className={styles.tip}>
|
||||
Make sure your files include the quality in the name. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||
</li>
|
||||
<li className={styles.tip}>
|
||||
Point Sonarr to the folder containing all of your tv shows not a specific one. eg. <span className={styles.code}>"\tv shows\"</span> and not <span className={styles.code}>"\tv shows\the simpsons\"</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{
|
||||
items.length > 0 ?
|
||||
<div className={styles.recentFolders}>
|
||||
<FieldSet
|
||||
legend="Recent Folders"
|
||||
>
|
||||
<Table
|
||||
columns={rootFolderColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((rootFolder) => {
|
||||
return (
|
||||
<ImportSeriesRootFolderRowConnector
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</FieldSet>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name="fa fa-hdd-o"
|
||||
/>
|
||||
Choose another folder
|
||||
</Button>
|
||||
</div> :
|
||||
|
||||
<div className={styles.startImport}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name="fa fa-hdd-o"
|
||||
/>
|
||||
Start Import
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={this.onNewRootFolderSelect}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesSelectFolder.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired,
|
||||
onDeleteRootFolderPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesSelectFolder;
|
|
@ -0,0 +1,87 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { push } from 'react-router-redux';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.rootFolders,
|
||||
(rootFolders) => {
|
||||
return rootFolders;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchRootFolders,
|
||||
addRootFolder,
|
||||
deleteRootFolder,
|
||||
push
|
||||
};
|
||||
|
||||
class ImportSeriesSelectFolderConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isSaving,
|
||||
saveError
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
const newRootFolders = _.differenceBy(items, prevProps.items, (item) => item.id);
|
||||
|
||||
if (newRootFolders.length === 1) {
|
||||
this.props.push(`${window.Sonarr.urlBase}/add/import/${newRootFolders[0].id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onNewRootFolderSelect = (path) => {
|
||||
this.props.addRootFolder({ path });
|
||||
}
|
||||
|
||||
onDeleteRootFolderPress = (id) => {
|
||||
this.props.deleteRootFolder({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportSeriesSelectFolder
|
||||
{...this.props}
|
||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
||||
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSeriesSelectFolderConnector.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
addRootFolder: PropTypes.func.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesSelectFolderConnector);
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
|
||||
function SeriesMonitoringOptionsPopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="All Episodes"
|
||||
data="Monitor all episodes except specials"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Future Episodes"
|
||||
data="Monitor episodes that have not aired yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Missing Episodes"
|
||||
data="Monitor episodes that do not have files or have not aired yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Existing Episodes"
|
||||
data="Monitor episodes that have files or have not aired yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="First Season"
|
||||
data="Monitor all episodes of the first season. All other seasons will be ignored"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Latest Season"
|
||||
data="Monitor all episodes of the latest season and future seasons"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="None"
|
||||
data="No episodes will be monitored."
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesMonitoringOptionsPopoverContent;
|
26
frontend/src/AddArtist/SeriesTypePopoverContent.js
Normal file
26
frontend/src/AddArtist/SeriesTypePopoverContent.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
|
||||
function SeriesTypePopoverContent() {
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Anime"
|
||||
data="Episodes released using an absolute episode number"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Daily"
|
||||
data="Episodes released daily or less frequently that use year-month-day (2017-05-25)"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Standard"
|
||||
data="Episodes released with SxxEyy pattern"
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesTypePopoverContent;
|
Loading…
Add table
Add a link
Reference in a new issue