mirror of
https://github.com/Sonarr/Sonarr
synced 2026-05-06 03:51:06 +02:00
Use react-query for manual import
This commit is contained in:
parent
8da611ea58
commit
ec44e1c513
13 changed files with 767 additions and 671 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ interface InteractiveImport extends ModelBase {
|
|||
releaseType: ReleaseType;
|
||||
rejections: Rejection[];
|
||||
episodeFileId?: number;
|
||||
downloadId?: string;
|
||||
}
|
||||
|
||||
export default InteractiveImport;
|
||||
|
|
|
|||
121
frontend/src/InteractiveImport/interactiveImportFoldersStore.ts
Normal file
121
frontend/src/InteractiveImport/interactiveImportFoldersStore.ts
Normal 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;
|
||||
};
|
||||
111
frontend/src/InteractiveImport/interactiveImportOptionsStore.ts
Normal file
111
frontend/src/InteractiveImport/interactiveImportOptionsStore.ts
Normal 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;
|
||||
209
frontend/src/InteractiveImport/useInteractiveImport.ts
Normal file
209
frontend/src/InteractiveImport/useInteractiveImport.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
Loading…
Reference in a new issue