mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-22 22:15:17 -04:00
New: Project Aphrodite
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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 ImportMovieTableConnector from './ImportMovieTableConnector';
|
||||
import ImportMovieFooterConnector from './ImportMovieFooterConnector';
|
||||
|
||||
class ImportMovie 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
|
||||
} = 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 &&
|
||||
<ImportMovieTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
contentBody={contentBody}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
<ImportMovieFooterConnector
|
||||
selectedIds={this.getSelectedIds()}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovie.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),
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportMovie.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default ImportMovie;
|
||||
@@ -0,0 +1,152 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setImportMovieValue, importMovie, clearImportMovie } from 'Store/Actions/importMovieActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { setAddMovieDefault } from 'Store/Actions/addMovieActions';
|
||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||
import ImportMovie from './ImportMovie';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.rootFolders,
|
||||
(state) => state.addMovie,
|
||||
(state) => state.importMovie,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(
|
||||
match,
|
||||
rootFolders,
|
||||
addMovie,
|
||||
importMovieState,
|
||||
qualityProfiles,
|
||||
) => {
|
||||
const {
|
||||
isFetching: rootFoldersFetching,
|
||||
isPopulated: rootFoldersPopulated,
|
||||
error: rootFoldersError,
|
||||
items
|
||||
} = rootFolders;
|
||||
|
||||
const rootFolderId = parseInt(match.params.rootFolderId);
|
||||
|
||||
const result = {
|
||||
rootFolderId,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
qualityProfiles: qualityProfiles.items,
|
||||
defaultQualityProfileId: addMovie.defaults.qualityProfileId
|
||||
};
|
||||
|
||||
if (items.length) {
|
||||
const rootFolder = _.find(items, { id: rootFolderId });
|
||||
|
||||
return {
|
||||
...result,
|
||||
...rootFolder,
|
||||
items: importMovieState.items
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetImportMovieValue: setImportMovieValue,
|
||||
dispatchImportMovie: importMovie,
|
||||
dispatchClearImportMovie: clearImportMovie,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchSetAddSeriesDefault: setAddMovieDefault
|
||||
};
|
||||
|
||||
class ImportMovieConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
qualityProfiles,
|
||||
defaultQualityProfileId,
|
||||
dispatchFetchRootFolders,
|
||||
dispatchSetAddSeriesDefault
|
||||
} = this.props;
|
||||
|
||||
if (!this.props.rootFoldersPopulated) {
|
||||
dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
let setDefaults = false;
|
||||
const setDefaultPayload = {};
|
||||
|
||||
if (
|
||||
!defaultQualityProfileId ||
|
||||
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
|
||||
) {
|
||||
setDefaults = true;
|
||||
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
dispatchSetAddSeriesDefault(setDefaultPayload);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearImportMovie();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.dispatchSetAddSeriesDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.dispatchSetImportMovieValue({
|
||||
id,
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.dispatchImportMovie({ ids });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportMovie
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routeMatchShape = createRouteMatchShape({
|
||||
rootFolderId: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
ImportMovieConnector.propTypes = {
|
||||
match: routeMatchShape.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
defaultQualityProfileId: PropTypes.number.isRequired,
|
||||
dispatchSetImportMovieValue: PropTypes.func.isRequired,
|
||||
dispatchImportMovie: PropTypes.func.isRequired,
|
||||
dispatchClearImportMovie: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchSetAddSeriesDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector);
|
||||
@@ -0,0 +1,33 @@
|
||||
.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;
|
||||
}
|
||||
|
||||
.loadingButton {
|
||||
composes: importButton;
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
|
||||
margin: 0 10px 0 12px;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
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 './ImportMovieFooter.css';
|
||||
|
||||
const MIXED = 'mixed';
|
||||
|
||||
class ImportMovieFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId
|
||||
} = 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 (!_.isEmpty(newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
this.props.onInputChange({ name, value });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedCount,
|
||||
isImporting,
|
||||
isLookingUpMovie,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed,
|
||||
onImportPress,
|
||||
onCancelLookupPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor,
|
||||
qualityProfileId
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MOVIE_MONITORED_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>
|
||||
|
||||
<div>
|
||||
<div className={styles.label}>
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.importButtonContainer}>
|
||||
<SpinnerButton
|
||||
className={styles.importButton}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isImporting}
|
||||
isDisabled={!selectedCount || isLookingUpMovie}
|
||||
onPress={onImportPress}
|
||||
>
|
||||
Import {selectedCount} {selectedCount > 1 ? 'Movies' : 'Movie'}
|
||||
</SpinnerButton>
|
||||
|
||||
{
|
||||
isLookingUpMovie &&
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
kind={kinds.WARNING}
|
||||
onPress={onCancelLookupPress}
|
||||
>
|
||||
Cancel Processing
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpMovie &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={24}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpMovie &&
|
||||
'Processing Folders'
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieFooter.propTypes = {
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
isImporting: PropTypes.bool.isRequired,
|
||||
isLookingUpMovie: PropTypes.bool.isRequired,
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
isMonitorMixed: PropTypes.bool.isRequired,
|
||||
isQualityProfileIdMixed: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired,
|
||||
onCancelLookupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportMovieFooter;
|
||||
@@ -0,0 +1,50 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { cancelLookupMovie } from 'Store/Actions/importMovieActions';
|
||||
import ImportMovieFooter from './ImportMovieFooter';
|
||||
|
||||
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.addMovie,
|
||||
(state) => state.importMovie,
|
||||
(state, { selectedIds }) => selectedIds,
|
||||
(addMovie, importMovie, selectedIds) => {
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId
|
||||
} = addMovie.defaults;
|
||||
|
||||
const {
|
||||
isLookingUpMovie,
|
||||
isImporting,
|
||||
items
|
||||
} = importMovie;
|
||||
|
||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
isLookingUpMovie,
|
||||
isImporting,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
isMonitorMixed,
|
||||
isQualityProfileIdMixed
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onCancelLookupPress: cancelLookupMovie
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieFooter);
|
||||
@@ -0,0 +1,30 @@
|
||||
.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 {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.movie {
|
||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.detailsIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import styles from './ImportMovieHeader.css';
|
||||
|
||||
function ImportMovieHeader(props) {
|
||||
const {
|
||||
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
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.qualityProfile}
|
||||
name="qualityProfileId"
|
||||
>
|
||||
Quality Profile
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.movie}
|
||||
name="movie"
|
||||
>
|
||||
Movie
|
||||
</VirtualTableHeaderCell>
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
ImportMovieHeader.propTypes = {
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportMovieHeader;
|
||||
@@ -0,0 +1,31 @@
|
||||
.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 {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 250px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.series {
|
||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||
|
||||
flex: 0 1 400px;
|
||||
min-width: 300px;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
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 ImportMovieSelectMovieConnector from './SelectMovie/ImportMovieSelectMovieConnector';
|
||||
import styles from './ImportMovieRow.css';
|
||||
|
||||
function ImportMovieRow(props) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
selectedMovie,
|
||||
isExistingMovie,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
onInputChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableRow style={style}>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={!selectedMovie || isExistingMovie}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<VirtualTableRowCell className={styles.folder}>
|
||||
{id}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.monitor}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.MOVIE_MONITORED_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={styles.series}>
|
||||
<ImportMovieSelectMovieConnector
|
||||
id={id}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
ImportMovieRow.propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
selectedMovie: PropTypes.object,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportMovieRow.defaultsProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
export default ImportMovieRow;
|
||||
@@ -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 { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import ImportMovieRow from './ImportMovieRow';
|
||||
|
||||
function createImportMovieItemSelector() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state) => state.importMovie.items,
|
||||
(id, items) => {
|
||||
return _.find(items, { id }) || {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createImportMovieItemSelector(),
|
||||
createAllMoviesSelector(),
|
||||
(item, movies) => {
|
||||
const selectedMovie = item && item.selectedMovie;
|
||||
const isExistingMovie = !!selectedMovie && _.some(movies, { tmdbId: selectedMovie.tmdbId });
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExistingMovie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupMovie,
|
||||
setImportMovieValue
|
||||
};
|
||||
|
||||
class ImportMovieRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setImportMovieValue({
|
||||
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
|
||||
} = this.props;
|
||||
|
||||
if (!items || !monitor) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImportMovieRow
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onMovieSelect={this.onMovieSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieRowConnector.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
queueLookupMovie: PropTypes.func.isRequired,
|
||||
setImportMovieValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieRowConnector);
|
||||
@@ -0,0 +1,3 @@
|
||||
.input {
|
||||
composes: input from 'Components/Form/CheckInput.css';
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import ImportMovieHeader from './ImportMovieHeader';
|
||||
import ImportMovieRowConnector from './ImportMovieRowConnector';
|
||||
|
||||
class ImportMovieTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
unmappedFolders,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
onMovieLookup,
|
||||
onSetImportMovieValue
|
||||
} = this.props;
|
||||
|
||||
const values = {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId
|
||||
};
|
||||
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const id = unmappedFolder.name;
|
||||
|
||||
onMovieLookup(id, unmappedFolder.path);
|
||||
|
||||
onSetImportMovieValue({
|
||||
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 selectedMovie = item.selectedMovie;
|
||||
const isSelected = selectedState[id];
|
||||
|
||||
const isExistingMovie = !!selectedMovie &&
|
||||
_.some(prevProps.allMovies, { tmdbId: selectedMovie.tmdbId });
|
||||
|
||||
// Props doesn't have a selected series or
|
||||
// the selected series is an existing series.
|
||||
if ((!selectedMovie && prevItem.selectedMovie) || (isExistingMovie && !prevItem.selectedMovie)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// State is selected, but a series isn't selected or
|
||||
// the selected series is an existing series.
|
||||
if (isSelected && (!selectedMovie || isExistingMovie)) {
|
||||
onSelectedChange({ id, value: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// A series is being selected that wasn't previously selected.
|
||||
if (selectedMovie && selectedMovie !== prevItem.selectedMovie) {
|
||||
onSelectedChange({ id, value: true });
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
rootFolderId,
|
||||
items,
|
||||
selectedState,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<ImportMovieRowConnector
|
||||
key={key}
|
||||
style={style}
|
||||
rootFolderId={rootFolderId}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
contentBody,
|
||||
scrollTop,
|
||||
selectedState,
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTable
|
||||
items={items}
|
||||
contentBody={contentBody}
|
||||
isSmallScreen={isSmallScreen}
|
||||
rowHeight={52}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<ImportMovieHeader
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieTable.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
defaultMonitor: PropTypes.string.isRequired,
|
||||
defaultQualityProfileId: PropTypes.number,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allMovies: PropTypes.arrayOf(PropTypes.object),
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||
onMovieLookup: PropTypes.func.isRequired,
|
||||
onSetImportMovieValue: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportMovieTable;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import ImportMovieTable from './ImportMovieTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addMovie,
|
||||
(state) => state.importMovie,
|
||||
(state) => state.app.dimensions,
|
||||
createAllMoviesSelector(),
|
||||
(addMovie, importMovie, dimensions, allMovies) => {
|
||||
return {
|
||||
defaultMonitor: addMovie.defaults.monitor,
|
||||
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
|
||||
items: importMovie.items,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
allMovies
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onMovieLookup(name, path) {
|
||||
dispatch(queueLookupMovie({
|
||||
name,
|
||||
path,
|
||||
term: name
|
||||
}));
|
||||
},
|
||||
|
||||
onSetImportMovieValue(values) {
|
||||
dispatch(setImportMovieValue(values));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportMovieTable);
|
||||
@@ -0,0 +1,8 @@
|
||||
.series {
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSearchResult.css';
|
||||
|
||||
class ImportMovieSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tmdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
studio,
|
||||
isExistingMovie
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.series}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieSearchResult.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportMovieSearchResult;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||
import ImportMovieSearchResult from './ImportMovieSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
(isExistingMovie) => {
|
||||
return {
|
||||
isExistingMovie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ImportMovieSearchResult);
|
||||
@@ -0,0 +1,70 @@
|
||||
.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: input from 'Components/Form/TextInput.css';
|
||||
|
||||
border-radius: 0;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
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 FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSelectMovie.css';
|
||||
|
||||
const tetherOptions = {
|
||||
skipMoveElement: true,
|
||||
constraints: [
|
||||
{
|
||||
to: 'window',
|
||||
attachment: 'together',
|
||||
pin: true
|
||||
}
|
||||
],
|
||||
attachment: 'top center',
|
||||
targetAttachment: 'bottom center'
|
||||
};
|
||||
|
||||
class ImportMovieSelectMovie extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._movieLookupTimeout = 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._movieLookupTimeout) {
|
||||
clearTimeout(this._movieLookupTimeout);
|
||||
}
|
||||
|
||||
this.setState({ term: value }, () => {
|
||||
this._movieLookupTimeout = setTimeout(() => {
|
||||
this.props.onSearchInputChange(value);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.onSearchInputChange(this.state.term);
|
||||
}
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onMovieSelect(tmdbId);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedMovie,
|
||||
isExistingMovie,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
queued,
|
||||
isLookingUpMovie
|
||||
} = 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}
|
||||
>
|
||||
{
|
||||
isLookingUpMovie && queued && !isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedMovie && isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && selectedMovie &&
|
||||
<ImportMovieTitle
|
||||
title={selectedMovie.title}
|
||||
year={selectedMovie.year}
|
||||
network={selectedMovie.network}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !selectedMovie &&
|
||||
<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}>
|
||||
<Icon name={icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name={`${name}_textInput`}
|
||||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
<Icon name={icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportMovieSearchResultConnector
|
||||
key={item.tmdbId}
|
||||
tmdbId={item.tmdbId}
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
studio={item.studio}
|
||||
onPress={this.onMovieSelect}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</TetherComponent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieSelectMovie.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
selectedMovie: PropTypes.object,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
isLookingUpMovie: PropTypes.bool.isRequired,
|
||||
onSearchInputChange: PropTypes.func.isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportMovieSelectMovie.defaultProps = {
|
||||
isFetching: true,
|
||||
isPopulated: false,
|
||||
items: [],
|
||||
queued: true
|
||||
};
|
||||
|
||||
export default ImportMovieSelectMovie;
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
|
||||
import createImportMovieItemSelector from 'Store/Selectors/createImportMovieItemSelector';
|
||||
import ImportMovieSelectMovie from './ImportMovieSelectMovie';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.importMovie.isLookingUpMovie,
|
||||
createImportMovieItemSelector(),
|
||||
(isLookingUpMovie, item) => {
|
||||
return {
|
||||
isLookingUpMovie,
|
||||
...item
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupMovie,
|
||||
setImportMovieValue
|
||||
};
|
||||
|
||||
class ImportMovieSelectMovieConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchInputChange = (term) => {
|
||||
this.props.queueLookupMovie({
|
||||
name: this.props.id,
|
||||
term,
|
||||
topOfQueue: true
|
||||
});
|
||||
}
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
const {
|
||||
id,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
this.props.setImportMovieValue({
|
||||
id,
|
||||
selectedMovie: _.find(items, { tmdbId })
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportMovieSelectMovie
|
||||
{...this.props}
|
||||
onSearchInputChange={this.onSearchInputChange}
|
||||
onMovieSelect={this.onMovieSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovieSelectMovieConnector.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedMovie: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
queueLookupMovie: PropTypes.func.isRequired,
|
||||
setImportMovieValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieSelectMovieConnector);
|
||||
@@ -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 './ImportMovieTitle.css';
|
||||
|
||||
function ImportMovieTitle(props) {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
studio,
|
||||
isExistingMovie
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!!studio &&
|
||||
<Label>{studio}</Label>
|
||||
}
|
||||
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImportMovieTitle.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
isExistingMovie: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ImportMovieTitle;
|
||||
Reference in New Issue
Block a user