899 lines
24 KiB
TypeScript
899 lines
24 KiB
TypeScript
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 { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
|
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 { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
|
import ImportMode from 'InteractiveImport/ImportMode';
|
|
import InteractiveImport, {
|
|
InteractiveImportCommandOptions,
|
|
} from 'InteractiveImport/InteractiveImport';
|
|
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 { 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 { SortCallback } from 'typings/callbacks';
|
|
import { SelectStateInputProps } from 'typings/props';
|
|
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';
|
|
|
|
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: '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 importModeOptions = [
|
|
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
|
{ key: 'move', value: 'Move Files' },
|
|
{ key: 'copy', value: 'Hardlink/Copy Files' },
|
|
];
|
|
|
|
function isSameEpisodeFile(
|
|
file: InteractiveImport,
|
|
originalFile?: InteractiveImport
|
|
) {
|
|
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: AppState) => state.episodeFiles.isDeleting,
|
|
(state: AppState) => state.episodeFiles.deleteError,
|
|
(isDeleting, deleteError) => {
|
|
return {
|
|
isDeleting,
|
|
deleteError,
|
|
};
|
|
}
|
|
);
|
|
|
|
const importModeSelector = createSelector(
|
|
(state: AppState) => state.interactiveImport.importMode,
|
|
(importMode) => {
|
|
return importMode;
|
|
}
|
|
);
|
|
|
|
interface InteractiveImportModalContentProps {
|
|
downloadId?: string;
|
|
seriesId?: number;
|
|
seasonNumber?: number;
|
|
showSeries?: boolean;
|
|
allowSeriesChange?: 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,
|
|
showSeries = 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(episodeFilesInfoSelector);
|
|
const importMode = useSelector(importModeSelector);
|
|
|
|
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
|
|
const [
|
|
withoutEpisodeFileIdRowsSelected,
|
|
setWithoutEpisodeFileIdRowsSelected,
|
|
] = 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: '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: Column[] = cloneDeep(COLUMNS);
|
|
|
|
if (!showSeries) {
|
|
const seriesColumn = result.find((c) => c.name === 'series');
|
|
|
|
if (seriesColumn) {
|
|
seriesColumn.isVisible = false;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}, [showSeries]);
|
|
|
|
const selectedIds: number[] = 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 }: SelectStateInputProps) => {
|
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
|
},
|
|
[items, setSelectState]
|
|
);
|
|
|
|
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
|
|
({ 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: number[], 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 ? 'auto' : importMode;
|
|
|
|
const existingFiles: Partial<EpisodeFile>[] = [];
|
|
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 {
|
|
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: 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,
|
|
seriesId,
|
|
folder,
|
|
filterExistingFiles: filter,
|
|
})
|
|
);
|
|
},
|
|
[downloadId, seriesId, 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 onSeriesSelect = useCallback(
|
|
(series: Series) => {
|
|
dispatch(
|
|
updateInteractiveImportItems({
|
|
ids: selectedIds,
|
|
series,
|
|
seasonNumber: undefined,
|
|
episodes: [],
|
|
})
|
|
);
|
|
|
|
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
|
|
|
setSelectModalOpen(null);
|
|
},
|
|
[selectedIds, setSelectModalOpen, dispatch]
|
|
);
|
|
|
|
const onSeasonSelect = useCallback(
|
|
(seasonNumber: number) => {
|
|
dispatch(
|
|
updateInteractiveImportItems({
|
|
ids: selectedIds,
|
|
seasonNumber,
|
|
episodes: [],
|
|
})
|
|
);
|
|
|
|
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
|
|
|
|
setSelectModalOpen(null);
|
|
},
|
|
[selectedIds, setSelectModalOpen, dispatch]
|
|
);
|
|
|
|
const onEpisodesSelect = useCallback(
|
|
(episodes: SelectedEpisode[]) => {
|
|
dispatch(
|
|
updateInteractiveImportItems({
|
|
ids: selectedIds,
|
|
episodes,
|
|
})
|
|
);
|
|
|
|
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 orderedSelectedIds = items.reduce((acc: number[], 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={'all'}
|
|
isSelected={!filterExistingFiles}
|
|
onPress={onFilterExistingFilesChange}
|
|
>
|
|
All Files
|
|
</SelectedMenuItem>
|
|
|
|
<SelectedMenuItem
|
|
name={'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}
|
|
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?.seriesType === '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;
|