New: Project Aphrodite

This commit is contained in:
Qstick
2018-11-23 02:04:42 -05:00
parent 65efa15551
commit 8430cb40ab
1080 changed files with 73015 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,168 @@
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'
},
{
name: 'actions',
label: ''
}
];
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,
onRemoveRecentFolderPress,
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}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/>
);
})
}
</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,
onRemoveRecentFolderPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default InteractiveImportSelectFolderModalContent;

View File

@@ -0,0 +1,80 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRecentFolder, removeRecentFolder } 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,
removeRecentFolder,
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);
}
onRemoveRecentFolderPress = (folder) => {
this.props.removeRecentFolder({ folder });
}
//
// Render
render() {
if (this.path) {
return null;
}
return (
<InteractiveImportSelectFolderModalContent
{...this.props}
onQuickImportPress={this.onQuickImportPress}
onInteractiveImportPress={this.onInteractiveImportPress}
onRemoveRecentFolderPress={this.onRemoveRecentFolderPress}
/>
);
}
}
InteractiveImportSelectFolderModalContentConnector.propTypes = {
path: PropTypes.string,
onFolderSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
addRecentFolder: PropTypes.func.isRequired,
removeRecentFolder: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportSelectFolderModalContentConnector);

View File

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

View File

@@ -0,0 +1,64 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import TableRowButton from 'Components/Table/TableRowButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import styles from './RecentFolderRow.css';
class RecentFolderRow extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.folder);
}
onRemovePress = (event) => {
event.stopPropagation();
const {
folder,
onRemoveRecentFolderPress
} = this.props;
onRemoveRecentFolderPress(folder);
}
//
// Render
render() {
const {
folder,
lastUsed
} = this.props;
return (
<TableRowButton onPress={this.onPress}>
<TableRowCell>{folder}</TableRowCell>
<RelativeDateCellConnector date={lastUsed} />
<TableRowCell className={styles.actions}>
<IconButton
title="Remove"
name={icons.REMOVE}
onPress={this.onRemovePress}
/>
</TableRowCell>
</TableRowButton>
);
}
}
RecentFolderRow.propTypes = {
folder: PropTypes.string.isRequired,
lastUsed: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired,
onRemoveRecentFolderPress: PropTypes.func.isRequired
};
export default RecentFolderRow;

View File

@@ -0,0 +1,73 @@
.filterContainer {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.filterText {
margin-left: 5px;
}
.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 {
.leftButtons,
.centerButtons,
.rightButtons {
flex-direction: column;
}
.leftButtons {
align-items: flex-start;
}
.centerButtons {
align-items: center;
}
.rightButtons {
align-items: flex-end;
}
a,
button {
margin-left: 0;
&:first-child {
margin-bottom: 5px;
}
}
}
}

View File

@@ -0,0 +1,361 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, 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 Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import SelectedMenuItem from 'Components/Menu/SelectedMenuItem';
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 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: 'quality',
label: 'Quality',
isSortable: true,
isVisible: true
},
{
name: 'language',
label: 'Language',
isSortable: true,
isVisible: true
},
{
name: 'size',
label: 'Size',
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER
}),
isVisible: true
}
];
const filterExistingFilesOptions = {
ALL: 'all',
NEW: 'new'
};
class InteractiveImportModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
isSelectSeriesModalOpen: 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.props.importMode);
}
onFilterExistingFilesChange = (value) => {
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
}
onImportModeChange = ({ value }) => {
this.props.onImportModeChange(value);
}
onSelectSeriesPress = () => {
this.setState({ isSelectSeriesModalOpen: true });
}
onSelectSeriesModalClose = () => {
this.setState({ isSelectSeriesModalOpen: false });
}
//
// Render
render() {
const {
downloadId,
allowSeriesChange,
showFilterExistingFiles,
showImportMode,
filterExistingFiles,
title,
folder,
isFetching,
isPopulated,
error,
items,
sortKey,
sortDirection,
importMode,
interactiveImportErrorMessage,
onSortPress,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
invalidRowsSelected,
isSelectSeriesModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
const errorMessage = getErrorMessage(error, '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>
{
showFilterExistingFiles &&
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.FILTER}
size={22}
/>
<div className={styles.filterText}>
{
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
isSelected={!filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
All Files
</SelectedMenuItem>
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
isSelected={filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
Unmapped Files Only
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
}
{
isFetching &&
<LoadingIndicator />
}
{
error &&
<div>{errorMessage}</div>
}
{
isPopulated && !!items.length && !isFetching && !isFetching &&
<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}
allowSeriesChange={allowSeriesChange}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
);
})
}
</TableBody>
</Table>
}
{
isPopulated && !items.length && !isFetching &&
'No video files were found in the selected folder'
}
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.leftButtons}>
{
!downloadId && showImportMode &&
<SelectInput
className={styles.importMode}
name="importMode"
value={importMode}
values={importModeOptions}
onChange={this.onImportModeChange}
/>
}
</div>
<div className={styles.centerButtons}>
{
allowSeriesChange &&
<Button onPress={this.onSelectSeriesPress}>
Select Series
</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}
/>
</ModalContent>
);
}
}
InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string,
allowSeriesChange: PropTypes.bool.isRequired,
showImportMode: PropTypes.bool.isRequired,
showFilterExistingFiles: PropTypes.bool.isRequired,
filterExistingFiles: PropTypes.bool.isRequired,
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,
onFilterExistingFilesChange: PropTypes.func.isRequired,
onImportModeChange: PropTypes.func.isRequired,
onImportSelectedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModalContent.defaultProps = {
allowSeriesChange: true,
showFilterExistingFiles: false,
showImportMode: true,
importMode: 'move'
};
export default InteractiveImportModalContent;

View File

@@ -0,0 +1,203 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
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'),
(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,
filterExistingFiles: true
};
}
componentDidMount() {
const {
downloadId,
folder
} = this.props;
const {
filterExistingFiles
} = this.state;
this.props.fetchInteractiveImportItems({
downloadId,
folder,
filterExistingFiles
});
}
componentDidUpdate(prevProps, prevState) {
const {
filterExistingFiles
} = this.state;
if (prevState.filterExistingFiles !== filterExistingFiles) {
const {
downloadId,
folder
} = this.props;
this.props.fetchInteractiveImportItems({
downloadId,
folder,
filterExistingFiles
});
}
}
componentWillUnmount() {
this.props.clearInteractiveImport();
}
//
// Listeners
onSortPress = (sortKey, sortDirection) => {
this.props.setInteractiveImportSort({ sortKey, sortDirection });
}
onFilterExistingFilesChange = (filterExistingFiles) => {
this.setState({ filterExistingFiles });
}
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,
language
} = 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;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
return false;
}
if (!language) {
this.setState({ interactiveImportErrorMessage: 'Language must be chosen for each selected file' });
return false;
}
files.push({
path: item.path,
folderName: item.folderName,
seriesId: series.id,
episodeIds: _.map(episodes, 'id'),
quality,
language,
downloadId: this.props.downloadId
});
}
});
if (!files.length) {
return;
}
this.props.executeCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode
});
this.props.onModalClose();
}
//
// Render
render() {
const {
interactiveImportErrorMessage,
filterExistingFiles
} = this.state;
return (
<InteractiveImportModalContent
{...this.props}
interactiveImportErrorMessage={interactiveImportErrorMessage}
filterExistingFiles={filterExistingFiles}
onSortPress={this.onSortPress}
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
onImportModeChange={this.onImportModeChange}
onImportSelectedPress={this.onImportSelectedPress}
/>
);
}
}
InteractiveImportModalContentConnector.propTypes = {
downloadId: PropTypes.string,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
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
};
InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true
};
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);

View File

@@ -0,0 +1,18 @@
.relativePath {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
word-break: break-all;
}
.quality,
.language {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
text-align: center;
}
.label {
composes: label from 'Components/Label.css';
cursor: pointer;
}

View File

@@ -0,0 +1,251 @@
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 MovieQuality from 'Movie/MovieQuality';
// import EpisodeLanguage from 'Episode/EpisodeLanguage';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
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,
isSelectQualityModalOpen: false
};
}
componentDidMount() {
const {
id,
series,
quality
} = this.props;
if (
series &&
quality
) {
this.props.onSelectedChange({ id, value: true });
}
}
componentDidUpdate(prevProps) {
const {
id,
series,
quality,
isSelected,
onValidRowChange
} = this.props;
if (
prevProps.series === series &&
prevProps.quality === quality &&
prevProps.isSelected === isSelected
) {
return;
}
const isValid = !!(
series &&
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 });
}
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
}
onSelectSeriesModalClose = (changed) => {
this.setState({ isSelectSeriesModalOpen: false });
this.selectRowAfterChange(changed);
}
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
}
//
// Render
render() {
const {
id,
allowSeriesChange,
relativePath,
series,
quality,
size,
rejections,
isSelected,
onSelectedChange
} = this.props;
const {
isSelectSeriesModalOpen,
isSelectQualityModalOpen
} = this.state;
const seriesTitle = series ? series.title : '';
const showSeriesPlaceholder = isSelected && !series;
const showQualityPlaceholder = isSelected && !quality;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCellButton
isDisabled={!allowSeriesChange}
onPress={this.onSelectSeriesPress}
>
{
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
onPress={this.onSelectQualityPress}
>
{
showQualityPlaceholder &&
<InteractiveImportRowCellPlaceholder />
}
{
!showQualityPlaceholder && !!quality &&
<MovieQuality
className={styles.label}
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}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
id={id}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
</TableRow>
);
}
}
InteractiveImportRow.propTypes = {
id: PropTypes.number.isRequired,
allowSeriesChange: PropTypes.bool.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;

View File

@@ -0,0 +1,7 @@
.placeholder {
display: inline-block;
margin: -8px 0;
width: 100%;
height: 25px;
border: 2px dashed $dangerColor;
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
function InteractiveImportRowCellPlaceholder() {
return (
<span className={styles.placeholder} />
);
}
export default InteractiveImportRowCellPlaceholder;

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

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

View File

@@ -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(({ id, name }) => {
return {
key: id,
value: 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;

View File

@@ -0,0 +1,95 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities';
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 {
isSchemaFetching: isFetching,
isSchemaPopulated: isPopulated,
schemaError: error,
schema
} = qualityProfiles;
return {
isFetching,
isPopulated,
error,
items: getQualities(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.id === qualityId);
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);

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

View File

@@ -0,0 +1,18 @@
.modalBody {
composes: modalBody from 'Components/Modal/ModalBody.css';
display: flex;
flex: 1 1 auto;
flex-direction: column;
}
.filterInput {
composes: input from 'Components/Form/TextInput.css';
flex: 0 0 auto;
margin-bottom: 20px;
}
.scroller {
flex: 1 1 auto;
}

View File

@@ -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,
onMovieSelect,
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}
onMovieSelect={onMovieSelect}
/>
) :
null;
})
}
</Scroller>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectSeriesModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMovieSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectSeriesModalContent;

View File

@@ -0,0 +1,75 @@
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 createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import SelectSeriesModalContent from './SelectSeriesModalContent';
function createMapStateToProps() {
return createSelector(
createAllMoviesSelector(),
(items) => {
return {
items: items.sort((a, b) => {
if (a.sortTitle < b.sortTitle) {
return -1;
}
if (a.sortTitle > b.sortTitle) {
return 1;
}
return 0;
})
};
}
);
}
const mapDispatchToProps = {
updateInteractiveImportItem
};
class SelectSeriesModalContentConnector extends Component {
//
// Listeners
onMovieSelect = (seriesId) => {
const series = _.find(this.props.items, { id: seriesId });
this.props.ids.forEach((id) => {
this.props.updateInteractiveImportItem({
id,
series,
seasonNumber: undefined,
episodes: []
});
});
this.props.onModalClose(true);
}
//
// Render
render() {
return (
<SelectSeriesModalContent
{...this.props}
onMovieSelect={this.onMovieSelect}
/>
);
}
}
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);

View File

@@ -0,0 +1,4 @@
.series {
padding: 8px;
border-bottom: 1px solid $borderColor;
}

View 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.onMovieSelect(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,
onMovieSelect: PropTypes.func.isRequired
};
export default SelectSeriesRow;