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

Convert Manual Import to Typescript

This commit is contained in:
Qstick
2023-05-02 23:07:12 -05:00
parent d2112f2bdc
commit eeb997430c
130 changed files with 3314 additions and 3161 deletions
@@ -1,169 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import PathInputConnector from 'Components/Form/PathInputConnector';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css';
const recentFoldersColumns = [
{
name: 'folder',
label: translate('Folder')
},
{
name: 'lastUsed',
label: translate('LastUsed')
},
{
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>
{translate('ManualImport')} - {translate('SelectFolder')}
</ModalHeader>
<ModalBody>
<PathInputConnector
name="folder"
value={folder}
onChange={this.onPathChange}
/>
{
!!recentFolders.length &&
<div className={styles.recentFoldersContainer}>
<Table
columns={recentFoldersColumns}
>
<TableBody>
{
recentFolders.slice(0).reverse().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}
/>
{translate('QuickImport')}
</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}
/>
{translate('InteractiveImport')}
</Button>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('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;
@@ -0,0 +1,172 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import PathInputConnector from 'Components/Form/PathInputConnector';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import {
addRecentFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css';
const recentFoldersColumns = [
{
name: 'folder',
label: translate('Folder'),
},
{
name: 'lastUsed',
label: translate('LastUsed'),
},
{
name: 'actions',
label: '',
},
];
interface InteractiveImportSelectFolderModalContentProps {
modalTitle: string;
onFolderSelect(folder: string): void;
onModalClose(): void;
}
function InteractiveImportSelectFolderModalContent(
props: InteractiveImportSelectFolderModalContentProps
) {
const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState('');
const dispatch = useDispatch();
const recentFolders = useSelector(
createSelector(
(state: AppState) => state.interactiveImport.recentFolders,
(recentFolders) => {
return recentFolders;
}
)
);
const onPathChange = useCallback(
({ value }: { value: string }) => {
setFolder(value);
},
[setFolder]
);
const onRecentPathPress = useCallback(
(value: string) => {
setFolder(value);
},
[setFolder]
);
const onQuickImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));
dispatch(
executeCommand({
name: commandNames.DOWNLOADED_MOVIES_SCAN,
path: folder,
})
);
onModalClose();
}, [folder, onModalClose, dispatch]);
const onInteractiveImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));
onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectFolder')}
</ModalHeader>
<ModalBody>
<PathInputConnector
name="folder"
value={folder}
onChange={onPathChange}
/>
{recentFolders.length ? (
<div className={styles.recentFoldersContainer}>
<Table columns={recentFoldersColumns}>
<TableBody>
{recentFolders
.slice(0)
.reverse()
.map((recentFolder) => {
return (
<RecentFolderRow
key={recentFolder.folder}
folder={recentFolder.folder}
lastUsed={recentFolder.lastUsed}
onPress={onRecentPathPress}
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
/>
);
})}
</TableBody>
</Table>
</div>
) : null}
<div className={styles.buttonsContainer}>
<div className={styles.buttonContainer}>
<Button
className={styles.button}
kind={kinds.PRIMARY}
size={sizes.LARGE}
isDisabled={!folder}
onPress={onQuickImportPress}
>
<Icon className={styles.buttonIcon} name={icons.QUICK} />
{translate('MoveAutomatically')}
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
className={styles.button}
kind={kinds.PRIMARY}
size={sizes.LARGE}
isDisabled={!folder}
onPress={onInteractiveImportPress}
>
<Icon className={styles.buttonIcon} name={icons.INTERACTIVE} />
{translate('InteractiveImport')}
</Button>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default InteractiveImportSelectFolderModalContent;
@@ -1,80 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { addRecentFolder, removeRecentFolder } from 'Store/Actions/interactiveImportActions';
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_MOVIES_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);
@@ -0,0 +1,6 @@
interface RecentFolder {
folder: string;
lastUsed: string;
}
export default RecentFolder;
@@ -0,0 +1,3 @@
type ImportMode = 'auto' | 'move' | 'copy' | 'chooseImportMode';
export default ImportMode;
@@ -26,6 +26,12 @@
justify-content: flex-end;
}
.deleteButton {
composes: button from '~Components/Link/Button.css';
margin-right: 10px;
}
.importMode,
.bulkSelect {
composes: select from '~Components/Form/SelectInput.css';
@@ -2,6 +2,7 @@
// Please do not change this file!
interface CssExports {
'bulkSelect': string;
'deleteButton': string;
'errorMessage': string;
'filterContainer': string;
'filterText': string;
@@ -1,435 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import SelectInput from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
const columns = [
{
name: 'relativePath',
label: translate('RelativePath'),
isSortable: true,
isVisible: true
},
{
name: 'movie',
label: translate('Movie'),
isSortable: true,
isVisible: true
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
isVisible: true
},
{
name: 'quality',
label: translate('Quality'),
isSortable: true,
isVisible: true
},
{
name: 'languages',
label: translate('Languages'),
isSortable: true,
isVisible: true
},
{
name: 'size',
label: translate('Size'),
isSortable: true,
isVisible: true
},
{
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat')
}),
isSortable: true,
isVisible: true
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER
}),
isSortable: true,
isVisible: true
}
];
const filterExistingFilesOptions = {
ALL: translate('All'),
NEW: translate('New')
};
const importModeOptions = [
{ key: 'chooseImportMode', value: translate('ChooseImportMode'), disabled: true },
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') }
];
const SELECT = 'select';
const MOVIE = 'movie';
const LANGUAGE = 'language';
const QUALITY = 'quality';
const RELEASE_GROUP = 'releaseGroup';
class InteractiveImportModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
invalidRowsSelected: [],
selectModalOpen: null
};
}
//
// 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 {
downloadId,
showImportMode,
importMode,
onImportSelectedPress
} = this.props;
const selected = this.getSelectedIds();
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
onImportSelectedPress(selected, finalImportMode);
};
onFilterExistingFilesChange = (value) => {
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
};
onImportModeChange = ({ value }) => {
this.props.onImportModeChange(value);
};
onSelectModalSelect = ({ value }) => {
this.setState({ selectModalOpen: value });
};
onSelectModalClose = () => {
this.setState({ selectModalOpen: null });
};
//
// Render
render() {
const {
downloadId,
allowMovieChange,
showFilterExistingFiles,
showImportMode,
filterExistingFiles,
title,
folder,
isFetching,
isPopulated,
error,
items,
sortKey,
sortDirection,
importMode,
interactiveImportErrorMessage,
onSortPress,
onModalClose
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
invalidRowsSelected,
selectModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
const errorMessage = getErrorMessage(error, translate('UnableToLoadManualImportItems'));
const bulkSelectOptions = [
{ key: SELECT, value: translate('SelectDotDot'), disabled: true },
{ key: LANGUAGE, value: translate('SelectLanguage') },
{ key: QUALITY, value: translate('SelectQuality') },
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
];
if (allowMovieChange) {
bulkSelectOptions.splice(1, 0, {
key: MOVIE,
value: translate('SelectMovie')
});
}
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImport')} - {title || folder}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
{
showFilterExistingFiles &&
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon
name={icons.FILTER}
size={22}
/>
<div className={styles.filterText}>
{
filterExistingFiles ? translate('UnmappedFilesOnly') : translate('AllFiles')
}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
isSelected={!filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
{translate('AllFiles')}
</SelectedMenuItem>
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
isSelected={filterExistingFiles}
onPress={this.onFilterExistingFilesChange}
>
{translate('UnmappedFilesOnly')}
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
}
{
isFetching &&
<LoadingIndicator />
}
{
error &&
<div>{errorMessage}</div>
}
{
isPopulated && !!items.length && !isFetching && !isFetching &&
<Table
columns={columns}
horizontalScroll={true}
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}
allowMovieChange={allowMovieChange}
onSelectedChange={this.onSelectedChange}
onValidRowChange={this.onValidRowChange}
/>
);
})
}
</TableBody>
</Table>
}
{
isPopulated && !items.length && !isFetching &&
translate('NoVideoFilesFoundSelectedFolder')
}
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.leftButtons}>
{
!downloadId && showImportMode ?
<SelectInput
className={styles.importMode}
name="importMode"
value={importMode}
values={importModeOptions}
onChange={this.onImportModeChange}
/> :
null
}
<SelectInput
className={styles.bulkSelect}
name="select"
value={SELECT}
values={bulkSelectOptions}
isDisabled={!selectedIds.length}
onChange={this.onSelectModalSelect}
/>
</div>
<div className={styles.rightButtons}>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
{
interactiveImportErrorMessage &&
<span className={styles.errorMessage}>{interactiveImportErrorMessage}</span>
}
<Button
kind={kinds.SUCCESS}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
onPress={this.onImportSelectedPress}
>
{translate('Import')}
</Button>
</div>
</ModalFooter>
<SelectMovieModal
isOpen={selectModalOpen === MOVIE}
ids={selectedIds}
onModalClose={this.onSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === LANGUAGE}
ids={selectedIds}
languageIds={[0]}
onModalClose={this.onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === QUALITY}
ids={selectedIds}
qualityId={0}
proper={false}
real={false}
onModalClose={this.onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === RELEASE_GROUP}
ids={selectedIds}
releaseGroup=""
onModalClose={this.onSelectModalClose}
/>
</ModalContent>
);
}
}
InteractiveImportModalContent.propTypes = {
downloadId: PropTypes.string,
allowMovieChange: 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 = {
allowMovieChange: true,
showFilterExistingFiles: false,
showImportMode: true,
importMode: 'move'
};
export default InteractiveImportModalContent;
@@ -0,0 +1,787 @@
import { cloneDeep, without } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import * as commandNames from 'Commands/commandNames';
import SelectInput from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 ConfirmModal from 'Components/Modal/ConfirmModal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import { MovieFile } from 'MovieFile/MovieFile';
import { QualityModel } from 'Quality/Quality';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearInteractiveImport,
fetchInteractiveImportItems,
reprocessInteractiveImportItems,
setInteractiveImportMode,
setInteractiveImportSort,
updateInteractiveImportItems,
} from 'Store/Actions/interactiveImportActions';
import {
deleteMovieFiles,
updateMovieFiles,
} from 'Store/Actions/movieFileActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SortCallback } from 'typings/callbacks';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import InteractiveImportRow from './InteractiveImportRow';
import styles from './InteractiveImportModalContent.css';
type SelectType = 'select' | 'movie' | 'releaseGroup' | 'quality' | 'language';
type FilterExistingFiles = 'all' | 'new';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof InteractiveImportRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'relativePath',
label: translate('RelativePath'),
isSortable: true,
isVisible: true,
},
{
name: 'movie',
label: translate('Movie'),
isSortable: true,
isVisible: true,
},
{
name: 'releaseGroup',
label: translate('ReleaseGroup'),
isVisible: true,
},
{
name: 'quality',
label: translate('Quality'),
isSortable: true,
isVisible: true,
},
{
name: 'languages',
label: translate('Languages'),
isSortable: true,
isVisible: true,
},
{
name: 'size',
label: translate('Size'),
isSortable: true,
isVisible: true,
},
{
name: 'customFormats',
label: React.createElement(Icon, {
name: icons.INTERACTIVE,
title: translate('CustomFormat'),
}),
isSortable: true,
isVisible: true,
},
{
name: 'rejections',
label: React.createElement(Icon, {
name: icons.DANGER,
kind: kinds.DANGER,
}),
isSortable: true,
isVisible: true,
},
];
const importModeOptions = [
{
key: 'chooseImportMode',
value: translate('ChooseImportMode'),
disabled: true,
},
{ key: 'move', value: translate('MoveFiles') },
{ key: 'copy', value: translate('HardlinkCopyFiles') },
];
function isSameMovieFile(
file: InteractiveImport,
originalFile?: InteractiveImport
) {
const { movie } = file;
if (!originalFile) {
return false;
}
if (!originalFile.movie || movie?.id !== originalFile.movie.id) {
return false;
}
return true;
}
const movieFilesInfoSelector = createSelector(
(state: AppState) => state.movieFiles.isDeleting,
(state: AppState) => state.movieFiles.deleteError,
(isDeleting, deleteError) => {
return {
isDeleting,
deleteError,
};
}
);
const importModeSelector = createSelector(
(state: AppState) => state.interactiveImport.importMode,
(importMode) => {
return importMode;
}
);
interface InteractiveImportModalContentProps {
downloadId?: string;
movieId?: number;
seasonNumber?: number;
showMovie?: boolean;
allowMovieChange?: boolean;
showDelete?: boolean;
showImportMode?: boolean;
showFilterExistingFiles?: boolean;
title?: string;
folder?: string;
sortKey?: string;
sortDirection?: string;
initialSortKey?: string;
initialSortDirection?: string;
modalTitle: string;
onModalClose(): void;
}
function InteractiveImportModalContent(
props: InteractiveImportModalContentProps
) {
const {
downloadId,
movieId,
seasonNumber,
allowMovieChange = true,
showMovie = true,
showFilterExistingFiles = false,
showDelete = false,
showImportMode = true,
title,
folder,
initialSortKey,
initialSortDirection,
modalTitle,
onModalClose,
} = props;
const {
isFetching,
isPopulated,
error,
items,
originalItems,
sortKey,
sortDirection,
}: InteractiveImportAppState = useSelector(
createClientSideCollectionSelector('interactiveImport')
);
const { isDeleting, deleteError } = useSelector(movieFilesInfoSelector);
const importMode = useSelector(importModeSelector);
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
const [withoutMovieFileIdRowsSelected, setWithoutMovieFileIdRowsSelected] =
useState<number[]>([]);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
useState(false);
const [filterExistingFiles, setFilterExistingFiles] = useState(false);
const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] =
useState<string | null>(null);
const [selectState, setSelectState] = useSelectState();
const [bulkSelectOptions, setBulkSelectOptions] = useState([
{ key: 'select', value: translate('SelectDotDot'), disabled: true },
{ key: 'quality', value: translate('SelectQuality') },
{ key: 'releaseGroup', value: translate('SelectReleaseGroup') },
{ key: 'language', value: translate('SelectLanguage') },
]);
const { allSelected, allUnselected, selectedState } = selectState;
const previousIsDeleting = usePrevious(isDeleting);
const dispatch = useDispatch();
const columns: Column[] = useMemo(() => {
const result: Column[] = cloneDeep(COLUMNS);
if (!showMovie) {
const movieColumn = result.find((c) => c.name === 'movie');
if (movieColumn) {
movieColumn.isVisible = false;
}
}
return result;
}, [showMovie]);
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
useEffect(
() => {
if (allowMovieChange) {
const newBulkSelectOptions = [...bulkSelectOptions];
newBulkSelectOptions.splice(1, 0, {
key: 'movie',
value: translate('SelectMovie'),
});
setBulkSelectOptions(newBulkSelectOptions);
}
if (initialSortKey) {
const sortProps: { sortKey: string; sortDirection?: string } = {
sortKey: initialSortKey,
};
if (initialSortDirection) {
sortProps.sortDirection = initialSortDirection;
}
dispatch(setInteractiveImportSort(sortProps));
}
dispatch(
fetchInteractiveImportItems({
downloadId,
movieId,
seasonNumber,
folder,
filterExistingFiles,
})
);
// returned function will be called on component unmount
return () => {
dispatch(clearInteractiveImport());
};
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
if (!isDeleting && previousIsDeleting && !deleteError) {
onModalClose();
}
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
const onSelectAllChange = useCallback(
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, hasMovieFileId, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
setWithoutMovieFileIdRowsSelected(
hasMovieFileId || !value
? without(withoutMovieFileIdRowsSelected, id)
: [...withoutMovieFileIdRowsSelected, id]
);
},
[
items,
withoutMovieFileIdRowsSelected,
setSelectState,
setWithoutMovieFileIdRowsSelected,
]
);
const onValidRowChange = useCallback(
(id: number, isValid: boolean) => {
if (isValid && invalidRowsSelected.includes(id)) {
setInvalidRowsSelected(without(invalidRowsSelected, id));
} else if (!isValid && !invalidRowsSelected.includes(id)) {
setInvalidRowsSelected([...invalidRowsSelected, id]);
}
},
[invalidRowsSelected, setInvalidRowsSelected]
);
const onDeleteSelectedPress = useCallback(() => {
setIsConfirmDeleteModalOpen(true);
}, [setIsConfirmDeleteModalOpen]);
const onConfirmDelete = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
const movieFileIds = items.reduce((acc: number[], item) => {
if (selectedIds.indexOf(item.id) > -1 && item.movieFileId) {
acc.push(item.movieFileId);
}
return acc;
}, []);
dispatch(deleteMovieFiles({ movieFileIds }));
}, [items, selectedIds, setIsConfirmDeleteModalOpen, dispatch]);
const onConfirmDeleteModalClose = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
}, [setIsConfirmDeleteModalOpen]);
const onImportSelectedPress = useCallback(() => {
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
const existingFiles: Partial<MovieFile>[] = [];
const files: InteractiveImportCommandOptions[] = [];
if (finalImportMode === 'chooseImportMode') {
setInteractiveImportErrorMessage('An import mode must be selected');
return;
}
items.forEach((item) => {
const isSelected = selectedIds.indexOf(item.id) > -1;
if (isSelected) {
const { movie, releaseGroup, quality, languages, movieFileId } = item;
if (!movie) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrMovie')
);
return;
}
if (!quality) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrQuality')
);
return;
}
if (!languages) {
setInteractiveImportErrorMessage(
translate('InteractiveImportErrLanguage')
);
return;
}
setInteractiveImportErrorMessage(null);
if (movieFileId) {
const originalItem = originalItems.find((i) => i.id === item.id);
if (isSameMovieFile(item, originalItem)) {
existingFiles.push({
id: movieFileId,
releaseGroup,
quality,
languages,
});
return;
}
}
files.push({
path: item.path,
folderName: item.folderName,
movieId: movie.id,
releaseGroup,
quality,
languages,
downloadId,
movieFileId,
});
}
});
let shouldClose = false;
if (existingFiles.length) {
dispatch(
updateMovieFiles({
files: existingFiles,
})
);
shouldClose = true;
}
if (files.length) {
dispatch(
executeCommand({
name: commandNames.INTERACTIVE_IMPORT,
files,
importMode: finalImportMode,
})
);
shouldClose = true;
}
if (shouldClose) {
onModalClose();
}
}, [
downloadId,
showImportMode,
importMode,
items,
originalItems,
selectedIds,
onModalClose,
dispatch,
]);
const onSortPress = useCallback<SortCallback>(
(sortKey, sortDirection) => {
dispatch(setInteractiveImportSort({ sortKey, sortDirection }));
},
[dispatch]
);
const onFilterExistingFilesChange = useCallback<
(value: FilterExistingFiles) => void
>(
(value) => {
const filter = value !== 'all';
setFilterExistingFiles(filter);
dispatch(
fetchInteractiveImportItems({
downloadId,
movieId,
folder,
filterExistingFiles: filter,
})
);
},
[downloadId, movieId, folder, setFilterExistingFiles, dispatch]
);
const onImportModeChange = useCallback<
({ value }: { value: ImportMode }) => void
>(
({ value }) => {
dispatch(setInteractiveImportMode({ importMode: value }));
},
[dispatch]
);
const onSelectModalSelect = useCallback<
({ value }: { value: SelectType }) => void
>(
({ value }) => {
setSelectModalOpen(value);
},
[setSelectModalOpen]
);
const onSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onMovieSelect = useCallback(
(movie: Movie) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
movie,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, setSelectModalOpen, dispatch]
);
const onReleaseGroupSelect = useCallback(
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
releaseGroup,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const onLanguagesSelect = useCallback(
(newLanguages: Language[]) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
languages: newLanguages,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const onQualitySelect = useCallback(
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
quality,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
setSelectModalOpen(null);
},
[selectedIds, dispatch]
);
const errorMessage = getErrorMessage(
error,
translate('UnableToLoadManualImportItems')
);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {title || folder}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
{showFilterExistingFiles && (
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
<Icon name={icons.FILTER} size={22} />
<div className={styles.filterText}>
{filterExistingFiles
? translate('UnmappedFilesOnly')
: translate('AllFiles')}
</div>
</MenuButton>
<MenuContent>
<SelectedMenuItem
name={'all'}
isSelected={!filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
{translate('AllFiles')}
</SelectedMenuItem>
<SelectedMenuItem
name={'new'}
isSelected={filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
{translate('UnmappedFilesOnly')}
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
)}
{isFetching ? <LoadingIndicator /> : null}
{error ? <div>{errorMessage}</div> : null}
{isPopulated && !!items.length && !isFetching && !isFetching ? (
<Table
columns={columns}
horizontalScroll={true}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onSelectAllChange={onSelectAllChange}
>
<TableBody>
{items.map((item) => {
return (
<InteractiveImportRow
key={item.id}
isSelected={selectedState[item.id]}
{...item}
allowMovieChange={allowMovieChange}
columns={columns}
modalTitle={modalTitle}
onSelectedChange={onSelectedChange}
onValidRowChange={onValidRowChange}
/>
);
})}
</TableBody>
</Table>
) : null}
{isPopulated && !items.length && !isFetching
? translate('NoVideoFilesFoundSelectedFolder')
: null}
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.leftButtons}>
{showDelete ? (
<SpinnerButton
className={styles.deleteButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={
!selectedIds.length || !!withoutMovieFileIdRowsSelected.length
}
onPress={onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
) : null}
{!downloadId && showImportMode ? (
<SelectInput
className={styles.importMode}
name="importMode"
value={importMode}
values={importModeOptions}
onChange={onImportModeChange}
/>
) : null}
<SelectInput
className={styles.bulkSelect}
name="select"
value={'select'}
values={bulkSelectOptions}
isDisabled={!selectedIds.length}
onChange={onSelectModalSelect}
/>
</div>
<div className={styles.rightButtons}>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
{interactiveImportErrorMessage && (
<span className={styles.errorMessage}>
{interactiveImportErrorMessage}
</span>
)}
<Button
kind={kinds.SUCCESS}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
onPress={onImportSelectedPress}
>
{translate('Import')}
</Button>
</div>
</ModalFooter>
<SelectMovieModal
isOpen={selectModalOpen === 'movie'}
modalTitle={modalTitle}
onMovieSelect={onMovieSelect}
onModalClose={onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === 'releaseGroup'}
releaseGroup=""
modalTitle={modalTitle}
onReleaseGroupSelect={onReleaseGroupSelect}
onModalClose={onSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === 'language'}
languageIds={[0]}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === 'quality'}
qualityId={0}
proper={false}
real={false}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onSelectModalClose}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedMovieFiles')}
message={translate('DeleteSelectedMovieFilesMessage')}
confirmLabel="Delete"
onConfirm={onConfirmDelete}
onCancel={onConfirmDeleteModalClose}
/>
</ModalContent>
);
}
export default InteractiveImportModalContent;
@@ -1,206 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearInteractiveImport, fetchInteractiveImportItems, setInteractiveImportMode, setInteractiveImportSort } from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import translate from 'Utilities/String/translate';
import InteractiveImportModalContent from './InteractiveImportModalContent';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('interactiveImport'),
(interactiveImport) => {
return interactiveImport;
}
);
}
const mapDispatchToProps = {
dispatchFetchInteractiveImportItems: fetchInteractiveImportItems,
dispatchSetInteractiveImportSort: setInteractiveImportSort,
dispatchSetInteractiveImportMode: setInteractiveImportMode,
dispatchClearInteractiveImport: clearInteractiveImport,
dispatchExecuteCommand: executeCommand
};
class InteractiveImportModalContentConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
interactiveImportErrorMessage: null,
filterExistingFiles: true
};
}
componentDidMount() {
const {
downloadId,
movieId,
folder
} = this.props;
const {
filterExistingFiles
} = this.state;
this.props.dispatchFetchInteractiveImportItems({
downloadId,
movieId,
folder,
filterExistingFiles
});
}
componentDidUpdate(prevProps, prevState) {
const {
filterExistingFiles
} = this.state;
if (prevState.filterExistingFiles !== filterExistingFiles) {
const {
downloadId,
movieId,
folder
} = this.props;
this.props.dispatchFetchInteractiveImportItems({
downloadId,
movieId,
folder,
filterExistingFiles
});
}
}
componentWillUnmount() {
this.props.dispatchClearInteractiveImport();
}
//
// Listeners
onSortPress = (sortKey, sortDirection) => {
this.props.dispatchSetInteractiveImportSort({ sortKey, sortDirection });
};
onFilterExistingFilesChange = (filterExistingFiles) => {
this.setState({ filterExistingFiles });
};
onImportModeChange = (importMode) => {
this.props.dispatchSetInteractiveImportMode({ importMode });
};
onImportSelectedPress = (selected, importMode) => {
const {
items
} = this.props;
const files = [];
if (importMode === 'chooseImportMode') {
this.setState({ interactiveImportErrorMessage: 'An import mode must be selected' });
return;
}
items.forEach((item) => {
const isSelected = selected.indexOf(item.id) > -1;
if (isSelected) {
const {
movie,
quality,
languages,
releaseGroup
} = item;
if (!movie) {
this.setState({ interactiveImportErrorMessage: translate('InteractiveImportErrMovie') });
return false;
}
if (!quality) {
this.setState({ interactiveImportErrorMessage: translate('InteractiveImportErrQuality') });
return false;
}
if (!languages) {
this.setState({ interactiveImportErrorMessage: translate('InteractiveImportErrLanguage') });
return false;
}
files.push({
path: item.path,
folderName: item.folderName,
movieId: movie.id,
releaseGroup,
quality,
languages,
downloadId: this.props.downloadId
});
}
});
if (!files.length) {
return;
}
this.props.dispatchExecuteCommand({
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,
movieId: PropTypes.number,
folder: PropTypes.string,
filterExistingFiles: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
dispatchClearInteractiveImport: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
InteractiveImportModalContentConnector.defaultProps = {
filterExistingFiles: true
};
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);
@@ -1,364 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
class InteractiveImportRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isSelectMovieModalOpen: false,
isSelectReleaseGroupModalOpen: false,
isSelectQualityModalOpen: false,
isSelectLanguageModalOpen: false
};
}
componentDidMount() {
const {
id,
movie,
quality,
languages
} = this.props;
if (
movie &&
quality &&
languages
) {
this.props.onSelectedChange({ id, value: true });
}
}
componentDidUpdate(prevProps) {
const {
id,
movie,
quality,
languages,
isSelected,
onValidRowChange
} = this.props;
if (
prevProps.movie === movie &&
prevProps.quality === quality &&
prevProps.languages === languages &&
prevProps.isSelected === isSelected
) {
return;
}
const isValid = !!(
movie &&
quality &&
languages
);
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
onSelectMoviePress = () => {
this.setState({ isSelectMovieModalOpen: true });
};
onSelectReleaseGroupPress = () => {
this.setState({ isSelectReleaseGroupModalOpen: true });
};
onSelectQualityPress = () => {
this.setState({ isSelectQualityModalOpen: true });
};
onSelectLanguagePress = () => {
this.setState({ isSelectLanguageModalOpen: true });
};
onSelectMovieModalClose = (changed) => {
this.setState({ isSelectMovieModalOpen: false });
this.selectRowAfterChange(changed);
};
onSelectReleaseGroupModalClose = (changed) => {
this.setState({ isSelectReleaseGroupModalOpen: false });
this.selectRowAfterChange(changed);
};
onSelectQualityModalClose = (changed) => {
this.setState({ isSelectQualityModalOpen: false });
this.selectRowAfterChange(changed);
};
onSelectLanguageModalClose = (changed) => {
this.setState({ isSelectLanguageModalOpen: false });
this.selectRowAfterChange(changed);
};
//
// Render
render() {
const {
id,
allowMovieChange,
relativePath,
movie,
quality,
languages,
releaseGroup,
size,
customFormats,
rejections,
isReprocessing,
isSelected,
onSelectedChange
} = this.props;
const {
isSelectMovieModalOpen,
isSelectQualityModalOpen,
isSelectLanguageModalOpen,
isSelectReleaseGroupModalOpen
} = this.state;
const movieTitle = movie ? movie.title + ( movie.year > 0 ? ` (${movie.year})` : '') : '';
const showMoviePlaceholder = isSelected && !movie;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages && !isReprocessing;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
<TableRowCell
className={styles.relativePath}
title={relativePath}
>
{relativePath}
</TableRowCell>
<TableRowCellButton
isDisabled={!allowMovieChange}
title={allowMovieChange ? translate('ClickToChangeMovie') : undefined}
onPress={this.onSelectMoviePress}
>
{
showMoviePlaceholder ? <InteractiveImportRowCellPlaceholder /> : movieTitle
}
</TableRowCellButton>
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={this.onSelectReleaseGroupPress}
>
{
showReleaseGroupPlaceholder ?
<InteractiveImportRowCellPlaceholder /> :
releaseGroup
}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
onPress={this.onSelectQualityPress}
>
{
showQualityPlaceholder &&
<InteractiveImportRowCellPlaceholder />
}
{
!showQualityPlaceholder && !!quality &&
<MovieQuality
className={styles.label}
quality={quality}
/>
}
</TableRowCellButton>
<TableRowCellButton
className={styles.languages}
title={translate('ClickToChangeLanguage')}
onPress={this.onSelectLanguagePress}
>
{
showLanguagePlaceholder &&
<InteractiveImportRowCellPlaceholder />
}
{
!showLanguagePlaceholder && !!languages && !isReprocessing ?
<MovieLanguage
className={styles.label}
languages={languages}
/> :
null
}
{
isReprocessing ?
<LoadingIndicator className={styles.reprocessing}
size={20}
/> : null
}
</TableRowCellButton>
<TableRowCell>
{formatBytes(size)}
</TableRowCell>
<TableRowCell>
{
customFormats?.length ?
<Popover
anchor={
<Icon name={icons.INTERACTIVE} />
}
title={translate('Formats')}
body={
<div className={styles.customFormatTooltip}>
<MovieFormats formats={customFormats} />
</div>
}
position={tooltipPositions.LEFT}
/> :
null
}
</TableRowCell>
<TableRowCell>
{
rejections.length ?
<Popover
anchor={
<Icon
name={icons.DANGER}
kind={kinds.DANGER}
/>
}
title={translate('ReleaseRejected')}
body={
<ul>
{
rejections.map((rejection, index) => {
return (
<li key={index}>
{rejection.reason}
</li>
);
})
}
</ul>
}
position={tooltipPositions.LEFT}
canFlip={false}
/> :
null
}
</TableRowCell>
<SelectMovieModal
isOpen={isSelectMovieModalOpen}
ids={[id]}
relativePath={relativePath}
onModalClose={this.onSelectMovieModalClose}
/>
<SelectReleaseGroupModal
isOpen={isSelectReleaseGroupModalOpen}
ids={[id]}
releaseGroup={releaseGroup ?? ''}
onModalClose={this.onSelectReleaseGroupModalClose}
/>
<SelectQualityModal
isOpen={isSelectQualityModalOpen}
ids={[id]}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
onModalClose={this.onSelectQualityModalClose}
/>
<SelectLanguageModal
isOpen={isSelectLanguageModalOpen}
ids={[id]}
languageIds={languages ? languages.map((l) => l.id) : []}
onModalClose={this.onSelectLanguageModalClose}
/>
</TableRow>
);
}
}
InteractiveImportRow.propTypes = {
id: PropTypes.number.isRequired,
allowMovieChange: PropTypes.bool.isRequired,
relativePath: PropTypes.string.isRequired,
movie: PropTypes.object,
quality: PropTypes.object,
languages: PropTypes.arrayOf(PropTypes.object),
releaseGroup: PropTypes.string,
size: PropTypes.number.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
isReprocessing: PropTypes.bool,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onValidRowChange: PropTypes.func.isRequired
};
export default InteractiveImportRow;
@@ -0,0 +1,362 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import { QualityModel } from 'Quality/Quality';
import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
import styles from './InteractiveImportRow.css';
type SelectType = 'movie' | 'releaseGroup' | 'quality' | 'language';
type SelectedChangeProps = SelectStateInputProps & {
hasMovieFileId: boolean;
};
interface InteractiveImportRowProps {
id: number;
allowMovieChange: boolean;
relativePath: string;
movie?: Movie;
releaseGroup?: string;
quality?: QualityModel;
languages?: Language[];
size: number;
customFormats?: object[];
rejections: Rejection[];
columns: Column[];
movieFileId?: number;
isReprocessing?: boolean;
isSelected?: boolean;
modalTitle: string;
onSelectedChange(result: SelectedChangeProps): void;
onValidRowChange(id: number, isValid: boolean): void;
}
function InteractiveImportRow(props: InteractiveImportRowProps) {
const {
id,
allowMovieChange,
relativePath,
movie,
quality,
languages,
releaseGroup,
size,
customFormats,
rejections,
isSelected,
modalTitle,
movieFileId,
columns,
onSelectedChange,
onValidRowChange,
} = props;
const dispatch = useDispatch();
const isMovieColumnVisible = useMemo(
() => columns.find((c) => c.name === 'movie')?.isVisible ?? false,
[columns]
);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
useEffect(
() => {
if (allowMovieChange && movie && quality && languages) {
onSelectedChange({
id,
hasMovieFileId: !!movieFileId,
value: true,
shiftKey: false,
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
const isValid = !!(movie && quality && languages);
if (isSelected && !isValid) {
onValidRowChange(id, false);
} else {
onValidRowChange(id, true);
}
}, [id, movie, quality, languages, isSelected, onValidRowChange]);
const onSelectedChangeWrapper = useCallback(
(result: SelectedChangeProps) => {
onSelectedChange({
...result,
hasMovieFileId: !!movieFileId,
});
},
[movieFileId, onSelectedChange]
);
const selectRowAfterChange = useCallback(() => {
if (!isSelected) {
onSelectedChange({
id,
hasMovieFileId: !!movieFileId,
value: true,
shiftKey: false,
});
}
}, [id, movieFileId, isSelected, onSelectedChange]);
const onSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onSelectMoviePress = useCallback(() => {
setSelectModalOpen('movie');
}, [setSelectModalOpen]);
const onMovieSelect = useCallback(
(movie: Movie) => {
dispatch(
updateInteractiveImportItem({
id,
movie,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectReleaseGroupPress = useCallback(() => {
setSelectModalOpen('releaseGroup');
}, [setSelectModalOpen]);
const onReleaseGroupSelect = useCallback(
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItem({
id,
releaseGroup,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectQualityPress = useCallback(() => {
setSelectModalOpen('quality');
}, [setSelectModalOpen]);
const onQualitySelect = useCallback(
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItem({
id,
quality,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const onSelectLanguagePress = useCallback(() => {
setSelectModalOpen('language');
}, [setSelectModalOpen]);
const onLanguagesSelect = useCallback(
(languages: Language[]) => {
dispatch(
updateInteractiveImportItem({
id,
languages,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
);
const movieTitle = movie ? movie.title : '';
const showMoviePlaceholder = isSelected && !movie;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
const showQualityPlaceholder = isSelected && !quality;
const showLanguagePlaceholder = isSelected && !languages;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChangeWrapper}
/>
<TableRowCell className={styles.relativePath} title={relativePath}>
{relativePath}
</TableRowCell>
{isMovieColumnVisible ? (
<TableRowCellButton
isDisabled={!allowMovieChange}
title={allowMovieChange ? translate('ClickToChangeMovie') : undefined}
onPress={onSelectMoviePress}
>
{showMoviePlaceholder ? (
<InteractiveImportRowCellPlaceholder />
) : (
movieTitle
)}
</TableRowCellButton>
) : null}
<TableRowCellButton
title={translate('ClickToChangeReleaseGroup')}
onPress={onSelectReleaseGroupPress}
>
{showReleaseGroupPlaceholder ? (
<InteractiveImportRowCellPlaceholder isOptional={true} />
) : (
releaseGroup
)}
</TableRowCellButton>
<TableRowCellButton
className={styles.quality}
title={translate('ClickToChangeQuality')}
onPress={onSelectQualityPress}
>
{showQualityPlaceholder && <InteractiveImportRowCellPlaceholder />}
{!showQualityPlaceholder && !!quality && (
<MovieQuality className={styles.label} quality={quality} />
)}
</TableRowCellButton>
<TableRowCellButton
className={styles.languages}
title={translate('ClickToChangeLanguage')}
onPress={onSelectLanguagePress}
>
{showLanguagePlaceholder && <InteractiveImportRowCellPlaceholder />}
{!showLanguagePlaceholder && !!languages && (
<MovieLanguage className={styles.label} languages={languages} />
)}
</TableRowCellButton>
<TableRowCell>{formatBytes(size)}</TableRowCell>
<TableRowCell>
{customFormats?.length ? (
<Popover
anchor={<Icon name={icons.INTERACTIVE} />}
title={translate('Formats')}
body={
<div className={styles.customFormatTooltip}>
<MovieFormats formats={customFormats} />
</div>
}
position={tooltipPositions.LEFT}
/>
) : null}
</TableRowCell>
<TableRowCell>
{rejections.length ? (
<Popover
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
title={translate('ReleaseRejected')}
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection.reason}</li>;
})}
</ul>
}
position={tooltipPositions.LEFT}
canFlip={false}
/>
) : null}
</TableRowCell>
<SelectMovieModal
isOpen={selectModalOpen === 'movie'}
modalTitle={modalTitle}
onMovieSelect={onMovieSelect}
onModalClose={onSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === 'releaseGroup'}
releaseGroup={releaseGroup ?? ''}
modalTitle={modalTitle}
onReleaseGroupSelect={onReleaseGroupSelect}
onModalClose={onSelectModalClose}
/>
<SelectQualityModal
isOpen={selectModalOpen === 'quality'}
qualityId={quality ? quality.quality.id : 0}
proper={quality ? quality.revision.version > 1 : false}
real={quality ? quality.revision.real > 0 : false}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === 'language'}
languageIds={languages ? languages.map((l) => l.id) : []}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
/>
</TableRow>
);
}
export default InteractiveImportRow;
@@ -5,3 +5,7 @@
height: 25px;
border: 2px dashed var(--dangerColor);
}
.optional {
border: 2px dashed var(--gray);
}
@@ -1,6 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'optional': string;
'placeholder': string;
}
export const cssExports: CssExports;
@@ -1,10 +0,0 @@
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
function InteractiveImportRowCellPlaceholder() {
return (
<span className={styles.placeholder} />
);
}
export default InteractiveImportRowCellPlaceholder;
@@ -0,0 +1,22 @@
import classNames from 'classnames';
import React from 'react';
import styles from './InteractiveImportRowCellPlaceholder.css';
interface InteractiveImportRowCellPlaceholderProps {
isOptional?: boolean;
}
function InteractiveImportRowCellPlaceholder(
props: InteractiveImportRowCellPlaceholderProps
) {
return (
<span
className={classNames(
styles.placeholder,
props.isOptional && styles.optional
)}
/>
);
}
export default InteractiveImportRowCellPlaceholder;
@@ -0,0 +1,34 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import Movie from 'Movie/Movie';
import { QualityModel } from 'Quality/Quality';
import Rejection from 'typings/Rejection';
export interface InteractiveImportCommandOptions {
path: string;
folderName: string;
movieId: number;
releaseGroup?: string;
quality: QualityModel;
languages: Language[];
downloadId?: string;
movieFileId?: number;
}
interface InteractiveImport extends ModelBase {
path: string;
relativePath: string;
folderName: string;
name: string;
size: number;
releaseGroup: string;
quality: QualityModel;
languages: Language[];
movie?: Movie;
qualityWeight: number;
customFormats: object[];
rejections: Rejection[];
movieFileId?: number;
}
export default InteractiveImport;
@@ -1,79 +0,0 @@
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}
closeOnBackgroundClick={false}
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;
@@ -0,0 +1,74 @@
import React, { useCallback, useEffect, useState } from 'react';
import Modal from 'Components/Modal/Modal';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import InteractiveImportSelectFolderModalContent from './Folder/InteractiveImportSelectFolderModalContent';
import InteractiveImportModalContent from './Interactive/InteractiveImportModalContent';
interface InteractiveImportModalProps {
isOpen: boolean;
folder?: string;
downloadId?: string;
modalTitle?: string;
onModalClose(): void;
}
function InteractiveImportModal(props: InteractiveImportModalProps) {
const {
isOpen,
folder,
downloadId,
modalTitle = translate('ManualImport'),
onModalClose,
...otherProps
} = props;
const [folderPath, setFolderPath] = useState<string | undefined>(folder);
const previousIsOpen = usePrevious(isOpen);
const onFolderSelect = useCallback(
(path: string) => {
setFolderPath(path);
},
[setFolderPath]
);
useEffect(() => {
setFolderPath(folder);
}, [folder, setFolderPath]);
useEffect(() => {
if (previousIsOpen && !isOpen) {
setFolderPath(folder);
}
}, [folder, previousIsOpen, isOpen, setFolderPath]);
return (
<Modal
isOpen={isOpen}
size={sizes.EXTRA_LARGE}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
{folderPath || downloadId ? (
<InteractiveImportModalContent
{...otherProps}
folder={folderPath}
downloadId={downloadId}
modalTitle={modalTitle}
onModalClose={onModalClose}
/>
) : (
<InteractiveImportSelectFolderModalContent
{...otherProps}
modalTitle={modalTitle}
onFolderSelect={onFolderSelect}
onModalClose={onModalClose}
/>
)}
</Modal>
);
}
export default InteractiveImportModal;
@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import SelectLanguageModalContentConnector from './SelectLanguageModalContentConnector';
class SelectLanguageModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
size={sizes.MEDIUM}
>
<SelectLanguageModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectLanguageModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectLanguageModal;
@@ -0,0 +1,31 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import Language from 'Language/Language';
import SelectLanguageModalContent from './SelectLanguageModalContent';
interface SelectLanguageModalProps {
isOpen: boolean;
languageIds: number[];
modalTitle: string;
onLanguagesSelect(languages: Language[]): void;
onModalClose(): void;
}
function SelectLanguageModal(props: SelectLanguageModalProps) {
const { isOpen, languageIds, modalTitle, onLanguagesSelect, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
<SelectLanguageModalContent
languageIds={languageIds}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectLanguageModal;
@@ -1,153 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectLanguageModalContent.css';
class SelectLanguageModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
languageIds
} = props;
this.state = {
languageIds
};
}
//
// Listeners
onLanguageChange = ({ value, name }) => {
const {
languageIds
} = this.state;
const changedId = parseInt(name);
let newLanguages = languageIds;
if (value) {
newLanguages.push(changedId);
}
if (!value) {
newLanguages = languageIds.filter((i) => i !== changedId);
}
this.setState({ languageIds: newLanguages });
};
onLanguageSelect = () => {
this.props.onLanguageSelect(this.state);
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
onModalClose
} = this.props;
const {
languageIds
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImportSelectLanguage')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadLanguages')}
</div>
}
{
isPopulated && !error &&
<Form>
{
items.map(( language ) => {
return (
<FormGroup
key={language.id}
size={sizes.EXTRA_SMALL}
className={styles.languageInput}
>
<FormLabel>{language.name}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name={language.id.toString()}
value={languageIds.includes(language.id)}
onChange={this.onLanguageChange}
/>
</FormGroup>
);
})
}
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onLanguageSelect}
>
{translate('SelectLanguages')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectLanguageModalContent.propTypes = {
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onLanguageSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
SelectLanguageModalContent.defaultProps = {
languages: []
};
export default SelectLanguageModalContent;
@@ -0,0 +1,126 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Language from 'Language/Language';
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
import translate from 'Utilities/String/translate';
import styles from './SelectLanguageModalContent.css';
interface SelectLanguageModalContentProps {
languageIds: number[];
modalTitle: string;
onLanguagesSelect(languages: Language[]): void;
onModalClose(): void;
}
function createFilteredLanguagesSelector() {
return createSelector(createLanguagesSelector(), (languages) => {
const { isFetching, isPopulated, error, items } =
languages as LanguageSettingsAppState;
const filterItems = ['Any', 'Original'];
const filteredLanguages = items.filter(
(lang: Language) => !filterItems.includes(lang.name)
);
return {
isFetching,
isPopulated,
error,
items: filteredLanguages,
};
});
}
function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
const { modalTitle, onLanguagesSelect, onModalClose } = props;
const { isFetching, isPopulated, error, items } = useSelector(
createFilteredLanguagesSelector()
);
const [languageIds, setLanguageIds] = useState(props.languageIds);
const onLanguageChange = useCallback(
({ name, value }: { name: string; value: boolean }) => {
const changedId = parseInt(name);
let newLanguages = [...languageIds];
if (value) {
newLanguages.push(changedId);
} else {
newLanguages = languageIds.filter((i) => i !== changedId);
}
setLanguageIds(newLanguages);
},
[languageIds, setLanguageIds]
);
const onLanguagesSelectWrapper = useCallback(() => {
const languages = items.filter((lang) => languageIds.includes(lang.id));
onLanguagesSelect(languages);
}, [items, languageIds, onLanguagesSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectLanguage')}
</ModalHeader>
<ModalBody>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<div>{translate('UnableToLoadLanguages')}</div>
) : null}
{isPopulated && !error ? (
<Form>
{items.map((language) => {
return (
<FormGroup
key={language.id}
size={sizes.EXTRA_SMALL}
className={styles.languageInput}
>
<FormLabel>{language.name}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name={language.id.toString()}
value={languageIds.includes(language.id)}
onChange={onLanguageChange}
/>
</FormGroup>
);
})}
</Form>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onLanguagesSelectWrapper}>
{translate('SelectLanguages')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectLanguageModalContent;
@@ -1,107 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import { fetchLanguages } from 'Store/Actions/settingsActions';
import SelectLanguageModalContent from './SelectLanguageModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.languages,
(languages) => {
const {
isFetching,
isPopulated,
error,
items
} = languages;
const filterItems = ['Any', 'Original'];
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
return {
isFetching,
isPopulated,
error,
items: filteredLanguages
};
}
);
}
const mapDispatchToProps = {
dispatchFetchLanguages: fetchLanguages,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems,
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
};
class SelectLanguageModalContentConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.dispatchFetchLanguages();
}
};
//
// Listeners
onLanguageSelect = ({ languageIds }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
const languages = [];
languageIds.forEach((languageId) => {
const language = _.find(this.props.items,
(item) => item.id === parseInt(languageId));
if (language !== undefined) {
languages.push(language);
}
});
dispatchUpdateInteractiveImportItems({
ids,
languages
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectLanguageModalContent
{...this.props}
onLanguageSelect={this.onLanguageSelect}
/>
);
}
}
SelectLanguageModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectLanguageModalContentConnector);
@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectMovieModalContentConnector from './SelectMovieModalContentConnector';
class SelectMovieModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectMovieModal;
@@ -0,0 +1,27 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import Movie from 'Movie/Movie';
import SelectMovieModalContent from './SelectMovieModalContent';
interface SelectMovieModalProps {
isOpen: boolean;
modalTitle: string;
onMovieSelect(movie: Movie): void;
onModalClose(): void;
}
function SelectMovieModal(props: SelectMovieModalProps) {
const { isOpen, modalTitle, onMovieSelect, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectMovieModalContent
modalTitle={modalTitle}
onMovieSelect={onMovieSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectMovieModal;
@@ -1,237 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import FuseWorker from 'Components/Page/Header/fuse.worker';
import Scroller from 'Components/Scroller/Scroller';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import { scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import SelectMovieRow from './SelectMovieRow';
import styles from './SelectMovieModalContent.css';
class SelectMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._worker = null;
this.state = {
scroller: null,
filter: '',
loading: false,
suggestions: props.items
};
}
componentWillUnmount() {
if (this._worker) {
this._worker.removeEventListener('message', this.onSuggestionsReceived, false);
this._worker.terminate();
this._worker = null;
}
}
getWorker() {
if (!this._worker) {
this._worker = new FuseWorker();
this._worker.addEventListener('message', this.onSuggestionsReceived, false);
}
return this._worker;
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
};
rowRenderer = ({ key, rowIndex, style }) => {
const item = this.state.suggestions[rowIndex];
return (
<VirtualTableRow
key={key}
style={style}
>
<SelectMovieRow
key={item.id}
id={item.id}
title={item.title}
year={item.year}
onMovieSelect={this.props.onMovieSelect}
/>
</VirtualTableRow>
);
};
//
// Listeners
onFilterChange = ({ value }) => {
if (value) {
this.setState({
loading: true,
filter: value.toLowerCase()
});
this.requestSuggestions(value);
} else {
this.setState({
loading: false,
filter: '',
suggestions: this.props.items
});
this.requestSuggestions.cancel();
}
};
requestSuggestions = _.debounce((value) => {
if (!this.state.loading) {
return;
}
const requestLoading = this.state.requestLoading;
this.setState({
requestValue: value,
requestLoading: true
});
if (!requestLoading) {
const payload = {
value,
movies: this.props.items
};
this.getWorker().postMessage(payload);
}
}, 250);
onSuggestionsReceived = (message) => {
const {
value,
suggestions
} = message.data;
if (!this.state.loading) {
this.setState({
requestValue: null,
requestLoading: false
});
} else if (value === this.state.requestValue) {
this.setState({
suggestions: suggestions.map((suggestion) => suggestion.item),
requestValue: null,
requestLoading: false,
loading: false
});
} else {
this.setState({
suggestions: suggestions.map((suggestion) => suggestion.item),
requestLoading: true
});
const payload = {
value: this.state.requestValue,
movies: this.props.items
};
this.getWorker().postMessage(payload);
}
};
//
// Render
render() {
const {
relativePath,
onModalClose
} = this.props;
const {
scroller,
filter,
loading,
suggestions
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
<div className={styles.header}>
{translate('ManualImportSelectMovie')}
</div>
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder={translate('FilterPlaceHolder')}
name="filter"
value={filter}
autoFocus={true}
onChange={this.onFilterChange}
/>
<Scroller
registerScroller={this.setScrollerRef}
className={styles.scroller}
autoFocus={false}
>
<div>
{
loading || !scroller ?
<LoadingIndicator /> :
<VirtualTable
header={
<div />
}
items={suggestions}
isSmallScreen={false}
scroller={scroller}
focusScroller={false}
rowRenderer={this.rowRenderer}
/>
}
</div>
</Scroller>
</ModalBody>
<ModalFooter className={styles.footer}>
<div className={styles.path}>{relativePath}</div>
<div className={styles.buttons}>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
</div>
</ModalFooter>
</ModalContent>
);
}
}
SelectMovieModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
relativePath: PropTypes.string.isRequired,
onMovieSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectMovieModalContent;
@@ -0,0 +1,93 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Scroller from 'Components/Scroller/Scroller';
import { scrollDirections } from 'Helpers/Props';
import Movie from 'Movie/Movie';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import SelectMovieRow from './SelectMovieRow';
import styles from './SelectMovieModalContent.css';
interface SelectMovieModalContentProps {
modalTitle: string;
onMovieSelect(movie: Movie): void;
onModalClose(): void;
}
function SelectMovieModalContent(props: SelectMovieModalContentProps) {
const { modalTitle, onMovieSelect, onModalClose } = props;
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
const [filter, setFilter] = useState('');
const onFilterChange = useCallback(
({ value }: { value: string }) => {
setFilter(value);
},
[setFilter]
);
const onMovieSelectWrapper = useCallback(
(movieId: number) => {
const movie = allMovies.find((s) => s.id === movieId) as Movie;
onMovieSelect(movie);
},
[allMovies, onMovieSelect]
);
const items = useMemo(() => {
const sorted = [...allMovies].sort((a, b) =>
a.sortTitle.localeCompare(b.sortTitle)
);
return sorted.filter((item) =>
item.title.toLowerCase().includes(filter.toLowerCase())
);
}, [allMovies, filter]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{modalTitle} - Select Movie</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<TextInput
className={styles.filterInput}
placeholder="Filter movies"
name="filter"
value={filter}
autoFocus={true}
onChange={onFilterChange}
/>
<Scroller className={styles.scroller} autoFocus={false}>
{items.map((item) => {
return (
<SelectMovieRow
key={item.id}
id={item.id}
title={item.title}
year={item.year}
onMovieSelect={onMovieSelectWrapper}
/>
);
})}
</Scroller>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectMovieModalContent;
@@ -1,115 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { reprocessInteractiveImportItems, updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
import SelectMovieModalContent from './SelectMovieModalContent';
function createCleanMovieSelector() {
return createSelector(
createAllMoviesSelector(),
(items) => {
return items.map((movie) => {
const {
id,
title,
titleSlug,
sortTitle,
year,
images,
alternateTitles = []
} = movie;
return {
id,
title,
titleSlug,
sortTitle,
year,
images,
alternateTitles,
firstCharacter: title.charAt(0).toLowerCase()
};
}).sort((a, b) => {
if (a.sortTitle < b.sortTitle) {
return -1;
}
if (a.sortTitle > b.sortTitle) {
return 1;
}
return 0;
});
}
);
}
function createMapStateToProps() {
return createDeepEqualSelector(
createCleanMovieSelector(),
(movies) => {
return {
items: movies
};
}
);
}
const mapDispatchToProps = {
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems,
dispatchUpdateInteractiveImportItem: updateInteractiveImportItem
};
class SelectMovieModalContentConnector extends Component {
//
// Listeners
onMovieSelect = (movieId) => {
const {
ids,
items,
dispatchUpdateInteractiveImportItem,
dispatchReprocessInteractiveImportItems,
onModalClose
} = this.props;
const movie = items.find((s) => s.id === movieId);
ids.forEach((id) => {
dispatchUpdateInteractiveImportItem({
id,
movie
});
});
dispatchReprocessInteractiveImportItems({ ids });
onModalClose(true);
};
//
// Render
render() {
return (
<SelectMovieModalContent
{...this.props}
onMovieSelect={this.onMovieSelect}
/>
);
}
}
SelectMovieModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectMovieModalContentConnector);
@@ -1,37 +0,0 @@
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,41 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { QualityModel } from 'Quality/Quality';
import SelectQualityModalContent from './SelectQualityModalContent';
interface SelectQualityModalProps {
isOpen: boolean;
qualityId: number;
proper: boolean;
real: boolean;
modalTitle: string;
onQualitySelect(quality: QualityModel): void;
onModalClose(): void;
}
function SelectQualityModal(props: SelectQualityModalProps) {
const {
isOpen,
qualityId,
proper,
real,
modalTitle,
onQualitySelect,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectQualityModalContent
qualityId={qualityId}
proper={proper}
real={real}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectQualityModal;
@@ -1,169 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
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>
{translate('ManualImportSelectQuality')}
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadQualities')}
</div>
}
{
isPopulated && !error &&
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={this.onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Proper')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={this.onProperChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Real')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={this.onRealChange}
/>
</FormGroup>
</Form>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onQualitySelect}
>
{translate('SelectQuality')}
</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,186 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { Error } from 'App/State/AppSectionState';
import AppState from 'App/State/AppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Quality, { QualityModel } from 'Quality/Quality';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
import getQualities from 'Utilities/Quality/getQualities';
import translate from 'Utilities/String/translate';
interface QualitySchemaState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: Quality[];
}
function createQualitySchemaSelector() {
return createSelector(
(state: AppState) => state.settings.qualityProfiles,
(qualityProfiles): QualitySchemaState => {
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
qualityProfiles;
const items = getQualities(schema.items) as Quality[];
return {
isFetching: isSchemaFetching,
isPopulated: isSchemaPopulated,
error: schemaError,
items,
};
}
);
}
interface SelectQualityModalContentProps {
qualityId: number;
proper: boolean;
real: boolean;
modalTitle: string;
onQualitySelect(quality: QualityModel): void;
onModalClose(): void;
}
function SelectQualityModalContent(props: SelectQualityModalContentProps) {
const { modalTitle, onQualitySelect, onModalClose } = props;
const [qualityId, setQualityId] = useState(props.qualityId);
const [proper, setProper] = useState(props.proper);
const [real, setReal] = useState(props.real);
const { isFetching, isPopulated, error, items } = useSelector(
createQualitySchemaSelector()
);
const dispatch = useDispatch();
useEffect(
() => {
dispatch(fetchQualityProfileSchema());
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const qualityOptions = useMemo(() => {
return items.map(({ id, name }) => {
return {
key: id,
value: name,
};
});
}, [items]);
const onQualityChange = useCallback(
({ value }: { value: string }) => {
setQualityId(parseInt(value));
},
[setQualityId]
);
const onProperChange = useCallback(
({ value }: CheckInputChanged) => {
setProper(value);
},
[setProper]
);
const onRealChange = useCallback(
({ value }: CheckInputChanged) => {
setReal(value);
},
[setReal]
);
const onQualitySelectWrapper = useCallback(() => {
const quality = items.find((item) => item.id === qualityId) as Quality;
const revision = {
version: proper ? 2 : 1,
real: real ? 1 : 0,
isRepack: false,
};
onQualitySelect({
quality,
revision,
});
}, [items, qualityId, proper, real, onQualitySelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SelectQuality')}
</ModalHeader>
<ModalBody>
{isFetching && <LoadingIndicator />}
{!isFetching && error ? (
<div>{translate('UnableToLoadQualities')}</div>
) : null}
{isPopulated && !error ? (
<Form>
<FormGroup>
<FormLabel>{translate('Quality')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="quality"
value={qualityId}
values={qualityOptions}
onChange={onQualityChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Proper')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="proper"
value={proper}
onChange={onProperChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Real')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="real"
value={real}
onChange={onRealChange}
/>
</FormGroup>
</Form>
) : null}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
<Button kind={kinds.SUCCESS} onPress={onQualitySelectWrapper}>
{translate('SelectQuality')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectQualityModalContent;
@@ -1,105 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import getQualities from 'Utilities/Quality/getQualities';
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 = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems,
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
};
class SelectQualityModalContentConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.dispatchFetchQualityProfileSchema();
}
};
//
// Listeners
onQualitySelect = ({ qualityId, proper, real }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
const quality = _.find(this.props.items,
(item) => item.id === qualityId);
const revision = {
version: proper ? 2 : 1,
real: real ? 1 : 0
};
dispatchUpdateInteractiveImportItems({
ids,
quality: {
quality,
revision
}
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectQualityModalContent
{...this.props}
onQualitySelect={this.onQualitySelect}
/>
);
}
}
SelectQualityModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SelectQualityModalContentConnector);
@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Modal from 'Components/Modal/Modal';
import SelectReleaseGroupModalContentConnector from './SelectReleaseGroupModalContentConnector';
class SelectReleaseGroupModal extends Component {
//
// Render
render() {
const {
isOpen,
onModalClose,
...otherProps
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<SelectReleaseGroupModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
}
SelectReleaseGroupModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModal;
@@ -0,0 +1,34 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
interface SelectReleaseGroupModalProps {
isOpen: boolean;
releaseGroup: string;
modalTitle: string;
onReleaseGroupSelect(releaseGroup: string): void;
onModalClose(): void;
}
function SelectReleaseGroupModal(props: SelectReleaseGroupModalProps) {
const {
isOpen,
releaseGroup,
modalTitle,
onReleaseGroupSelect,
onModalClose,
} = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<SelectReleaseGroupModalContent
releaseGroup={releaseGroup}
modalTitle={modalTitle}
onReleaseGroupSelect={onReleaseGroupSelect}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default SelectReleaseGroupModal;
@@ -1,104 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectReleaseGroupModalContent.css';
class SelectReleaseGroupModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
releaseGroup
} = props;
this.state = {
releaseGroup
};
}
//
// Listeners
onReleaseGroupChange = ({ value }) => {
this.setState({ releaseGroup: value });
};
onReleaseGroupSelect = () => {
this.props.onReleaseGroupSelect(this.state);
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
releaseGroup
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('ManualImportSetReleaseGroup')}
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
autoFocus={true}
onChange={this.onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Cancel')}
</Button>
<Button
kind={kinds.SUCCESS}
onPress={this.onReleaseGroupSelect}
>
{translate('SetReleaseGroup')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
SelectReleaseGroupModalContent.propTypes = {
releaseGroup: PropTypes.string.isRequired,
onReleaseGroupSelect: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default SelectReleaseGroupModalContent;
@@ -0,0 +1,75 @@
import React, { useCallback, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, scrollDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './SelectReleaseGroupModalContent.css';
interface SelectReleaseGroupModalContentProps {
releaseGroup: string;
modalTitle: string;
onReleaseGroupSelect(releaseGroup: string): void;
onModalClose(): void;
}
function SelectReleaseGroupModalContent(
props: SelectReleaseGroupModalContentProps
) {
const { modalTitle, onReleaseGroupSelect, onModalClose } = props;
const [releaseGroup, setReleaseGroup] = useState(props.releaseGroup);
const onReleaseGroupChange = useCallback(
({ value }: { value: string }) => {
setReleaseGroup(value);
},
[setReleaseGroup]
);
const onReleaseGroupSelectWrapper = useCallback(() => {
onReleaseGroupSelect(releaseGroup);
}, [releaseGroup, onReleaseGroupSelect]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{modalTitle} - {translate('SetReleaseGroup')}
</ModalHeader>
<ModalBody
className={styles.modalBody}
scrollDirection={scrollDirections.NONE}
>
<Form>
<FormGroup>
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="releaseGroup"
value={releaseGroup}
autoFocus={true}
onChange={onReleaseGroupChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>Cancel</Button>
<Button kind={kinds.SUCCESS} onPress={onReleaseGroupSelectWrapper}>
{translate('SetReleaseGroup')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default SelectReleaseGroupModalContent;
@@ -1,54 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { reprocessInteractiveImportItems, updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
import SelectReleaseGroupModalContent from './SelectReleaseGroupModalContent';
const mapDispatchToProps = {
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
};
class SelectReleaseGroupModalContentConnector extends Component {
//
// Listeners
onReleaseGroupSelect = ({ releaseGroup }) => {
const {
ids,
dispatchUpdateInteractiveImportItems,
dispatchReprocessInteractiveImportItems
} = this.props;
dispatchUpdateInteractiveImportItems({
ids,
releaseGroup
});
dispatchReprocessInteractiveImportItems({ ids });
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<SelectReleaseGroupModalContent
{...this.props}
onReleaseGroupSelect={this.onReleaseGroupSelect}
/>
);
}
}
SelectReleaseGroupModalContentConnector.propTypes = {
ids: PropTypes.arrayOf(PropTypes.number).isRequired,
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(null, mapDispatchToProps)(SelectReleaseGroupModalContentConnector);