mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-16 10:03:51 -07:00
Initial Commit Rework
This commit is contained in:
parent
74a4cc048c
commit
95051cbd63
2483 changed files with 101351 additions and 111396 deletions
37
frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js
Normal file
37
frontend/src/InteractiveImport/Episode/SelectEpisodeModal.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectEpisodeModalContentConnector from './SelectEpisodeModalContentConnector';
|
||||
|
||||
class SelectEpisodeModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectEpisodeModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEpisodeModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEpisodeModal;
|
|
@ -0,0 +1,183 @@
|
|||
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 { kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectEpisodeRow from './SelectEpisodeRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'episodeNumber',
|
||||
label: '#',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: 'Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'airDate',
|
||||
label: 'Air Date',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class SelectEpisodeModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onEpisodesSelect = () => {
|
||||
this.props.onEpisodesSelect(this.getSelectedIds());
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const errorMessage = error && error.message || 'Unable to load episodes';
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Episode(s)
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
error &&
|
||||
<div>{errorMessage}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<SelectEpisodeRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
episodeNumber={item.episodeNumber}
|
||||
title={item.title}
|
||||
airDate={item.airDate}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length &&
|
||||
'No episodes were found for the selected season'
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onEpisodesSelect}
|
||||
>
|
||||
Select Episodes
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEpisodeModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onEpisodesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEpisodeModalContent;
|
|
@ -0,0 +1,103 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import connectSection from 'Store/connectSection';
|
||||
import { fetchEpisodes, setEpisodesSort, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import SelectEpisodeModalContent from './SelectEpisodeModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector(),
|
||||
(episodes) => {
|
||||
return episodes;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchEpisodes,
|
||||
setEpisodesSort,
|
||||
clearEpisodes,
|
||||
updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectEpisodeModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
artistId,
|
||||
seasonNumber
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchEpisodes({ artistId, seasonNumber });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This clears the episodes for the queue and hides the queue
|
||||
// We'll need another place to store episodes for manual import
|
||||
this.props.clearEpisodes();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setEpisodesSort({ sortKey, sortDirection });
|
||||
}
|
||||
|
||||
onEpisodesSelect = (episodeIds) => {
|
||||
const episodes = _.reduce(this.props.items, (acc, item) => {
|
||||
if (episodeIds.indexOf(item.id) > -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
this.props.updateInteractiveImportItem({
|
||||
id: this.props.id,
|
||||
episodes: _.sortBy(episodes, 'episodeNumber')
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectEpisodeModalContent
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onEpisodesSelect={this.onEpisodesSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEpisodeModalContentConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchEpisodes: PropTypes.func.isRequired,
|
||||
setEpisodesSort: PropTypes.func.isRequired,
|
||||
clearEpisodes: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connectSection(
|
||||
createMapStateToProps,
|
||||
mapDispatchToProps,
|
||||
undefined,
|
||||
undefined,
|
||||
{ section: 'episodes' }
|
||||
)(SelectEpisodeModalContentConnector);
|
67
frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js
Normal file
67
frontend/src/InteractiveImport/Episode/SelectEpisodeRow.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
|
||||
class SelectEpisodeRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
id,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
this.props.onSelectedChange({ id, value: !isSelected });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
episodeNumber,
|
||||
title,
|
||||
airDate,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
{episodeNumber}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{airDate}
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEpisodeRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
episodeNumber: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
airDate: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEpisodeRow;
|
|
@ -0,0 +1,24 @@
|
|||
.recentFoldersContainer {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.buttonsContainer {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.buttonIcon {
|
||||
margin-right: 5px;
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
import PathInputConnector from 'Components/Form/PathInputConnector';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import RecentFolderRow from './RecentFolderRow';
|
||||
import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||
|
||||
const recentFoldersColumns = [
|
||||
{
|
||||
name: 'folder',
|
||||
label: 'Folder'
|
||||
},
|
||||
{
|
||||
name: 'lastUsed',
|
||||
label: 'Last Used'
|
||||
}
|
||||
];
|
||||
|
||||
class InteractiveImportSelectFolderModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
folder: ''
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPathChange = ({ value }) => {
|
||||
this.setState({ folder: value });
|
||||
}
|
||||
|
||||
onRecentPathPress = (folder) => {
|
||||
this.setState({ folder });
|
||||
}
|
||||
|
||||
onQuickImportPress = () => {
|
||||
this.props.onQuickImportPress(this.state.folder);
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onInteractiveImportPress(this.state.folder);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
recentFolders,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const folder = this.state.folder;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Folder
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<PathInputConnector
|
||||
name="folder"
|
||||
value={folder}
|
||||
onChange={this.onPathChange}
|
||||
/>
|
||||
|
||||
{
|
||||
!!recentFolders.length &&
|
||||
<div className={styles.recentFoldersContainer}>
|
||||
<Table
|
||||
columns={recentFoldersColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
recentFolders.map((recentFolder) => {
|
||||
return (
|
||||
<RecentFolderRow
|
||||
key={recentFolder.folder}
|
||||
folder={recentFolder.folder}
|
||||
lastUsed={recentFolder.lastUsed}
|
||||
onPress={this.onRecentPathPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.buttonsContainer}>
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
isDisabled={!folder}
|
||||
onPress={this.onQuickImportPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.buttonIcon}
|
||||
name={icons.QUICK}
|
||||
/>
|
||||
|
||||
Quick Import
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
isDisabled={!folder}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.buttonIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
/>
|
||||
|
||||
Interactive Import
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportSelectFolderModalContent.propTypes = {
|
||||
recentFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onQuickImportPress: PropTypes.func.isRequired,
|
||||
onInteractiveImportPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveImportSelectFolderModalContent;
|
|
@ -0,0 +1,73 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import InteractiveImportSelectFolderModalContent from './InteractiveImportSelectFolderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.interactiveImport.recentFolders,
|
||||
(recentFolders) => {
|
||||
return {
|
||||
recentFolders
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addRecentFolder,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class InteractiveImportSelectFolderModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQuickImportPress = (folder) => {
|
||||
this.props.addRecentFolder({ folder });
|
||||
|
||||
this.props.executeCommand({
|
||||
name: commandNames.DOWNLOADED_EPSIODES_SCAN,
|
||||
path: folder
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
onInteractiveImportPress = (folder) => {
|
||||
this.props.addRecentFolder({ folder });
|
||||
this.props.onFolderSelect(folder);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
if (this.path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<InteractiveImportSelectFolderModalContent
|
||||
{...this.props}
|
||||
onQuickImportPress={this.onQuickImportPress}
|
||||
onInteractiveImportPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportSelectFolderModalContentConnector.propTypes = {
|
||||
path: PropTypes.string,
|
||||
onFolderSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
addRecentFolder: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportSelectFolderModalContentConnector);
|
41
frontend/src/InteractiveImport/Folder/RecentFolderRow.js
Normal file
41
frontend/src/InteractiveImport/Folder/RecentFolderRow.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
|
||||
class RecentFolderRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.folder);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
folder,
|
||||
lastUsed
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowCell>{folder}</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector date={lastUsed} />
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecentFolderRow.propTypes = {
|
||||
folder: PropTypes.string.isRequired,
|
||||
lastUsed: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RecentFolderRow;
|
|
@ -0,0 +1,45 @@
|
|||
.footer {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.leftButtons,
|
||||
.centerButtons,
|
||||
.rightButtons {
|
||||
display: flex;
|
||||
flex: 1 0 33%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.centerButtons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rightButtons {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.importMode {
|
||||
composes: select from 'Components/Form/SelectInput.css';
|
||||
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
color: $dangerColor;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.footer {
|
||||
a,
|
||||
button {
|
||||
margin-left: 0;
|
||||
|
||||
&:first-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,320 @@
|
|||
import _ from 'lodash';
|
||||
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 { icons, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
label: 'Series',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'season',
|
||||
label: 'Season',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'episodes',
|
||||
label: 'Episode(s)',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER
|
||||
}),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
invalidRowsSelected: [],
|
||||
isSelectSeriesModalOpen: false,
|
||||
isSelectSeasonModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onValidRowChange = (id, isValid) => {
|
||||
this.setState((state) => {
|
||||
if (isValid) {
|
||||
return {
|
||||
invalidRowsSelected: _.without(state.invalidRowsSelected, id)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
invalidRowsSelected: [...state.invalidRowsSelected, id]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onImportSelectedPress = () => {
|
||||
const selected = this.getSelectedIds();
|
||||
|
||||
this.props.onImportSelectedPress(selected, this.state.importMode);
|
||||
}
|
||||
|
||||
onImportModeChange = ({ value }) => {
|
||||
this.props.onImportModeChange(value);
|
||||
}
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectSeasonPress = () => {
|
||||
this.setState({ isSelectSeasonModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectSeriesModalClose = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: false });
|
||||
}
|
||||
|
||||
onSelectSeasonModalClose = () => {
|
||||
this.setState({ isSelectSeasonModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
downloadId,
|
||||
title,
|
||||
folder,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
importMode,
|
||||
interactiveImportErrorMessage,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
invalidRowsSelected,
|
||||
isSelectSeriesModalOpen,
|
||||
isSelectSeasonModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedItem = selectedIds.length ? _.find(items, { id: selectedIds[0] }) : null;
|
||||
const errorMessage = error && error.message || 'Unable to load manual import items';
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Copy Files' }
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - {title || folder}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
error &&
|
||||
<div>{errorMessage}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveImportRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length &&
|
||||
'No video files were found in the selected folder'
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
{
|
||||
!downloadId &&
|
||||
<div className={styles.leftButtons}>
|
||||
<SelectInput
|
||||
className={styles.importMode}
|
||||
name="importMode"
|
||||
value={importMode}
|
||||
values={importModeOptions}
|
||||
onChange={this.onImportModeChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={downloadId ? styles.leftButtons : styles.centerButtons}>
|
||||
<Button onPress={this.onSelectSeriesPress}>
|
||||
Select Series
|
||||
</Button>
|
||||
|
||||
<Button onPress={this.onSelectSeasonPress}>
|
||||
Select Season
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.rightButtons}>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{
|
||||
interactiveImportErrorMessage &&
|
||||
<span className={styles.errorMessage}>{interactiveImportErrorMessage}</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
|
||||
onPress={this.onImportSelectedPress}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
ids={selectedIds}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={isSelectSeasonModalOpen}
|
||||
ids={selectedIds}
|
||||
artistId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
onModalClose={this.onSelectSeasonModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportModalContent.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
importMode: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
interactiveImportErrorMessage: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContent.defaultProps = {
|
||||
importMode: 'move'
|
||||
};
|
||||
|
||||
export default InteractiveImportModalContent;
|
|
@ -0,0 +1,152 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { createSelector } from 'reselect';
|
||||
import connectSection from 'Store/connectSection';
|
||||
import { fetchInteractiveImportItems, setInteractiveImportSort, clearInteractiveImport, setInteractiveImportMode } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import InteractiveImportModalContent from './InteractiveImportModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector(),
|
||||
(interactiveImport) => {
|
||||
return interactiveImport;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchInteractiveImportItems,
|
||||
setInteractiveImportSort,
|
||||
setInteractiveImportMode,
|
||||
clearInteractiveImport,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class InteractiveImportModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
interactiveImportErrorMessage: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
downloadId,
|
||||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportItems({ downloadId, folder });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearInteractiveImport();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.setInteractiveImportSort({ sortKey, sortDirection });
|
||||
}
|
||||
|
||||
onImportModeChange = (importMode) => {
|
||||
this.props.setInteractiveImportMode({ importMode });
|
||||
}
|
||||
|
||||
onImportSelectedPress = (selected, importMode) => {
|
||||
const files = [];
|
||||
|
||||
_.forEach(this.props.items, (item) => {
|
||||
const isSelected = selected.indexOf(item.id) > -1;
|
||||
|
||||
if (isSelected) {
|
||||
const {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNaN(seasonNumber)) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!episodes || !episodes.length) {
|
||||
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
|
||||
return false;
|
||||
}
|
||||
|
||||
files.push({
|
||||
path: item.path,
|
||||
artistId: series.id,
|
||||
episodeIds: _.map(episodes, 'id'),
|
||||
quality,
|
||||
downloadId: this.props.downloadId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.executeCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<InteractiveImportModalContent
|
||||
{...this.props}
|
||||
interactiveImportErrorMessage={this.state.interactiveImportErrorMessage}
|
||||
onSortPress={this.onSortPress}
|
||||
onImportModeChange={this.onImportModeChange}
|
||||
onImportSelectedPress={this.onImportSelectedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportModalContentConnector.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
setInteractiveImportSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImport: PropTypes.func.isRequired,
|
||||
setInteractiveImportMode: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connectSection(
|
||||
createMapStateToProps,
|
||||
mapDispatchToProps,
|
||||
undefined,
|
||||
undefined,
|
||||
{ section: 'interactiveImport' }
|
||||
)(InteractiveImportModalContentConnector);
|
|
@ -0,0 +1,11 @@
|
|||
.relativePath {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.quality {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,294 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
|
||||
class InteractiveImportRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isSelectSeriesModalOpen: false,
|
||||
isSelectSeasonModalOpen: false,
|
||||
isSelectEpisodeModalOpen: false,
|
||||
isSelectQualityModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality
|
||||
} = this.props;
|
||||
|
||||
if (series && seasonNumber !== undefined && episodes.length && quality) {
|
||||
this.props.onSelectedChange({ id, value: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
isSelected,
|
||||
onValidRowChange
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSelected === isSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = !!(series && seasonNumber != null && episodes.length && quality);
|
||||
|
||||
if (isSelected && !isValid) {
|
||||
onValidRowChange(id, false);
|
||||
} else {
|
||||
onValidRowChange(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
selectRowAfterChange = (value) => {
|
||||
const {
|
||||
id,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
if (!isSelected && value === true) {
|
||||
this.props.onSelectedChange({ id, value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectSeasonPress = () => {
|
||||
this.setState({ isSelectSeasonModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectEpisodePress = () => {
|
||||
this.setState({ isSelectEpisodeModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectSeriesModalClose = (changed) => {
|
||||
this.setState({ isSelectSeriesModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectSeasonModalClose = (changed) => {
|
||||
this.setState({ isSelectSeasonModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectEpisodeModalClose = (changed) => {
|
||||
this.setState({ isSelectEpisodeModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
onSelectQualityModalClose = (changed) => {
|
||||
this.setState({ isSelectQualityModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
relativePath,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
size,
|
||||
rejections,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectSeriesModalOpen,
|
||||
isSelectSeasonModalOpen,
|
||||
isSelectEpisodeModalOpen,
|
||||
isSelectQualityModalOpen
|
||||
} = this.state;
|
||||
|
||||
const seriesTitle = series ? series.title : '';
|
||||
const episodeNumbers = episodes.map((episode) => episode.episodeNumber)
|
||||
.join(', ');
|
||||
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber);
|
||||
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.relativePath}
|
||||
title={relativePath}
|
||||
>
|
||||
{relativePath}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
onPress={this.onSelectSeriesPress}
|
||||
>
|
||||
{
|
||||
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series}
|
||||
onPress={this.onSelectSeasonPress}
|
||||
>
|
||||
{
|
||||
showSeasonNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seasonNumber
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series || isNaN(seasonNumber)}
|
||||
onPress={this.onSelectEpisodePress}
|
||||
>
|
||||
{
|
||||
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeNumbers
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.quality}
|
||||
onPress={this.onSelectQualityPress}
|
||||
>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{
|
||||
!!rejections.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title="Release Rejected"
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
rejections.map((rejection, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{rejection.reason}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
ids={[id]}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={isSelectSeasonModalOpen}
|
||||
ids={[id]}
|
||||
artistId={series && series.id}
|
||||
onModalClose={this.onSelectSeasonModalClose}
|
||||
/>
|
||||
|
||||
<SelectEpisodeModal
|
||||
isOpen={isSelectEpisodeModalOpen}
|
||||
id={id}
|
||||
artistId={series && series.id}
|
||||
seasonNumber={seasonNumber}
|
||||
onModalClose={this.onSelectEpisodeModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={isSelectQualityModalOpen}
|
||||
id={id}
|
||||
qualityId={quality.quality.id}
|
||||
proper={quality.revision.version > 1}
|
||||
real={quality.revision.real > 0}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
InteractiveImportRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
series: PropTypes.object,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object,
|
||||
size: PropTypes.number.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportRow.defaultProps = {
|
||||
episodes: []
|
||||
};
|
||||
|
||||
export default InteractiveImportRow;
|
|
@ -0,0 +1,7 @@
|
|||
.placeholder {
|
||||
display: inline-block;
|
||||
margin: -8px 0;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
border: 2px dashed $dangerColor;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import styles from './InteractiveImportRowCellPlaceholder.css';
|
||||
|
||||
function InteractiveImportRowCellPlaceholder() {
|
||||
return (
|
||||
<span className={styles.placeholder}></span>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveImportRowCellPlaceholder;
|
78
frontend/src/InteractiveImport/InteractiveImportModal.js
Normal file
78
frontend/src/InteractiveImport/InteractiveImportModal.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector';
|
||||
import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector';
|
||||
|
||||
class InteractiveImportModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
folder: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isOpen && !this.props.isOpen) {
|
||||
this.setState({ folder: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFolderSelect = (folder) => {
|
||||
this.setState({ folder });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
folder,
|
||||
downloadId,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const folderPath = folder || this.state.folder;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
folderPath || downloadId ?
|
||||
<InteractiveImportModalContentConnector
|
||||
folder={folderPath}
|
||||
downloadId={downloadId}
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/> :
|
||||
<InteractiveImportSelectFolderModalContentConnector
|
||||
{...otherProps}
|
||||
onFolderSelect={this.onFolderSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
folder: PropTypes.string,
|
||||
downloadId: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveImportModal;
|
37
frontend/src/InteractiveImport/Quality/SelectQualityModal.js
Normal file
37
frontend/src/InteractiveImport/Quality/SelectQualityModal.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectQualityModalContentConnector from './SelectQualityModalContentConnector';
|
||||
|
||||
class SelectQualityModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectQualityModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectQualityModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectQualityModal;
|
|
@ -0,0 +1,166 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
class SelectQualityModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
qualityId,
|
||||
proper,
|
||||
real
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
qualityId,
|
||||
proper,
|
||||
real
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityChange = ({ value }) => {
|
||||
this.setState({ qualityId: parseInt(value) });
|
||||
}
|
||||
|
||||
onProperChange = ({ value }) => {
|
||||
this.setState({ proper: value });
|
||||
}
|
||||
|
||||
onRealChange = ({ value }) => {
|
||||
this.setState({ real: value });
|
||||
}
|
||||
|
||||
onQualitySelect = () => {
|
||||
this.props.onQualitySelect(this.state);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
qualityId,
|
||||
proper,
|
||||
real
|
||||
} = this.state;
|
||||
|
||||
const qualityOptions = items.map(({ quality }) => {
|
||||
return {
|
||||
key: quality.id,
|
||||
value: quality.name
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Quality
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load qualities</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error &&
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Quality</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="quality"
|
||||
value={qualityId}
|
||||
values={qualityOptions}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Proper</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proper"
|
||||
value={proper}
|
||||
onChange={this.onProperChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Real</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="real"
|
||||
value={real}
|
||||
onChange={this.onRealChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onQualitySelect}
|
||||
>
|
||||
Select Quality
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectQualityModalContent.propTypes = {
|
||||
qualityId: PropTypes.number.isRequired,
|
||||
proper: PropTypes.bool.isRequired,
|
||||
real: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onQualitySelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectQualityModalContent;
|
|
@ -0,0 +1,94 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectQualityModalContent from './SelectQualityModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const {
|
||||
isFetchingSchema: isFetching,
|
||||
schemaPopulated: isPopulated,
|
||||
schemaError: error,
|
||||
schema
|
||||
} = qualityProfiles;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items: schema.items || []
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQualityProfileSchema,
|
||||
updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectQualityModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount = () => {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualitySelect = ({ qualityId, proper, real }) => {
|
||||
const quality = _.find(this.props.items,
|
||||
(item) => item.quality.id === qualityId).quality;
|
||||
|
||||
const revision = {
|
||||
version: proper ? 2 : 1,
|
||||
real: real ? 1 : 0
|
||||
};
|
||||
|
||||
this.props.updateInteractiveImportItem({
|
||||
id: this.props.id,
|
||||
quality: {
|
||||
quality,
|
||||
revision
|
||||
}
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectQualityModalContent
|
||||
{...this.props}
|
||||
onQualitySelect={this.onQualitySelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectQualityModalContentConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector);
|
37
frontend/src/InteractiveImport/Season/SelectSeasonModal.js
Normal file
37
frontend/src/InteractiveImport/Season/SelectSeasonModal.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectSeasonModalContentConnector from './SelectSeasonModalContentConnector';
|
||||
|
||||
class SelectSeasonModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectSeasonModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeasonModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeasonModal;
|
|
@ -0,0 +1,58 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
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 SelectSeasonRow from './SelectSeasonRow';
|
||||
|
||||
class SelectSeasonModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onSeasonSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Season
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<SelectSeasonRow
|
||||
key={item.seasonNumber}
|
||||
seasonNumber={item.seasonNumber}
|
||||
onSeasonSelect={onSeasonSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeasonModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSeasonSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeasonModalContent;
|
|
@ -0,0 +1,62 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import SelectSeasonModalContent from './SelectSeasonModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
(series) => {
|
||||
return {
|
||||
items: series.seasons
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectSeasonModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSeasonSelect = (seasonNumber) => {
|
||||
this.props.ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
seasonNumber,
|
||||
episodes: []
|
||||
});
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectSeasonModalContent
|
||||
{...this.props}
|
||||
onSeasonSelect={this.onSeasonSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeasonModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeasonModalContentConnector);
|
|
@ -0,0 +1,4 @@
|
|||
.season {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
}
|
40
frontend/src/InteractiveImport/Season/SelectSeasonRow.js
Normal file
40
frontend/src/InteractiveImport/Season/SelectSeasonRow.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './SelectSeasonRow.css';
|
||||
|
||||
class SelectSeasonRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onSeasonSelect(this.props.seasonNumber);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const seasonNumber = this.props.seasonNumber;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.season}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
seasonNumber === 0 ? 'Specials' : `Season ${seasonNumber}`
|
||||
}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeasonRow.propTypes = {
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
onSeasonSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeasonRow;
|
37
frontend/src/InteractiveImport/Series/SelectSeriesModal.js
Normal file
37
frontend/src/InteractiveImport/Series/SelectSeriesModal.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectSeriesModalContentConnector from './SelectSeriesModalContentConnector';
|
||||
|
||||
class SelectSeriesModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectSeriesModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeriesModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesModal;
|
|
@ -0,0 +1,18 @@
|
|||
.modalBody {
|
||||
composes: modalBody from 'Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
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 SelectSeriesRow from './SelectSeriesRow';
|
||||
import styles from './SelectSeriesModalContent.css';
|
||||
|
||||
class SelectSeriesModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
filter: ''
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
onSeriesSelect,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Series
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<TextInput
|
||||
className={styles.filterInput}
|
||||
placeholder="Filter series"
|
||||
name="filter"
|
||||
value={filter}
|
||||
autoFocus={true}
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<Scroller className={styles.scroller}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
(
|
||||
<SelectSeriesRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
title={item.title}
|
||||
onSeriesSelect={onSeriesSelect}
|
||||
/>
|
||||
) :
|
||||
null;
|
||||
})
|
||||
}
|
||||
</Scroller>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeriesModalContent.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSeriesSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesModalContent;
|
|
@ -0,0 +1,65 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import SelectSeriesModalContent from './SelectSeriesModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAllSeriesSelector(),
|
||||
(items) => {
|
||||
return {
|
||||
items
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectSeriesModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSeriesSelect = (artistId) => {
|
||||
const series = _.find(this.props.items, { id: artistId });
|
||||
|
||||
this.props.ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
series,
|
||||
seasonNumber: undefined,
|
||||
episodes: []
|
||||
});
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectSeriesModalContent
|
||||
{...this.props}
|
||||
onSeriesSelect={this.onSeriesSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeriesModalContentConnector.propTypes = {
|
||||
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectSeriesModalContentConnector);
|
|
@ -0,0 +1,4 @@
|
|||
.series {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
}
|
37
frontend/src/InteractiveImport/Series/SelectSeriesRow.js
Normal file
37
frontend/src/InteractiveImport/Series/SelectSeriesRow.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './SelectSeriesRow.css';
|
||||
|
||||
class SelectSeriesRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onSeriesSelect(this.props.id);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Link
|
||||
className={styles.series}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
>
|
||||
{this.props.title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectSeriesRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
onSeriesSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectSeriesRow;
|
Loading…
Add table
Add a link
Reference in a new issue