Use react-query for manual import

This commit is contained in:
Mark McDowall 2025-12-21 20:28:59 -08:00
parent 8da611ea58
commit ec44e1c513
13 changed files with 767 additions and 671 deletions

View file

@ -1,7 +1,6 @@
import BlocklistAppState from './BlocklistAppState';
import CaptchaAppState from './CaptchaAppState';
import ImportSeriesAppState from './ImportSeriesAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import OAuthAppState from './OAuthAppState';
import ProviderOptionsAppState from './ProviderOptionsAppState';
import SettingsAppState from './SettingsAppState';
@ -10,7 +9,6 @@ interface AppState {
blocklist: BlocklistAppState;
captcha: CaptchaAppState;
importSeries: ImportSeriesAppState;
interactiveImport: InteractiveImportAppState;
oAuth: OAuthAppState;
providerOptions: ProviderOptionsAppState;
settings: SettingsAppState;

View file

@ -1,21 +0,0 @@
import AppSectionState from 'App/State/AppSectionState';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport from 'InteractiveImport/InteractiveImport';
interface FavoriteFolder {
folder: string;
}
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[];
importMode: ImportMode;
favoriteFolders: FavoriteFolder[];
recentFolders: RecentFolder[];
}
export default InteractiveImportAppState;

View file

@ -1,10 +1,9 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
import { removeFavoriteFolder } from 'InteractiveImport/interactiveImportFoldersStore';
import translate from 'Utilities/String/translate';
import styles from './FavoriteFolderRow.css';
@ -14,8 +13,6 @@ interface FavoriteFolderRowProps {
}
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
@ -24,9 +21,9 @@ function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeFavoriteFolder({ folder }));
removeFavoriteFolder(folder);
},
[folder, dispatch]
[folder]
);
return (

View file

@ -1,7 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import PathInput from 'Components/Form/PathInput';
@ -15,7 +12,11 @@ import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
import {
addRecentFolder,
useFavoriteFolders,
useRecentFolders,
} from 'InteractiveImport/interactiveImportFoldersStore';
import translate from 'Utilities/String/translate';
import FavoriteFolderRow from './FavoriteFolderRow';
import RecentFolderRow from './RecentFolderRow';
@ -63,20 +64,10 @@ function InteractiveImportSelectFolderModalContent(
) {
const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState('');
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const { favoriteFolders, recentFolders } = useSelector(
createSelector(
(state: AppState) => state.interactiveImport,
(interactiveImport) => {
return {
favoriteFolders: interactiveImport.favoriteFolders,
recentFolders: interactiveImport.recentFolders,
};
}
)
);
const favoriteFolders = useFavoriteFolders();
const recentFolders = useRecentFolders();
const favoriteFolderMap = useMemo(() => {
return new Map(favoriteFolders.map((f) => [f.folder, f]));
@ -97,7 +88,7 @@ function InteractiveImportSelectFolderModalContent(
);
const onQuickImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));
addRecentFolder(folder);
executeCommand({
name: CommandNames.DownloadedEpisodesScan,
@ -105,12 +96,12 @@ function InteractiveImportSelectFolderModalContent(
});
onModalClose();
}, [folder, onModalClose, dispatch, executeCommand]);
}, [folder, onModalClose, executeCommand]);
const onInteractiveImportPress = useCallback(() => {
dispatch(addRecentFolder({ folder }));
addRecentFolder(folder);
onFolderSelect(folder);
}, [folder, onFolderSelect, dispatch]);
}, [folder, onFolderSelect]);
return (
<ModalContent onModalClose={onModalClose}>

View file

@ -1,5 +1,4 @@
import React, { SyntheticEvent, useCallback } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
@ -9,7 +8,7 @@ import {
addFavoriteFolder,
removeFavoriteFolder,
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
} from 'InteractiveImport/interactiveImportFoldersStore';
import translate from 'Utilities/String/translate';
import styles from './RecentFolderRow.css';
@ -26,8 +25,6 @@ function RecentFolderRow({
isFavorite,
onPress,
}: RecentFolderRowProps) {
const dispatch = useDispatch();
const handlePress = useCallback(() => {
onPress(folder);
}, [folder, onPress]);
@ -37,21 +34,21 @@ function RecentFolderRow({
e.stopPropagation();
if (isFavorite) {
dispatch(removeFavoriteFolder({ folder }));
removeFavoriteFolder(folder);
} else {
dispatch(addFavoriteFolder({ folder }));
addFavoriteFolder(folder);
}
},
[folder, isFavorite, dispatch]
[folder, isFavorite]
);
const handleRemovePress = useCallback(
(e: SyntheticEvent) => {
e.stopPropagation();
dispatch(removeRecentFolder({ folder }));
removeRecentFolder(folder);
},
[folder, dispatch]
[folder]
);
return (

View file

@ -1,10 +1,7 @@
import { cloneDeep, without } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { create } from 'zustand';
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CommandNames from 'Commands/CommandNames';
import { useExecuteCommand } from 'Commands/useCommands';
import SelectInput, { SelectInputOption } from 'Components/Form/SelectInput';
@ -31,6 +28,7 @@ import {
} from 'EpisodeFile/useEpisodeFiles';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import ImportMode from 'InteractiveImport/ImportMode';
@ -38,25 +36,26 @@ import SelectIndexerFlagsModal from 'InteractiveImport/IndexerFlags/SelectIndexe
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
import {
setInteractiveImportOption,
setInteractiveImportSort,
useInteractiveImportOptions,
} from 'InteractiveImport/interactiveImportOptionsStore';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import useInteractiveImport, {
useReprocessInteractiveImportItems,
useUpdateInteractiveImportItem,
useUpdateInteractiveImportItems,
} from 'InteractiveImport/useInteractiveImport';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import {
clearInteractiveImport,
fetchInteractiveImportItems,
reprocessInteractiveImportItems,
setInteractiveImportMode,
setInteractiveImportSort,
updateInteractiveImportItem,
updateInteractiveImportItems,
} from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SortCallback } from 'typings/callbacks';
import { CheckInputChanged } from 'typings/inputs';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
@ -200,12 +199,7 @@ function isSameEpisodeFile(
return !hasDifferentItems(originalFile.episodes, episodes);
}
const importModeSelector = createSelector(
(state: AppState) => state.interactiveImport.importMode,
(importMode) => {
return importMode;
}
);
const filterExistingFilesStore = create<boolean>(() => false);
export interface InteractiveImportModalContentProps {
downloadId?: string;
@ -246,25 +240,40 @@ function InteractiveImportModalContentInner(
onModalClose,
} = props;
const filterExistingFiles = filterExistingFilesStore((state) => state);
const [reprocessingItems, setReprocessingItems] = useState<Set<number>>(
new Set()
);
const {
isFetching,
isPopulated,
isFetched: isPopulated,
error,
items,
data,
originalItems,
sortKey,
sortDirection,
}: InteractiveImportAppState = useSelector(
createClientSideCollectionSelector('interactiveImport')
);
} = useInteractiveImport({
downloadId,
seriesId,
seasonNumber,
folder,
filterExistingFiles,
});
const { sortKey, sortDirection, importMode } = useInteractiveImportOptions();
const { updateInteractiveImportItem } = useUpdateInteractiveImportItem();
const { updateInteractiveImportItems } = useUpdateInteractiveImportItems();
const { reprocessInteractiveImportItems } =
useReprocessInteractiveImportItems();
const items = data;
const { isDeleting, deleteEpisodeFiles, deleteError } =
useDeleteEpisodeFiles();
const { updateEpisodeFiles } = useUpdateEpisodeFiles();
const importMode = useSelector(importModeSelector);
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
const [
withoutEpisodeFileIdRowsSelected,
@ -275,11 +284,9 @@ function InteractiveImportModalContentInner(
);
const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] =
useState(false);
const [filterExistingFiles, setFilterExistingFiles] = useState(false);
const [interactiveImportErrorMessage, setInteractiveImportErrorMessage] =
useState<string | null>(null);
const previousIsDeleting = usePrevious(isDeleting);
const dispatch = useDispatch();
const executeCommand = useExecuteCommand();
const {
@ -392,31 +399,14 @@ function InteractiveImportModalContentInner(
useEffect(
() => {
if (initialSortKey) {
const sortProps: { sortKey: string; sortDirection?: string } = {
const sortDirection: SortDirection =
(initialSortDirection as SortDirection) || 'ascending';
setInteractiveImportSort({
sortKey: initialSortKey,
};
if (initialSortDirection) {
sortProps.sortDirection = initialSortDirection;
}
dispatch(setInteractiveImportSort(sortProps));
sortDirection,
});
}
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
[]
@ -428,7 +418,7 @@ function InteractiveImportModalContentInner(
}
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
const onSelectAllChange = useCallback(
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
if (value) {
selectAll();
@ -439,7 +429,7 @@ function InteractiveImportModalContentInner(
[selectAll, unselectAll]
);
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
const handleSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, hasEpisodeFileId, shiftKey = false }) => {
toggleSelected({
id,
@ -460,7 +450,7 @@ function InteractiveImportModalContentInner(
]
);
const onValidRowChange = useCallback(
const handleValidRowChange = useCallback(
(id: number, isValid: boolean) => {
if (isValid && invalidRowsSelected.includes(id)) {
setInvalidRowsSelected(without(invalidRowsSelected, id));
@ -471,11 +461,11 @@ function InteractiveImportModalContentInner(
[invalidRowsSelected, setInvalidRowsSelected]
);
const onDeleteSelectedPress = useCallback(() => {
const handleDeleteSelectedPress = useCallback(() => {
setIsConfirmDeleteModalOpen(true);
}, [setIsConfirmDeleteModalOpen]);
const onConfirmDelete = useCallback(() => {
const handleConfirmDelete = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
const episodeFileIds = items.reduce((acc: number[], item) => {
@ -489,11 +479,11 @@ function InteractiveImportModalContentInner(
deleteEpisodeFiles({ episodeFileIds });
}, [items, selectedIds, setIsConfirmDeleteModalOpen, deleteEpisodeFiles]);
const onConfirmDeleteModalClose = useCallback(() => {
const handleConfirmDeleteModalClose = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
}, [setIsConfirmDeleteModalOpen]);
const onImportSelectedPress = useCallback(() => {
const handleImportSelectedPress = useCallback(() => {
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
const existingFiles: Partial<EpisodeFile>[] = [];
@ -626,41 +616,38 @@ function InteractiveImportModalContentInner(
updateEpisodeFiles,
]);
const onSortPress = useCallback<SortCallback>(
(sortKey, sortDirection) => {
dispatch(setInteractiveImportSort({ sortKey, sortDirection }));
const handleSetInteractiveImportMode = useCallback(
({ importMode }: { importMode: ImportMode }) => {
setInteractiveImportOption('importMode', importMode);
},
[dispatch]
[]
);
const onFilterExistingFilesChange = useCallback(
const handleSortPress = useCallback<SortCallback>(
(sortKey, sortDirection) => {
setInteractiveImportSort({ sortKey, sortDirection });
},
[]
);
const handleFilterExistingFilesChange = useCallback(
(value: string | undefined) => {
const filter = value !== 'all';
setFilterExistingFiles(filter);
dispatch(
fetchInteractiveImportItems({
downloadId,
seriesId,
folder,
filterExistingFiles: filter,
})
);
filterExistingFilesStore.setState(filter);
},
[downloadId, seriesId, folder, setFilterExistingFiles, dispatch]
[]
);
const onImportModeChange = useCallback<
const handleImportModeChange = useCallback<
({ value }: { value: ImportMode }) => void
>(
({ value }) => {
dispatch(setInteractiveImportMode({ importMode: value }));
handleSetInteractiveImportMode({ importMode: value });
},
[dispatch]
[handleSetInteractiveImportMode]
);
const onSelectModalSelect = useCallback<
const handleSelectModalSelect = useCallback<
({ value }: { value: SelectType }) => void
>(
({ value }) => {
@ -669,143 +656,154 @@ function InteractiveImportModalContentInner(
[setSelectModalOpen]
);
const onSelectModalClose = useCallback(() => {
const handleSelectModalClose = useCallback(() => {
setSelectModalOpen(null);
}, [setSelectModalOpen]);
const onSeriesSelect = useCallback(
(series: Series) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
series,
seasonNumber: undefined,
episodes: [],
})
);
const handleReprocessItems = useCallback(
(ids: number[]) => {
setReprocessingItems((prev) => {
const newSet = new Set(prev);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
ids.forEach((id) => newSet.add(id));
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(
(selectedEpisodes: SelectedEpisode[]) => {
selectedEpisodes.forEach((selectedEpisode) => {
const { id, episodes } = selectedEpisode;
dispatch(
updateInteractiveImportItem({
id,
episodes,
})
);
return newSet;
});
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
reprocessInteractiveImportItems(ids);
},
[reprocessInteractiveImportItems]
);
const handleSeriesSelect = useCallback(
(series: Series) => {
const updates = {
series,
seasonNumber: undefined,
episodes: [],
};
updateInteractiveImportItems(selectedIds, updates);
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const handleSeasonSelect = useCallback(
(seasonNumber: number) => {
const updates = {
seasonNumber,
episodes: [],
};
updateInteractiveImportItems(selectedIds, updates);
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, setSelectModalOpen, dispatch]
[
selectedIds,
setSelectModalOpen,
updateInteractiveImportItems,
handleReprocessItems,
]
);
const onReleaseGroupSelect = useCallback(
const handleEpisodesSelect = useCallback(
(selectedEpisodes: SelectedEpisode[]) => {
selectedEpisodes.forEach(({ id, episodes }) => {
updateInteractiveImportItem(id, { episodes });
});
const selectedIds = selectedEpisodes.map(({ id }) => id);
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[updateInteractiveImportItem, setSelectModalOpen, handleReprocessItems]
);
const handleReleaseGroupSelect = useCallback(
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
releaseGroup,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
updateInteractiveImportItems(selectedIds, { releaseGroup });
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, dispatch]
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const onLanguagesSelect = useCallback(
const handleLanguagesSelect = useCallback(
(newLanguages: Language[]) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
languages: newLanguages,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
updateInteractiveImportItems(selectedIds, { languages: newLanguages });
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, dispatch]
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const onQualitySelect = useCallback(
const handleQualitySelect = useCallback(
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
quality,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
updateInteractiveImportItems(selectedIds, { quality });
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, dispatch]
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const onIndexerFlagsSelect = useCallback(
const handleIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
updateInteractiveImportItems(selectedIds, { indexerFlags });
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, dispatch]
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const onReleaseTypeSelect = useCallback(
const handleReleaseTypeSelect = useCallback(
(releaseType: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: selectedIds }));
updateInteractiveImportItems(selectedIds, {
releaseType: releaseType as ReleaseType,
});
handleReprocessItems(selectedIds);
setSelectModalOpen(null);
},
[selectedIds, dispatch]
[
selectedIds,
updateInteractiveImportItems,
setSelectModalOpen,
handleReprocessItems,
]
);
const orderedSelectedIds = items.reduce((acc: number[], file) => {
@ -832,7 +830,7 @@ function InteractiveImportModalContentInner(
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
{showFilterExistingFiles && (
{showFilterExistingFiles ? (
<div className={styles.filterContainer}>
<Menu alignMenu={align.RIGHT}>
<MenuButton>
@ -849,7 +847,7 @@ function InteractiveImportModalContentInner(
<SelectedMenuItem
name="all"
isSelected={!filterExistingFiles}
onPress={onFilterExistingFilesChange}
onPress={handleFilterExistingFilesChange}
>
{translate('AllFiles')}
</SelectedMenuItem>
@ -857,14 +855,14 @@ function InteractiveImportModalContentInner(
<SelectedMenuItem
name="new"
isSelected={filterExistingFiles}
onPress={onFilterExistingFilesChange}
onPress={handleFilterExistingFilesChange}
>
{translate('UnmappedFilesOnly')}
</SelectedMenuItem>
</MenuContent>
</Menu>
</div>
)}
) : null}
{isFetching ? <LoadingIndicator /> : null}
@ -879,8 +877,8 @@ function InteractiveImportModalContentInner(
allUnselected={allUnselected}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onSelectAllChange={onSelectAllChange}
onSortPress={handleSortPress}
onSelectAllChange={handleSelectAllChange}
>
<TableBody>
{items.map((item) => {
@ -891,8 +889,10 @@ function InteractiveImportModalContentInner(
allowSeriesChange={allowSeriesChange}
columns={columns}
modalTitle={modalTitle}
onSelectedChange={onSelectedChange}
onValidRowChange={onValidRowChange}
isReprocessing={reprocessingItems.has(item.id)}
onReprocessItems={handleReprocessItems}
onSelectedChange={handleSelectedChange}
onValidRowChange={handleValidRowChange}
/>
);
})}
@ -915,7 +915,7 @@ function InteractiveImportModalContentInner(
isDisabled={
!selectedIds.length || !!withoutEpisodeFileIdRowsSelected.length
}
onPress={onDeleteSelectedPress}
onPress={handleDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
@ -927,7 +927,7 @@ function InteractiveImportModalContentInner(
name="importMode"
value={importMode}
values={importModeOptions}
onChange={onImportModeChange}
onChange={handleImportModeChange}
/>
) : null}
@ -937,7 +937,7 @@ function InteractiveImportModalContentInner(
value="select"
values={bulkSelectOptions}
isDisabled={!selectedIds.length}
onChange={onSelectModalSelect}
onChange={handleSelectModalSelect}
/>
</div>
@ -953,7 +953,7 @@ function InteractiveImportModalContentInner(
<Button
kind={kinds.SUCCESS}
isDisabled={!selectedIds.length || !!invalidRowsSelected.length}
onPress={onImportSelectedPress}
onPress={handleImportSelectedPress}
>
{folder ? translate('Apply') : translate('Import')}
</Button>
@ -963,16 +963,16 @@ function InteractiveImportModalContentInner(
<SelectSeriesModal
isOpen={selectModalOpen === 'series'}
modalTitle={modalTitle}
onSeriesSelect={onSeriesSelect}
onModalClose={onSelectModalClose}
onSeriesSelect={handleSeriesSelect}
onModalClose={handleSelectModalClose}
/>
<SelectSeasonModal
isOpen={selectModalOpen === 'season'}
seriesId={selectedItem?.series?.id}
modalTitle={modalTitle}
onSeasonSelect={onSeasonSelect}
onModalClose={onSelectModalClose}
onSeasonSelect={handleSeasonSelect}
onModalClose={handleSelectModalClose}
/>
<SelectEpisodeModal
@ -982,24 +982,24 @@ function InteractiveImportModalContentInner(
seasonNumber={selectedItem?.seasonNumber}
isAnime={selectedItem?.series?.seriesType === 'anime'}
modalTitle={modalTitle}
onEpisodesSelect={onEpisodesSelect}
onModalClose={onSelectModalClose}
onEpisodesSelect={handleEpisodesSelect}
onModalClose={handleSelectModalClose}
/>
<SelectReleaseGroupModal
isOpen={selectModalOpen === 'releaseGroup'}
releaseGroup=""
modalTitle={modalTitle}
onReleaseGroupSelect={onReleaseGroupSelect}
onModalClose={onSelectModalClose}
onReleaseGroupSelect={handleReleaseGroupSelect}
onModalClose={handleSelectModalClose}
/>
<SelectLanguageModal
isOpen={selectModalOpen === 'language'}
languageIds={[0]}
modalTitle={modalTitle}
onLanguagesSelect={onLanguagesSelect}
onModalClose={onSelectModalClose}
onLanguagesSelect={handleLanguagesSelect}
onModalClose={handleSelectModalClose}
/>
<SelectQualityModal
@ -1008,24 +1008,24 @@ function InteractiveImportModalContentInner(
proper={false}
real={false}
modalTitle={modalTitle}
onQualitySelect={onQualitySelect}
onModalClose={onSelectModalClose}
onQualitySelect={handleQualitySelect}
onModalClose={handleSelectModalClose}
/>
<SelectIndexerFlagsModal
isOpen={selectModalOpen === 'indexerFlags'}
indexerFlags={0}
modalTitle={modalTitle}
onIndexerFlagsSelect={onIndexerFlagsSelect}
onModalClose={onSelectModalClose}
onIndexerFlagsSelect={handleIndexerFlagsSelect}
onModalClose={handleSelectModalClose}
/>
<SelectReleaseTypeModal
isOpen={selectModalOpen === 'releaseType'}
releaseType="unknown"
modalTitle={modalTitle}
onReleaseTypeSelect={onReleaseTypeSelect}
onModalClose={onSelectModalClose}
onReleaseTypeSelect={handleReleaseTypeSelect}
onModalClose={handleSelectModalClose}
/>
<ConfirmModal
@ -1034,8 +1034,8 @@ function InteractiveImportModalContentInner(
title={translate('DeleteSelectedEpisodeFiles')}
message={translate('DeleteSelectedEpisodeFilesHelpText')}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onConfirmDeleteModalClose}
onConfirm={handleConfirmDelete}
onCancel={handleConfirmDeleteModalClose}
/>
</ModalContent>
);
@ -1044,12 +1044,19 @@ function InteractiveImportModalContentInner(
function InteractiveImportModalContent(
props: InteractiveImportModalContentProps
) {
const { items }: InteractiveImportAppState = useSelector(
createClientSideCollectionSelector('interactiveImport')
);
const filterExistingFiles = filterExistingFilesStore((state) => state);
const { downloadId, seriesId, seasonNumber, folder } = props;
const { data } = useInteractiveImport({
downloadId,
seriesId,
seasonNumber,
folder,
filterExistingFiles,
});
return (
<SelectProvider<InteractiveImport> items={items}>
<SelectProvider<InteractiveImport> items={data}>
<InteractiveImportModalContentInner {...props} />
</SelectProvider>
);

View file

@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useSelect } from 'App/Select/SelectContext';
import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -27,13 +26,10 @@ import ReleaseType from 'InteractiveImport/ReleaseType';
import SelectReleaseTypeModal from 'InteractiveImport/ReleaseType/SelectReleaseTypeModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import { useUpdateInteractiveImportItem } from 'InteractiveImport/useInteractiveImport';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
@ -77,6 +73,7 @@ interface InteractiveImportRowProps {
episodeFileId?: number;
isReprocessing?: boolean;
modalTitle: string;
onReprocessItems: (ids: number[]) => void;
onSelectedChange(result: SelectedChangeProps): void;
onValidRowChange(id: number, isValid: boolean): void;
}
@ -102,13 +99,14 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
modalTitle,
episodeFileId,
columns,
onReprocessItems,
onSelectedChange,
onValidRowChange,
} = props;
const dispatch = useDispatch();
const { useIsSelected } = useSelect<InteractiveImport>();
const isSelected = useIsSelected(id);
const { updateInteractiveImportItem } = useUpdateInteractiveImportItem();
const isSeriesColumnVisible = useMemo(
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
@ -202,21 +200,23 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onSeriesSelect = useCallback(
(series: Series) => {
dispatch(
updateInteractiveImportItem({
id,
series,
seasonNumber: undefined,
episodes: [],
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, {
series,
seasonNumber: undefined,
episodes: [],
});
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectSeasonPress = useCallback(() => {
@ -225,20 +225,22 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onSeasonSelect = useCallback(
(seasonNumber: number) => {
dispatch(
updateInteractiveImportItem({
id,
seasonNumber,
episodes: [],
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, {
seasonNumber,
episodes: [],
});
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectEpisodePress = useCallback(() => {
@ -247,19 +249,20 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onEpisodesSelect = useCallback(
(selectedEpisodes: SelectedEpisode[]) => {
dispatch(
updateInteractiveImportItem({
id,
episodes: selectedEpisodes[0].episodes,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
const episodes = selectedEpisodes[0].episodes;
updateInteractiveImportItem(id, { episodes });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectReleaseGroupPress = useCallback(() => {
@ -268,19 +271,19 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onReleaseGroupSelect = useCallback(
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItem({
id,
releaseGroup,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, { releaseGroup });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectQualityPress = useCallback(() => {
@ -289,19 +292,19 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onQualitySelect = useCallback(
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItem({
id,
quality,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, { quality });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectLanguagePress = useCallback(() => {
@ -310,19 +313,19 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onLanguagesSelect = useCallback(
(languages: Language[]) => {
dispatch(
updateInteractiveImportItem({
id,
languages,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, { languages });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectReleaseTypePress = useCallback(() => {
@ -331,19 +334,19 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onReleaseTypeSelect = useCallback(
(releaseType: ReleaseType) => {
dispatch(
updateInteractiveImportItem({
id,
releaseType,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, { releaseType });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const onSelectIndexerFlagsPress = useCallback(() => {
@ -352,19 +355,19 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const onIndexerFlagsSelect = useCallback(
(indexerFlags: number) => {
dispatch(
updateInteractiveImportItem({
id,
indexerFlags,
})
);
dispatch(reprocessInteractiveImportItems({ ids: [id] }));
updateInteractiveImportItem(id, { indexerFlags });
onReprocessItems([id]);
setSelectModalOpen(null);
selectRowAfterChange();
},
[id, dispatch, setSelectModalOpen, selectRowAfterChange]
[
id,
updateInteractiveImportItem,
onReprocessItems,
setSelectModalOpen,
selectRowAfterChange,
]
);
const seriesTitle = series ? series.title : '';
@ -547,7 +550,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
body={
<ul>
{rejections.map((rejection, index) => {
return <li key={index}>{rejection.reason}</li>;
return <li key={index}>{rejection.message}</li>;
})}
</ul>
}

View file

@ -39,6 +39,7 @@ interface InteractiveImport extends ModelBase {
releaseType: ReleaseType;
rejections: Rejection[];
episodeFileId?: number;
downloadId?: string;
}
export default InteractiveImport;

View file

@ -0,0 +1,121 @@
import moment from 'moment';
import { createPersist } from 'Helpers/createPersist';
import sortByProp from 'Utilities/Array/sortByProp';
const MAXIMUM_RECENT_FOLDERS = 10;
interface RecentFolder {
folder: string;
lastUsed: string;
}
interface FavoriteFolder {
folder: string;
}
interface InteractiveImportFoldersState {
recentFolders: RecentFolder[];
favoriteFolders: FavoriteFolder[];
}
const store = createPersist<InteractiveImportFoldersState>(
'interactive_import_folders',
() => ({
recentFolders: [],
favoriteFolders: [],
})
);
export const useInteractiveImportFolders = () => {
return store((state) => state);
};
export const useRecentFolders = () => {
return store((state) => state.recentFolders);
};
export const useFavoriteFolders = () => {
return store((state) => state.favoriteFolders);
};
export const addRecentFolder = (folder: string) => {
store.setState((state) => {
const recentFolder: RecentFolder = {
folder,
lastUsed: moment().toISOString(),
};
const recentFolders = [...state.recentFolders];
const index = recentFolders.findIndex((r) => r.folder === folder);
if (index > -1) {
recentFolders.splice(index, 1);
}
recentFolders.push(recentFolder);
const sliceIndex = Math.max(
recentFolders.length - MAXIMUM_RECENT_FOLDERS,
0
);
return {
...state,
recentFolders: recentFolders.slice(sliceIndex),
};
});
};
export const removeRecentFolder = (folder: string) => {
store.setState((state) => {
const recentFolders = [...state.recentFolders];
const index = recentFolders.findIndex((r) => r.folder === folder);
if (index > -1) {
recentFolders.splice(index, 1);
}
return {
...state,
recentFolders,
};
});
};
export const addFavoriteFolder = (folder: string) => {
store.setState((state) => {
const favoriteFolder: FavoriteFolder = { folder };
const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(
sortByProp('folder')
);
return {
...state,
favoriteFolders,
};
});
};
export const removeFavoriteFolder = (folder: string) => {
store.setState((state) => {
const favoriteFolders = state.favoriteFolders.filter(
(item) => item.folder !== folder
);
return {
...state,
favoriteFolders,
};
});
};
export const getInteractiveImportFolders = () => {
return store.getState();
};
export const getRecentFolders = () => {
return store.getState().recentFolders;
};
export const getFavoriteFolders = () => {
return store.getState().favoriteFolders;
};

View file

@ -0,0 +1,111 @@
import {
createOptionsStore,
PageableOptions,
} from 'Helpers/Hooks/useOptionsStore';
import ImportMode from './ImportMode';
export interface InteractiveImportOptions
extends Omit<PageableOptions, 'pageSize'> {
importMode: ImportMode;
}
export 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: 'Episodes',
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: 'releaseType',
label: 'Release Type',
isSortable: true,
isVisible: true,
},
{
name: 'customFormats',
label: 'Custom Formats',
isSortable: true,
isVisible: true,
},
{
name: 'indexerFlags',
label: 'Indexer Flags',
isSortable: true,
isVisible: true,
},
{
name: 'rejections',
label: 'Rejections',
isSortable: true,
isVisible: true,
},
];
const {
useOptions,
useOption,
getOptions,
getOption,
setOptions,
setOption,
setSort,
} = createOptionsStore<InteractiveImportOptions>(
'interactive_import_options',
() => {
return {
selectedFilterKey: 'all',
sortKey: 'relativePath',
sortDirection: 'ascending',
importMode: 'chooseImportMode',
columns: COLUMNS,
};
}
);
export const useInteractiveImportOptions = useOptions;
export const getInteractiveImportOptions = getOptions;
export const setInteractiveImportOptions = setOptions;
export const useInteractiveImportOption = useOption;
export const getInteractiveImportOption = getOption;
export const setInteractiveImportOption = setOption;
export const setInteractiveImportSort = setSort;

View file

@ -0,0 +1,209 @@
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import ModelBase from 'App/ModelBase';
import useApiMutation from 'Helpers/Hooks/useApiMutation';
import useApiQuery from 'Helpers/Hooks/useApiQuery';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import clientSideFilterAndSort from 'Utilities/Filter/clientSideFilterAndSort';
import InteractiveImport from './InteractiveImport';
import { useInteractiveImportOptions } from './interactiveImportOptionsStore';
import ReleaseType from './ReleaseType';
const DEFAULT_ITEMS: InteractiveImport[] = [];
interface InteractiveImportParams {
downloadId?: string;
seriesId?: number;
seasonNumber?: number;
folder?: string;
filterExistingFiles?: boolean;
}
const useInteractiveImport = (params: InteractiveImportParams) => {
const { sortKey, sortDirection } = useInteractiveImportOptions();
const { data, isFetching, isFetched, error, refetch } = useApiQuery<
InteractiveImport[]
>({
path: '/manualimport',
queryParams: { ...params },
queryOptions: {
// Set to 0 so we don't persist the data after the modal is closed and the query becomes inactive
gcTime: 0,
// Disable refetch on window focus to prevent refetching when the user switch tabs
refetchOnWindowFocus: false,
},
});
const items = data ?? DEFAULT_ITEMS;
const originalItems = [...items];
const { data: sortedItems } = useMemo(() => {
const sortPredicates = {
series: (item: InteractiveImport) => item.series?.title || '',
quality: (item: InteractiveImport) => item.quality?.quality?.name || '',
languages: (item: InteractiveImport) =>
item.languages?.map((l) => l.name).join(', ') || '',
};
return clientSideFilterAndSort(items, {
sortKey,
sortDirection,
sortPredicates,
});
}, [items, sortKey, sortDirection]);
return {
data: sortedItems,
originalItems,
isFetching,
isFetched,
error,
refetch,
};
};
export default useInteractiveImport;
export const useUpdateInteractiveImportItem = () => {
const queryClient = useQueryClient();
const updateInteractiveImportItem = useCallback(
(id: number, updates: Partial<InteractiveImport>) => {
queryClient.setQueriesData(
{ queryKey: ['/manualimport'] },
(oldData: InteractiveImport[] | undefined) => {
if (!oldData) {
return oldData;
}
return oldData.map((item) => {
return item.id === id ? { ...item, ...updates } : item;
});
}
);
},
[queryClient]
);
return { updateInteractiveImportItem };
};
export const useUpdateInteractiveImportItems = () => {
const queryClient = useQueryClient();
const updateInteractiveImportItems = useCallback(
(ids: number[], updates: Partial<InteractiveImport>) => {
queryClient.setQueriesData(
{ queryKey: ['/manualimport'] },
(oldData: InteractiveImport[] | undefined) => {
if (!oldData) {
return oldData;
}
return oldData.map((item) => {
return ids.includes(item.id) ? { ...item, ...updates } : item;
});
}
);
},
[queryClient]
);
return { updateInteractiveImportItems };
};
interface ReprocessInteractiveImportItem extends ModelBase {
path: string;
seriesId: number | undefined;
seasonNumber: number | undefined;
episodeIds: number[] | undefined;
quality: QualityModel | undefined;
languages: Language[];
releaseGroup: string | undefined;
downloadId: string | undefined;
indexerFlags: number;
releaseType: ReleaseType;
}
export const useReprocessInteractiveImportItems = () => {
const queryClient = useQueryClient();
const { mutate, isPending, error } = useApiMutation<
InteractiveImport[],
ReprocessInteractiveImportItem[]
>({
path: '/manualimport',
method: 'POST',
mutationOptions: {
onSuccess: (updatedItems) => {
queryClient.setQueriesData(
{ queryKey: ['/manualimport'] },
(oldData: InteractiveImport[] | undefined) => {
if (!oldData) {
return oldData;
}
return oldData.map((oldItem: InteractiveImport) => {
const reprocessedItem = updatedItems.find(
(updatedItem) => updatedItem.id === oldItem.id
);
return reprocessedItem ? reprocessedItem : oldItem;
});
}
);
},
},
});
const reprocessInteractiveImportItems = useCallback(
(ids: number[]) => {
const [, currentData] = queryClient.getQueriesData<InteractiveImport[]>({
queryKey: ['/manualimport'],
})[0];
if (!currentData) {
console.info('\x1b[36m[MarkTest] no data\x1b[0m');
return;
}
const requestPayload = ids.reduce<ReprocessInteractiveImportItem[]>(
(acc, id) => {
const item = currentData.find((i) => i.id === id);
if (!item) {
return acc;
}
acc.push({
id,
path: item.path,
seriesId: item.series ? item.series.id : undefined,
seasonNumber: item.seasonNumber,
episodeIds: (item.episodes || []).map((e) => e.id),
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
releaseType: item.releaseType,
downloadId: item.downloadId,
});
return acc;
},
[]
);
mutate(requestPayload);
},
[queryClient, mutate]
);
return {
reprocessInteractiveImportItems,
isReprocessing: isPending,
error,
};
};

View file

@ -1,6 +1,5 @@
import * as captcha from './captchaActions';
import * as importSeries from './importSeriesActions';
import * as interactiveImportActions from './interactiveImportActions';
import * as oAuth from './oAuthActions';
import * as providerOptions from './providerOptionActions';
import * as settings from './settingsActions';
@ -8,7 +7,6 @@ import * as settings from './settingsActions';
export default [
captcha,
importSeries,
interactiveImportActions,
oAuth,
providerOptions,
settings

View file

@ -1,316 +0,0 @@
import moment from 'moment';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import sortByProp from 'Utilities/Array/sortByProp';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import naturalExpansion from 'Utilities/String/naturalExpansion';
import { set, update, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
//
// Variables
export const section = 'interactiveImport';
let abortCurrentRequest = null;
let currentIds = [];
const MAXIMUM_RECENT_FOLDERS = 10;
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: [],
originalItems: [],
sortKey: 'relativePath',
sortDirection: sortDirections.ASCENDING,
favoriteFolders: [],
recentFolders: [],
importMode: 'chooseImportMode',
sortPredicates: {
relativePath: function(item, direction) {
const relativePath = item.relativePath;
return naturalExpansion(relativePath.toLowerCase());
},
series: function(item, direction) {
const series = item.series;
return series ? series.sortTitle : '';
},
quality: function(item, direction) {
return item.qualityWeight || 0;
},
customFormats: function(item, direction) {
return item.customFormatScore;
}
}
};
export const persistState = [
'interactiveImport.sortKey',
'interactiveImport.sortDirection',
'interactiveImport.favoriteFolders',
'interactiveImport.recentFolders',
'interactiveImport.importMode'
];
//
// Actions Types
export const FETCH_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/fetchInteractiveImportItems';
export const REPROCESS_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/reprocessInteractiveImportItems';
export const SET_INTERACTIVE_IMPORT_SORT = 'interactiveImport/setInteractiveImportSort';
export const UPDATE_INTERACTIVE_IMPORT_ITEM = 'interactiveImport/updateInteractiveImportItem';
export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteractiveImportItems';
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder';
export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder';
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
//
// Action Creators
export const fetchInteractiveImportItems = createThunk(FETCH_INTERACTIVE_IMPORT_ITEMS);
export const reprocessInteractiveImportItems = createThunk(REPROCESS_INTERACTIVE_IMPORT_ITEMS);
export const setInteractiveImportSort = createAction(SET_INTERACTIVE_IMPORT_SORT);
export const updateInteractiveImportItem = createAction(UPDATE_INTERACTIVE_IMPORT_ITEM);
export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPORT_ITEMS);
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER);
export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER);
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
if (!payload.downloadId && !payload.folder) {
dispatch(set({ section, error: { message: '`downloadId` or `folder` is required.' } }));
return;
}
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/manualimport',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null,
originalItems: data
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
},
[REPROCESS_INTERACTIVE_IMPORT_ITEMS]: function(getState, payload, dispatch) {
if (abortCurrentRequest) {
abortCurrentRequest();
}
dispatch(batchActions([
...currentIds.map((id) => updateItem({
section,
id,
isReprocessing: false,
updateOnly: true
})),
...payload.ids.map((id) => updateItem({
section,
id,
isReprocessing: true,
updateOnly: true
}))
]));
const items = getState()[section].items;
const requestPayload = payload.ids.map((id) => {
const item = items.find((i) => i.id === id);
return {
id,
path: item.path,
seriesId: item.series ? item.series.id : undefined,
seasonNumber: item.seasonNumber,
episodeIds: (item.episodes || []).map((e) => e.id),
quality: item.quality,
languages: item.languages,
releaseGroup: item.releaseGroup,
indexerFlags: item.indexerFlags,
releaseType: item.releaseType,
downloadId: item.downloadId
};
});
const { request, abortRequest } = createAjaxRequest({
method: 'POST',
url: '/manualimport',
contentType: 'application/json',
data: JSON.stringify(requestPayload)
});
abortCurrentRequest = abortRequest;
currentIds = payload.ids;
request.done((data) => {
dispatch(batchActions(
data.map((item) => updateItem({
section,
...item,
isReprocessing: false,
updateOnly: true
}))
));
});
request.fail((xhr) => {
if (xhr.aborted) {
return;
}
dispatch(batchActions(
payload.ids.map((id) => updateItem({
section,
id,
isReprocessing: false,
updateOnly: true
}))
));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[UPDATE_INTERACTIVE_IMPORT_ITEM]: (state, { payload }) => {
const id = payload.id;
const newState = Object.assign({}, state);
const items = newState.items;
const index = items.findIndex((item) => item.id === id);
const item = Object.assign({}, items[index], payload);
newState.items = [...items];
newState.items.splice(index, 1, item);
return newState;
},
[UPDATE_INTERACTIVE_IMPORT_ITEMS]: (state, { payload }) => {
const { ids, ...otherPayload } = payload;
const newState = Object.assign({}, state);
const items = [...newState.items];
ids.forEach((id) => {
const index = items.findIndex((item) => item.id === id);
const item = Object.assign({}, items[index], otherPayload);
items.splice(index, 1, item);
});
newState.items = items;
return newState;
},
[ADD_RECENT_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const recentFolder = { folder, lastUsed: moment().toISOString() };
const recentFolders = [...state.recentFolders];
const index = recentFolders.findIndex((r) => r.folder === folder);
if (index > -1) {
recentFolders.splice(index, 1);
}
recentFolders.push(recentFolder);
const sliceIndex = Math.max(recentFolders.length - MAXIMUM_RECENT_FOLDERS, 0);
return Object.assign({}, state, { recentFolders: recentFolders.slice(sliceIndex) });
},
[REMOVE_RECENT_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const recentFolders = [...state.recentFolders];
const index = recentFolders.findIndex((r) => r.folder === folder);
recentFolders.splice(index, 1);
return Object.assign({}, state, { recentFolders });
},
[ADD_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolder = { folder };
const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder'));
return Object.assign({}, state, { favoriteFolders });
},
[REMOVE_FAVORITE_FOLDER]: function(state, { payload }) {
const folder = payload.folder;
const favoriteFolders = state.favoriteFolders.reduce((acc, item) => {
if (item.folder !== folder) {
acc.push(item);
}
return acc;
}, []);
return Object.assign({}, state, { favoriteFolders });
},
[CLEAR_INTERACTIVE_IMPORT]: function(state) {
const newState = {
...defaultState,
favoriteFolders: state.favoriteFolders,
recentFolders: state.recentFolders,
importMode: state.importMode
};
return newState;
},
[SET_INTERACTIVE_IMPORT_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_INTERACTIVE_IMPORT_MODE]: function(state, { payload }) {
return Object.assign({}, state, { importMode: payload.importMode });
}
}, defaultState, section);