mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-22 22:16:13 -04:00
Convert Manual Import to Typescript
This commit is contained in:
@@ -1,568 +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 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 Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
label: 'Series',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'season',
|
||||
label: 'Season',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'episodes',
|
||||
label: 'Episode(s)',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: 'Release Group',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: 'Languages',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.INTERACTIVE,
|
||||
title: 'Custom Format'
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
const filterExistingFilesOptions = {
|
||||
ALL: 'all',
|
||||
NEW: 'new'
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' }
|
||||
];
|
||||
|
||||
const SELECT = 'select';
|
||||
const SERIES = 'series';
|
||||
const SEASON = 'season';
|
||||
const EPISODE = 'episode';
|
||||
const RELEASE_GROUP = 'releaseGroup';
|
||||
const QUALITY = 'quality';
|
||||
const LANGUAGE = 'language';
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const instanceColumns = _.cloneDeep(columns);
|
||||
|
||||
if (!props.showSeries) {
|
||||
instanceColumns.find((c) => c.name === 'series').isVisible = false;
|
||||
}
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
invalidRowsSelected: [],
|
||||
withoutEpisodeFileIdRowsSelected: [],
|
||||
selectModalOpen: null,
|
||||
columns: instanceColumns,
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (!isDeleting && prevProps.isDeleting && !deleteError) {
|
||||
onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, hasEpisodeFileId, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...toggleSelected(state, this.props.items, id, value, shiftKey),
|
||||
withoutEpisodeFileIdRowsSelected: hasEpisodeFileId || !value ?
|
||||
_.without(state.withoutEpisodeFileIdRowsSelected, id) :
|
||||
[...state.withoutEpisodeFileIdRowsSelected, id]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onValidRowChange = (id, isValid) => {
|
||||
this.setState((state) => {
|
||||
if (isValid) {
|
||||
return {
|
||||
invalidRowsSelected: _.without(state.invalidRowsSelected, id)
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
invalidRowsSelected: [...state.invalidRowsSelected, id]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteSelectedPress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
this.props.onDeleteSelectedPress(this.getSelectedIds());
|
||||
};
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
};
|
||||
|
||||
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,
|
||||
allowSeriesChange,
|
||||
autoSelectRow,
|
||||
showFilterExistingFiles,
|
||||
showDelete,
|
||||
showImportMode,
|
||||
filterExistingFiles,
|
||||
title,
|
||||
folder,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
importMode,
|
||||
interactiveImportErrorMessage,
|
||||
isDeleting,
|
||||
modalTitle,
|
||||
onSortPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
invalidRowsSelected,
|
||||
withoutEpisodeFileIdRowsSelected,
|
||||
selectModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
const orderedSelectedIds = items.reduce((acc, file) => {
|
||||
if (selectedIds.includes(file.id)) {
|
||||
acc.push(file.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const selectedItem = selectedIds.length ?
|
||||
items.find((file) => file.id === selectedIds[0]) :
|
||||
null;
|
||||
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
||||
const bulkSelectOptions = [
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: SEASON, value: 'Select Season' },
|
||||
{ key: EPISODE, value: 'Select Episode(s)' },
|
||||
{ key: QUALITY, value: 'Select Quality' },
|
||||
{ key: RELEASE_GROUP, value: 'Select Release Group' },
|
||||
{ key: LANGUAGE, value: 'Select Language' }
|
||||
];
|
||||
|
||||
if (allowSeriesChange) {
|
||||
bulkSelectOptions.splice(1, 0, {
|
||||
key: SERIES,
|
||||
value: 'Select Series'
|
||||
});
|
||||
}
|
||||
|
||||
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 ? 'Unmapped Files Only' : 'All Files'
|
||||
}
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
All Files
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
Unmapped Files Only
|
||||
</SelectedMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
error &&
|
||||
<div>{errorMessage}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length && !isFetching && !isFetching &&
|
||||
<Table
|
||||
columns={this.state.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}
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
autoSelectRow={autoSelectRow}
|
||||
columns={this.state.columns}
|
||||
modalTitle={modalTitle}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onValidRowChange={this.onValidRowChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length && !isFetching &&
|
||||
'No video files were found in the selected folder'
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.leftButtons}>
|
||||
{
|
||||
showDelete ?
|
||||
<SpinnerButton
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!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}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
{
|
||||
interactiveImportErrorMessage &&
|
||||
<span className={styles.errorMessage}>{interactiveImportErrorMessage}</span>
|
||||
}
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
|
||||
onPress={this.onImportSelectedPress}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={selectModalOpen === SERIES}
|
||||
ids={selectedIds}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={selectModalOpen === SEASON}
|
||||
ids={selectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectEpisodeModal
|
||||
isOpen={selectModalOpen === EPISODE}
|
||||
ids={orderedSelectedIds}
|
||||
seriesId={selectedItem && selectedItem.series && selectedItem.series.id}
|
||||
seasonNumber={selectedItem && selectedItem.seasonNumber}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseGroupModal
|
||||
isOpen={selectModalOpen === RELEASE_GROUP}
|
||||
ids={selectedIds}
|
||||
releaseGroup=""
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={selectModalOpen === LANGUAGE}
|
||||
ids={selectedIds}
|
||||
languageIds={[0]}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={selectModalOpen === QUALITY}
|
||||
ids={selectedIds}
|
||||
qualityId={0}
|
||||
proper={false}
|
||||
real={false}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Selected Episode Files"
|
||||
message={'Are you sure you want to delete the selected episode files?'}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportModalContent.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
showSeries: PropTypes.bool.isRequired,
|
||||
allowSeriesChange: PropTypes.bool.isRequired,
|
||||
autoSelectRow: PropTypes.bool.isRequired,
|
||||
showDelete: 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,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onDeleteSelectedPress: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContent.defaultProps = {
|
||||
showSeries: true,
|
||||
allowSeriesChange: true,
|
||||
autoSelectRow: true,
|
||||
showFilterExistingFiles: false,
|
||||
showDelete: false,
|
||||
showImportMode: true,
|
||||
importMode: 'move'
|
||||
};
|
||||
|
||||
export default InteractiveImportModalContent;
|
||||
@@ -0,0 +1,873 @@
|
||||
import { cloneDeep, without } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
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 SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import ImportMode from 'InteractiveImport/ImportMode';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
deleteEpisodeFiles,
|
||||
updateEpisodeFiles,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import {
|
||||
clearInteractiveImport,
|
||||
fetchInteractiveImportItems,
|
||||
reprocessInteractiveImportItems,
|
||||
setInteractiveImportMode,
|
||||
setInteractiveImportSort,
|
||||
updateInteractiveImportItems,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
|
||||
type SelectType =
|
||||
| 'select'
|
||||
| 'series'
|
||||
| 'season'
|
||||
| 'episode'
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language';
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
label: 'Relative Path',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
label: 'Series',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'season',
|
||||
label: 'Season',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'episodes',
|
||||
label: 'Episode(s)',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseGroup',
|
||||
label: 'Release Group',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: 'Languages',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.INTERACTIVE,
|
||||
title: 'Custom Format',
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
kind: kinds.DANGER,
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
const filterExistingFilesOptions = {
|
||||
ALL: 'all',
|
||||
NEW: 'new',
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' },
|
||||
];
|
||||
|
||||
function isSameEpisodeFile(file, originalFile) {
|
||||
const { series, seasonNumber, episodes } = file;
|
||||
|
||||
if (!originalFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!originalFile.series || series.id !== originalFile.series.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonNumber !== originalFile.seasonNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||
}
|
||||
|
||||
const episodeFilesInfoSelector = createSelector(
|
||||
(state) => state.episodeFiles.isDeleting,
|
||||
(state) => state.episodeFiles.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const importModeSelector = createSelector(
|
||||
(state) => state.interactiveImport.importMode,
|
||||
(importMode) => {
|
||||
return importMode;
|
||||
}
|
||||
);
|
||||
|
||||
interface InteractiveImportModalContentProps {
|
||||
downloadId?: string;
|
||||
seriesId?: number;
|
||||
seasonNumber?: number;
|
||||
showSeries?: boolean;
|
||||
allowSeriesChange?: boolean;
|
||||
autoSelectRow?: 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,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
allowSeriesChange = true,
|
||||
autoSelectRow = true,
|
||||
showSeries = true,
|
||||
showFilterExistingFiles = false,
|
||||
showDelete = false,
|
||||
showImportMode = true,
|
||||
title,
|
||||
folder,
|
||||
initialSortKey,
|
||||
initialSortDirection,
|
||||
modalTitle,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
originalItems,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
} = useSelector(createClientSideCollectionSelector('interactiveImport'));
|
||||
|
||||
const { isDeleting, deleteError } = useSelector(episodeFilesInfoSelector);
|
||||
const importMode = useSelector(importModeSelector);
|
||||
|
||||
const [invalidRowsSelected, setInvalidRowsSelected] = useState([]);
|
||||
const [
|
||||
withoutEpisodeFileIdRowsSelected,
|
||||
setWithoutEpisodeFileIdRowsSelected,
|
||||
] = useState([]);
|
||||
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: 'Select...', disabled: true },
|
||||
{ key: 'season', value: 'Select Season' },
|
||||
{ key: 'episode', value: 'Select Episode(s)' },
|
||||
{ key: 'quality', value: 'Select Quality' },
|
||||
{ key: 'releaseGroup', value: 'Select Release Group' },
|
||||
{ key: 'language', value: 'Select Language' },
|
||||
]);
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
const previousIsDeleting = usePrevious(isDeleting);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const columns: Column[] = useMemo(() => {
|
||||
const result = cloneDeep(COLUMNS);
|
||||
|
||||
if (!showSeries) {
|
||||
result.find((c) => c.name === 'series').isVisible = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [showSeries]);
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (allowSeriesChange) {
|
||||
const newBulkSelectOptions = [...bulkSelectOptions];
|
||||
|
||||
newBulkSelectOptions.splice(1, 0, {
|
||||
key: 'series',
|
||||
value: 'Select Series',
|
||||
});
|
||||
|
||||
setBulkSelectOptions(newBulkSelectOptions);
|
||||
}
|
||||
|
||||
if (initialSortKey) {
|
||||
const sortProps: { sortKey: string; sortDirection?: string } = {
|
||||
sortKey: initialSortKey,
|
||||
};
|
||||
|
||||
if (initialSortDirection) {
|
||||
sortProps.sortDirection = initialSortDirection;
|
||||
}
|
||||
|
||||
dispatch(setInteractiveImportSort(sortProps));
|
||||
}
|
||||
|
||||
dispatch(
|
||||
fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
seriesId,
|
||||
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 }) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, hasEpisodeFileId, shiftKey = false }) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
|
||||
setWithoutEpisodeFileIdRowsSelected(
|
||||
hasEpisodeFileId || !value
|
||||
? without(withoutEpisodeFileIdRowsSelected, id)
|
||||
: [...withoutEpisodeFileIdRowsSelected, id]
|
||||
);
|
||||
},
|
||||
[
|
||||
items,
|
||||
withoutEpisodeFileIdRowsSelected,
|
||||
setSelectState,
|
||||
setWithoutEpisodeFileIdRowsSelected,
|
||||
]
|
||||
);
|
||||
|
||||
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 episodeFileIds = items.reduce((acc, item) => {
|
||||
if (selectedIds.indexOf(item.id) > -1 && item.episodeFileId) {
|
||||
acc.push(item.episodeFileId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
dispatch(deleteEpisodeFiles({ episodeFileIds }));
|
||||
}, [items, selectedIds, setIsConfirmDeleteModalOpen, dispatch]);
|
||||
|
||||
const onConfirmDeleteModalClose = useCallback(() => {
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
}, [setIsConfirmDeleteModalOpen]);
|
||||
|
||||
const onImportSelectedPress = useCallback(() => {
|
||||
const finalImportMode =
|
||||
downloadId || !showImportMode ? ImportMode.Auto : importMode;
|
||||
|
||||
const existingFiles = [];
|
||||
const files = [];
|
||||
|
||||
if (finalImportMode === 'chooseImportMode') {
|
||||
setInteractiveImportErrorMessage('An import mode must be selected');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach((item) => {
|
||||
const isSelected = selectedIds.indexOf(item.id) > -1;
|
||||
|
||||
if (isSelected) {
|
||||
const {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
episodeFileId,
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
setInteractiveImportErrorMessage(
|
||||
'Series must be chosen for each selected file'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(seasonNumber)) {
|
||||
setInteractiveImportErrorMessage(
|
||||
'Season must be chosen for each selected file'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!episodes || !episodes.length) {
|
||||
setInteractiveImportErrorMessage(
|
||||
'One or more episodes must be chosen for each selected file'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
setInteractiveImportErrorMessage(
|
||||
'Quality must be chosen for each selected file'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!languages) {
|
||||
setInteractiveImportErrorMessage(
|
||||
'Language(s) must be chosen for each selected file'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setInteractiveImportErrorMessage(null);
|
||||
|
||||
if (episodeFileId) {
|
||||
const originalItem = originalItems.find((i) => i.id === item.id);
|
||||
|
||||
if (isSameEpisodeFile(item, originalItem)) {
|
||||
existingFiles.push({
|
||||
id: episodeFileId,
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
files.push({
|
||||
path: item.path,
|
||||
folderName: item.folderName,
|
||||
seriesId: series.id,
|
||||
episodeIds: episodes.map((e) => e.id),
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
downloadId,
|
||||
episodeFileId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let shouldClose = false;
|
||||
|
||||
if (existingFiles.length) {
|
||||
dispatch(
|
||||
updateEpisodeFiles({
|
||||
files: existingFiles,
|
||||
})
|
||||
);
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode,
|
||||
})
|
||||
);
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [
|
||||
downloadId,
|
||||
showImportMode,
|
||||
importMode,
|
||||
items,
|
||||
originalItems,
|
||||
selectedIds,
|
||||
onModalClose,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(sortKey, sortDirection) => {
|
||||
dispatch(setInteractiveImportSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterExistingFilesChange = useCallback(
|
||||
(value) => {
|
||||
const filter = value !== filterExistingFilesOptions.ALL;
|
||||
|
||||
setFilterExistingFiles(filter);
|
||||
|
||||
dispatch(
|
||||
fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
seriesId,
|
||||
folder,
|
||||
filterExistingFiles: filter,
|
||||
})
|
||||
);
|
||||
},
|
||||
[downloadId, seriesId, folder, setFilterExistingFiles, dispatch]
|
||||
);
|
||||
|
||||
const onImportModeChange = useCallback(
|
||||
({ value }) => {
|
||||
dispatch(setInteractiveImportMode({ importMode: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectModalSelect = useCallback(
|
||||
({ value }) => {
|
||||
setSelectModalOpen(value);
|
||||
},
|
||||
[setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectModalClose = useCallback(() => {
|
||||
setSelectModalOpen(null);
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSeriesSelect = useCallback(
|
||||
(series) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
series,
|
||||
seasonNumber: undefined,
|
||||
episodes: [],
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, setSelectModalOpen, dispatch]
|
||||
);
|
||||
|
||||
const onSeasonSelect = useCallback(
|
||||
(seasonNumber) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
seasonNumber,
|
||||
episodes: [],
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, setSelectModalOpen, dispatch]
|
||||
);
|
||||
|
||||
const onEpisodesSelect = useCallback(
|
||||
(episodes) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
episodes,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, setSelectModalOpen, dispatch]
|
||||
);
|
||||
|
||||
const onReleaseGroupSelect = useCallback(
|
||||
(releaseGroup) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
releaseGroup,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const onLanguagesSelect = useCallback(
|
||||
(newLanguages) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
languages: newLanguages,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const onQualitySelect = useCallback(
|
||||
(quality) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
quality,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const orderedSelectedIds = items.reduce((acc, file) => {
|
||||
if (selectedIds.includes(file.id)) {
|
||||
acc.push(file.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const selectedItem = selectedIds.length
|
||||
? items.find((file) => file.id === selectedIds[0])
|
||||
: null;
|
||||
|
||||
const errorMessage = getErrorMessage(
|
||||
error,
|
||||
'Unable to load manual import items'
|
||||
);
|
||||
|
||||
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 ? 'Unmapped Files Only' : 'All Files'}
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
All Files
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
Unmapped Files Only
|
||||
</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}
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
autoSelectRow={autoSelectRow}
|
||||
columns={columns}
|
||||
modalTitle={modalTitle}
|
||||
onSelectedChange={onSelectedChange}
|
||||
onValidRowChange={onValidRowChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !items.length && !isFetching
|
||||
? 'No video files were found in the selected folder'
|
||||
: null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.leftButtons}>
|
||||
{showDelete ? (
|
||||
<SpinnerButton
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={
|
||||
!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length
|
||||
}
|
||||
onPress={onDeleteSelectedPress}
|
||||
>
|
||||
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}>Cancel</Button>
|
||||
|
||||
{interactiveImportErrorMessage && (
|
||||
<span className={styles.errorMessage}>
|
||||
{interactiveImportErrorMessage}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
|
||||
onPress={onImportSelectedPress}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={selectModalOpen === 'series'}
|
||||
modalTitle={modalTitle}
|
||||
onSeriesSelect={onSeriesSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={selectModalOpen === 'season'}
|
||||
seriesId={selectedItem?.series?.id}
|
||||
modalTitle={modalTitle}
|
||||
onSeasonSelect={onSeasonSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectEpisodeModal
|
||||
isOpen={selectModalOpen === 'episode'}
|
||||
selectedIds={orderedSelectedIds}
|
||||
seriesId={selectedItem?.series?.id}
|
||||
seasonNumber={selectedItem?.seasonNumber}
|
||||
isAnime={selectedItem?.series.type === 'anime'}
|
||||
modalTitle={modalTitle}
|
||||
onEpisodesSelect={onEpisodesSelect}
|
||||
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="Delete Selected Episode Files"
|
||||
message={'Are you sure you want to delete the selected episode files?'}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={onConfirmDelete}
|
||||
onCancel={onConfirmDeleteModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveImportModalContent;
|
||||
@@ -1,327 +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 { sortDirections } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { deleteEpisodeFiles, updateEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import { clearInteractiveImport, fetchInteractiveImportItems, setInteractiveImportMode, setInteractiveImportSort } from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import InteractiveImportModalContent from './InteractiveImportModalContent';
|
||||
|
||||
function isSameEpisodeFile(file, originalFile) {
|
||||
const {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes
|
||||
} = file;
|
||||
|
||||
if (!originalFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!originalFile.series || series.id !== originalFile.series.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seasonNumber !== originalFile.seasonNumber) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('interactiveImport'),
|
||||
(state) => state.episodeFiles.isDeleting,
|
||||
(state) => state.episodeFiles.deleteError,
|
||||
(interactiveImport, isDeleting, deleteError) => {
|
||||
return {
|
||||
...interactiveImport,
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchInteractiveImportItems: fetchInteractiveImportItems,
|
||||
dispatchSetInteractiveImportSort: setInteractiveImportSort,
|
||||
dispatchSetInteractiveImportMode: setInteractiveImportMode,
|
||||
dispatchClearInteractiveImport: clearInteractiveImport,
|
||||
dispatchUpdateEpisodeFiles: updateEpisodeFiles,
|
||||
dispatchDeleteEpisodeFiles: deleteEpisodeFiles,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class InteractiveImportModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
interactiveImportErrorMessage: null,
|
||||
filterExistingFiles: true
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
downloadId,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
folder,
|
||||
initialSortKey,
|
||||
initialSortDirection,
|
||||
dispatchSetInteractiveImportSort,
|
||||
dispatchFetchInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
if (initialSortKey) {
|
||||
const sortProps = {
|
||||
sortKey: initialSortKey
|
||||
};
|
||||
|
||||
if (initialSortDirection) {
|
||||
sortProps.sortDirection = initialSortDirection;
|
||||
}
|
||||
|
||||
dispatchSetInteractiveImportSort(sortProps);
|
||||
}
|
||||
|
||||
dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
seriesId,
|
||||
seasonNumber,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
if (prevState.filterExistingFiles !== filterExistingFiles) {
|
||||
const {
|
||||
downloadId,
|
||||
seriesId,
|
||||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.dispatchFetchInteractiveImportItems({
|
||||
downloadId,
|
||||
seriesId,
|
||||
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 });
|
||||
};
|
||||
|
||||
onDeleteSelectedPress = (selected) => {
|
||||
const {
|
||||
items,
|
||||
dispatchDeleteEpisodeFiles
|
||||
} = this.props;
|
||||
|
||||
const episodeFileIds = items.reduce((acc, item) => {
|
||||
if (selected.indexOf(item.id) > -1 && item.episodeFileId) {
|
||||
acc.push(item.episodeFileId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
dispatchDeleteEpisodeFiles({ episodeFileIds });
|
||||
};
|
||||
|
||||
onImportSelectedPress = (selected, importMode) => {
|
||||
const {
|
||||
items,
|
||||
originalItems,
|
||||
dispatchUpdateEpisodeFiles,
|
||||
dispatchExecuteCommand,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const existingFiles = [];
|
||||
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 {
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
episodeFileId
|
||||
} = item;
|
||||
|
||||
if (!series) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Series must be chosen for each selected file' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(seasonNumber)) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Season must be chosen for each selected file' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!episodes || !episodes.length) {
|
||||
this.setState({ interactiveImportErrorMessage: 'One or more episodes must be chosen for each selected file' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Quality must be chosen for each selected file' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!languages) {
|
||||
this.setState({ interactiveImportErrorMessage: 'Language(s) must be chosen for each selected file' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (episodeFileId) {
|
||||
const originalItem = originalItems.find((i) => i.id === item.id);
|
||||
|
||||
if (isSameEpisodeFile(item, originalItem)) {
|
||||
existingFiles.push({
|
||||
id: episodeFileId,
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
files.push({
|
||||
path: item.path,
|
||||
folderName: item.folderName,
|
||||
seriesId: series.id,
|
||||
episodeIds: episodes.map((e) => e.id),
|
||||
releaseGroup,
|
||||
quality,
|
||||
languages,
|
||||
downloadId: this.props.downloadId,
|
||||
episodeFileId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let shouldClose = false;
|
||||
|
||||
if (existingFiles.length) {
|
||||
dispatchUpdateEpisodeFiles({
|
||||
files: existingFiles
|
||||
});
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (files.length) {
|
||||
dispatchExecuteCommand({
|
||||
name: commandNames.INTERACTIVE_IMPORT,
|
||||
files,
|
||||
importMode
|
||||
});
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
||||
if (shouldClose) {
|
||||
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}
|
||||
onDeleteSelectedPress={this.onDeleteSelectedPress}
|
||||
onImportSelectedPress={this.onImportSelectedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveImportModalContentConnector.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
seriesId: PropTypes.number,
|
||||
seasonNumber: PropTypes.number,
|
||||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
initialSortKey: PropTypes.string,
|
||||
initialSortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
originalItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportSort: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportMode: PropTypes.func.isRequired,
|
||||
dispatchClearInteractiveImport: PropTypes.func.isRequired,
|
||||
dispatchUpdateEpisodeFiles: PropTypes.func.isRequired,
|
||||
dispatchDeleteEpisodeFiles: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContentConnector.defaultProps = {
|
||||
filterExistingFiles: true
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(InteractiveImportModalContentConnector);
|
||||
@@ -1,504 +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 EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
|
||||
class InteractiveImportRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isSelectSeriesModalOpen: false,
|
||||
isSelectSeasonModalOpen: false,
|
||||
isSelectEpisodeModalOpen: false,
|
||||
isSelectReleaseGroupModalOpen: false,
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectLanguageModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
allowSeriesChange,
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
languages,
|
||||
episodeFileId,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
allowSeriesChange &&
|
||||
series &&
|
||||
seasonNumber != null &&
|
||||
episodes.length &&
|
||||
quality &&
|
||||
languages
|
||||
) {
|
||||
this.props.onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSeriesColumnVisible: columns.find((c) => c.name === 'series').isVisible
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
languages,
|
||||
isSelected,
|
||||
onValidRowChange
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.series === series &&
|
||||
prevProps.seasonNumber === seasonNumber &&
|
||||
!hasDifferentItems(prevProps.episodes, episodes) &&
|
||||
prevProps.quality === quality &&
|
||||
prevProps.languages === languages &&
|
||||
prevProps.isSelected === isSelected
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = !!(
|
||||
series &&
|
||||
seasonNumber != null &&
|
||||
episodes.length &&
|
||||
quality &&
|
||||
languages
|
||||
);
|
||||
|
||||
if (isSelected && !isValid) {
|
||||
onValidRowChange(id, false);
|
||||
} else {
|
||||
onValidRowChange(id, true);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
selectRowAfterChange = (value) => {
|
||||
const {
|
||||
id,
|
||||
episodeFileId,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
if (!isSelected && value === true) {
|
||||
this.props.onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectedChange = (result) => {
|
||||
const {
|
||||
episodeFileId,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({
|
||||
...result,
|
||||
hasEpisodeFileId: !!episodeFileId
|
||||
});
|
||||
};
|
||||
|
||||
onSelectSeriesPress = () => {
|
||||
this.setState({ isSelectSeriesModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectSeasonPress = () => {
|
||||
this.setState({ isSelectSeasonModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectEpisodePress = () => {
|
||||
this.setState({ isSelectEpisodeModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectReleaseGroupPress = () => {
|
||||
this.setState({ isSelectReleaseGroupModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectLanguagePress = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: true });
|
||||
};
|
||||
|
||||
onSelectSeriesModalClose = (changed) => {
|
||||
this.setState({ isSelectSeriesModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
};
|
||||
|
||||
onSelectSeasonModalClose = (changed) => {
|
||||
this.setState({ isSelectSeasonModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
};
|
||||
|
||||
onSelectEpisodeModalClose = (changed) => {
|
||||
this.setState({ isSelectEpisodeModalOpen: false });
|
||||
this.selectRowAfterChange(changed);
|
||||
};
|
||||
|
||||
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,
|
||||
allowSeriesChange,
|
||||
relativePath,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
languages,
|
||||
releaseGroup,
|
||||
size,
|
||||
customFormats,
|
||||
rejections,
|
||||
isReprocessing,
|
||||
isSelected,
|
||||
modalTitle
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectSeriesModalOpen,
|
||||
isSelectSeasonModalOpen,
|
||||
isSelectEpisodeModalOpen,
|
||||
isSelectReleaseGroupModalOpen,
|
||||
isSelectQualityModalOpen,
|
||||
isSelectLanguageModalOpen
|
||||
} = this.state;
|
||||
|
||||
const seriesTitle = series ? series.title : '';
|
||||
const isAnime = series ? series.seriesType === 'anime' : false;
|
||||
|
||||
const episodeInfo = episodes.map((episode) => {
|
||||
return (
|
||||
<div key={episode.id}>
|
||||
{episode.episodeNumber}
|
||||
|
||||
{
|
||||
isAnime && episode.absoluteEpisodeNumber != null ?
|
||||
` (${episode.absoluteEpisodeNumber})` :
|
||||
''
|
||||
}
|
||||
|
||||
{` - ${episode.title}`}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
|
||||
const showEpisodeNumbersPlaceholder = isSelected && Number.isInteger(seasonNumber) && !episodes.length;
|
||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||
const showQualityPlaceholder = isSelected && !quality;
|
||||
const showLanguagePlaceholder = isSelected && !languages;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.relativePath}
|
||||
title={relativePath}
|
||||
>
|
||||
{relativePath}
|
||||
</TableRowCell>
|
||||
|
||||
{
|
||||
this.state.isSeriesColumnVisible ?
|
||||
<TableRowCellButton
|
||||
isDisabled={!allowSeriesChange}
|
||||
title={allowSeriesChange ? 'Click to change series' : undefined}
|
||||
onPress={this.onSelectSeriesPress}
|
||||
>
|
||||
{
|
||||
showSeriesPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seriesTitle
|
||||
}
|
||||
</TableRowCellButton> :
|
||||
null
|
||||
}
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series}
|
||||
title={series ? 'Click to change season' : undefined}
|
||||
onPress={this.onSelectSeasonPress}
|
||||
>
|
||||
{
|
||||
showSeasonNumberPlaceholder ? <InteractiveImportRowCellPlaceholder /> : seasonNumber
|
||||
}
|
||||
|
||||
{
|
||||
isReprocessing && seasonNumber == null ?
|
||||
<LoadingIndicator className={styles.reprocessing}
|
||||
size={20}
|
||||
|
||||
/> : null
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series || isNaN(seasonNumber)}
|
||||
title={series && !isNaN(seasonNumber) ? 'Click to change episode' : undefined}
|
||||
onPress={this.onSelectEpisodePress}
|
||||
>
|
||||
{
|
||||
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeInfo
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
title="Click to change release group"
|
||||
onPress={this.onSelectReleaseGroupPress}
|
||||
>
|
||||
{
|
||||
showReleaseGroupPlaceholder ?
|
||||
<InteractiveImportRowCellPlaceholder
|
||||
isOptional={true}
|
||||
/> :
|
||||
releaseGroup
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.quality}
|
||||
title="Click to change quality"
|
||||
onPress={this.onSelectQualityPress}
|
||||
>
|
||||
{
|
||||
showQualityPlaceholder &&
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
}
|
||||
|
||||
{
|
||||
!showQualityPlaceholder && !!quality &&
|
||||
<EpisodeQuality
|
||||
className={styles.label}
|
||||
quality={quality}
|
||||
/>
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.language}
|
||||
title="Click to change language"
|
||||
onPress={this.onSelectLanguagePress}
|
||||
>
|
||||
{
|
||||
showLanguagePlaceholder &&
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
}
|
||||
|
||||
{
|
||||
!showLanguagePlaceholder && !!languages &&
|
||||
<EpisodeLanguages
|
||||
className={styles.label}
|
||||
languages={languages}
|
||||
/>
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{
|
||||
customFormats?.length ?
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon name={icons.INTERACTIVE} />
|
||||
}
|
||||
title="Formats"
|
||||
body={
|
||||
<div className={styles.customFormatTooltip}>
|
||||
<EpisodeFormats formats={customFormats} />
|
||||
</div>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{
|
||||
rejections.length ?
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title="Release Rejected"
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
rejections.map((rejection, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{rejection.reason}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
canFlip={false}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={isSelectSeriesModalOpen}
|
||||
ids={[id]}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeriesModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={isSelectSeasonModalOpen}
|
||||
ids={[id]}
|
||||
seriesId={series && series.id}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectSeasonModalClose}
|
||||
/>
|
||||
|
||||
<SelectEpisodeModal
|
||||
isOpen={isSelectEpisodeModalOpen}
|
||||
ids={[id]}
|
||||
seriesId={series && series.id}
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
relativePath={relativePath}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectEpisodeModalClose}
|
||||
/>
|
||||
|
||||
<SelectReleaseGroupModal
|
||||
isOpen={isSelectReleaseGroupModalOpen}
|
||||
ids={[id]}
|
||||
releaseGroup={releaseGroup ?? ''}
|
||||
modalTitle={modalTitle}
|
||||
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}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={isSelectLanguageModalOpen}
|
||||
ids={[id]}
|
||||
languageIds={languages ? languages.map((l) => l.id) : []}
|
||||
modalTitle={modalTitle}
|
||||
onModalClose={this.onSelectLanguageModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
InteractiveImportRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
allowSeriesChange: PropTypes.bool.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
series: PropTypes.object,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
releaseGroup: PropTypes.string,
|
||||
quality: PropTypes.object,
|
||||
languages: PropTypes.arrayOf(PropTypes.object),
|
||||
size: PropTypes.number.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
episodeFileId: PropTypes.number,
|
||||
isReprocessing: PropTypes.bool,
|
||||
isSelected: PropTypes.bool,
|
||||
modalTitle: PropTypes.string.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onValidRowChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportRow.defaultProps = {
|
||||
episodes: []
|
||||
};
|
||||
|
||||
export default InteractiveImportRow;
|
||||
@@ -0,0 +1,506 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
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 Column from 'Components/Table/Column';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Episode from 'Episode/Episode';
|
||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
import {
|
||||
reprocessInteractiveImportItems,
|
||||
updateInteractiveImportItem,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import Rejection from 'typings/Rejection';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
import styles from './InteractiveImportRow.css';
|
||||
|
||||
type SelectType =
|
||||
| 'series'
|
||||
| 'season'
|
||||
| 'episode'
|
||||
| 'releaseGroup'
|
||||
| 'quality'
|
||||
| 'language';
|
||||
|
||||
interface InteractiveImportRowProps {
|
||||
id: number;
|
||||
allowSeriesChange: boolean;
|
||||
relativePath: string;
|
||||
series?: Series;
|
||||
seasonNumber?: number;
|
||||
episodes?: Episode[];
|
||||
releaseGroup?: string;
|
||||
quality?: QualityModel;
|
||||
languages?: Language[];
|
||||
size: number;
|
||||
customFormats?: object[];
|
||||
rejections: Rejection[];
|
||||
columns: Column[];
|
||||
episodeFileId?: number;
|
||||
isReprocessing?: boolean;
|
||||
isSelected?: boolean;
|
||||
modalTitle: string;
|
||||
onSelectedChange(...args: unknown[]): void;
|
||||
onValidRowChange(id: number, isValid: boolean): void;
|
||||
}
|
||||
|
||||
function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||
const {
|
||||
id,
|
||||
allowSeriesChange,
|
||||
relativePath,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes = [],
|
||||
quality,
|
||||
languages,
|
||||
releaseGroup,
|
||||
size,
|
||||
customFormats,
|
||||
rejections,
|
||||
isReprocessing,
|
||||
isSelected,
|
||||
modalTitle,
|
||||
episodeFileId,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onValidRowChange,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isSeriesColumnVisible = useMemo(
|
||||
() => columns.find((c) => c.name === 'series').isVisible,
|
||||
[columns]
|
||||
);
|
||||
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
allowSeriesChange &&
|
||||
series &&
|
||||
seasonNumber != null &&
|
||||
episodes.length &&
|
||||
quality &&
|
||||
languages
|
||||
) {
|
||||
onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const isValid = !!(
|
||||
series &&
|
||||
seasonNumber != null &&
|
||||
episodes.length &&
|
||||
quality &&
|
||||
languages
|
||||
);
|
||||
|
||||
if (isSelected && !isValid) {
|
||||
onValidRowChange(id, false);
|
||||
} else {
|
||||
onValidRowChange(id, true);
|
||||
}
|
||||
}, [
|
||||
id,
|
||||
series,
|
||||
seasonNumber,
|
||||
episodes,
|
||||
quality,
|
||||
languages,
|
||||
isSelected,
|
||||
onValidRowChange,
|
||||
]);
|
||||
|
||||
const onSelectedChangeWrapper = useCallback(
|
||||
(result) => {
|
||||
onSelectedChange({
|
||||
...result,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
});
|
||||
},
|
||||
[episodeFileId, onSelectedChange]
|
||||
);
|
||||
|
||||
const selectRowAfterChange = useCallback(() => {
|
||||
if (!isSelected) {
|
||||
onSelectedChange({
|
||||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true,
|
||||
});
|
||||
}
|
||||
}, [id, episodeFileId, isSelected, onSelectedChange]);
|
||||
|
||||
const onSelectModalClose = useCallback(() => {
|
||||
setSelectModalOpen(null);
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSelectSeriesPress = useCallback(() => {
|
||||
setSelectModalOpen('series');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSeriesSelect = useCallback(
|
||||
(series: Series) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItem({
|
||||
id,
|
||||
series,
|
||||
seasonNumber: undefined,
|
||||
episodes: [],
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
selectRowAfterChange();
|
||||
},
|
||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||
);
|
||||
|
||||
const onSelectSeasonPress = useCallback(() => {
|
||||
setSelectModalOpen('season');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSeasonSelect = useCallback(
|
||||
(seasonNumber: number) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItem({
|
||||
id,
|
||||
seasonNumber,
|
||||
episodes: [],
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
|
||||
|
||||
setSelectModalOpen(null);
|
||||
selectRowAfterChange();
|
||||
},
|
||||
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
|
||||
);
|
||||
|
||||
const onSelectEpisodePress = useCallback(() => {
|
||||
setSelectModalOpen('episode');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onEpisodesSelect = useCallback(
|
||||
(selectedEpisodes: SelectedEpisode[]) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItem({
|
||||
id,
|
||||
episodes: selectedEpisodes[0].episodes,
|
||||
})
|
||||
);
|
||||
|
||||
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 seriesTitle = series ? series.title : '';
|
||||
const isAnime = series?.seriesType === 'anime';
|
||||
|
||||
const episodeInfo = episodes.map((episode) => {
|
||||
return (
|
||||
<div key={episode.id}>
|
||||
{episode.episodeNumber}
|
||||
|
||||
{isAnime && episode.absoluteEpisodeNumber != null
|
||||
? ` (${episode.absoluteEpisodeNumber})`
|
||||
: ''}
|
||||
|
||||
{` - ${episode.title}`}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder =
|
||||
isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
|
||||
const showEpisodeNumbersPlaceholder =
|
||||
isSelected && Number.isInteger(seasonNumber) && !episodes.length;
|
||||
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>
|
||||
|
||||
{isSeriesColumnVisible ? (
|
||||
<TableRowCellButton
|
||||
isDisabled={!allowSeriesChange}
|
||||
title={allowSeriesChange ? 'Click to change series' : undefined}
|
||||
onPress={onSelectSeriesPress}
|
||||
>
|
||||
{showSeriesPlaceholder ? (
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
) : (
|
||||
seriesTitle
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
) : null}
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series}
|
||||
title={series ? 'Click to change season' : undefined}
|
||||
onPress={onSelectSeasonPress}
|
||||
>
|
||||
{showSeasonNumberPlaceholder ? (
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
) : (
|
||||
seasonNumber
|
||||
)}
|
||||
|
||||
{isReprocessing && seasonNumber == null ? (
|
||||
<LoadingIndicator className={styles.reprocessing} size={20} />
|
||||
) : null}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series || isNaN(seasonNumber)}
|
||||
title={
|
||||
series && !isNaN(seasonNumber) ? 'Click to change episode' : undefined
|
||||
}
|
||||
onPress={onSelectEpisodePress}
|
||||
>
|
||||
{showEpisodeNumbersPlaceholder ? (
|
||||
<InteractiveImportRowCellPlaceholder />
|
||||
) : (
|
||||
episodeInfo
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
title="Click to change release group"
|
||||
onPress={onSelectReleaseGroupPress}
|
||||
>
|
||||
{showReleaseGroupPlaceholder ? (
|
||||
<InteractiveImportRowCellPlaceholder isOptional={true} />
|
||||
) : (
|
||||
releaseGroup
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.quality}
|
||||
title="Click to change quality"
|
||||
onPress={onSelectQualityPress}
|
||||
>
|
||||
{showQualityPlaceholder && <InteractiveImportRowCellPlaceholder />}
|
||||
|
||||
{!showQualityPlaceholder && !!quality && (
|
||||
<EpisodeQuality className={styles.label} quality={quality} />
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
className={styles.languages}
|
||||
title="Click to change language"
|
||||
onPress={onSelectLanguagePress}
|
||||
>
|
||||
{showLanguagePlaceholder && <InteractiveImportRowCellPlaceholder />}
|
||||
|
||||
{!showLanguagePlaceholder && !!languages && (
|
||||
<EpisodeLanguages className={styles.label} languages={languages} />
|
||||
)}
|
||||
</TableRowCellButton>
|
||||
|
||||
<TableRowCell>{formatBytes(size)}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{customFormats?.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.INTERACTIVE} />}
|
||||
title="Formats"
|
||||
body={
|
||||
<div className={styles.customFormatTooltip}>
|
||||
<EpisodeFormats formats={customFormats} />
|
||||
</div>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{rejections.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
|
||||
title="Release Rejected"
|
||||
body={
|
||||
<ul>
|
||||
{rejections.map((rejection, index) => {
|
||||
return <li key={index}>{rejection.reason}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
canFlip={false}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<SelectSeriesModal
|
||||
isOpen={selectModalOpen === 'series'}
|
||||
modalTitle={modalTitle}
|
||||
onSeriesSelect={onSeriesSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectSeasonModal
|
||||
isOpen={selectModalOpen === 'season'}
|
||||
seriesId={series && series.id}
|
||||
modalTitle={modalTitle}
|
||||
onSeasonSelect={onSeasonSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectEpisodeModal
|
||||
isOpen={selectModalOpen === 'episode'}
|
||||
selectedIds={[id]}
|
||||
seriesId={series && series.id}
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
selectedDetails={relativePath}
|
||||
modalTitle={modalTitle}
|
||||
onEpisodesSelect={onEpisodesSelect}
|
||||
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;
|
||||
Reference in New Issue
Block a user