mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-20 21:54:58 -04:00
Use react-query for series import
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import {
|
||||
setAddSeriesOption,
|
||||
@@ -13,13 +13,12 @@ import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import useRootFolders, { useRootFolder } from 'RootFolder/useRootFolders';
|
||||
import { clearImportSeries } from 'Store/Actions/importSeriesActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportSeriesFooter from './ImportSeriesFooter';
|
||||
import { clearImportSeries } from './importSeriesStore';
|
||||
import ImportSeriesTable from './ImportSeriesTable';
|
||||
|
||||
function ImportSeries() {
|
||||
const dispatch = useDispatch();
|
||||
const { rootFolderId: rootFolderIdString } = useParams<{
|
||||
rootFolderId: string;
|
||||
}>();
|
||||
@@ -68,9 +67,9 @@ function ImportSeries() {
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearImportSeries());
|
||||
clearImportSeries();
|
||||
};
|
||||
}, [rootFolderId, dispatch]);
|
||||
}, [rootFolderId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -79,13 +78,15 @@ function ImportSeries() {
|
||||
) {
|
||||
setAddSeriesOption('qualityProfileId', qualityProfiles[0].id);
|
||||
}
|
||||
}, [defaultQualityProfileId, qualityProfiles, dispatch]);
|
||||
}, [defaultQualityProfileId, qualityProfiles]);
|
||||
|
||||
return (
|
||||
<SelectProvider items={items}>
|
||||
<PageContent title={translate('ImportSeries')}>
|
||||
<PageContentBody ref={scrollerRef}>
|
||||
{rootFoldersFetching ? <LoadingIndicator /> : null}
|
||||
{rootFoldersFetching && !rootFoldersFetched ? (
|
||||
<LoadingIndicator />
|
||||
) : null}
|
||||
|
||||
{!rootFoldersFetching && !!rootFoldersError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
@@ -103,20 +104,14 @@ function ImportSeries() {
|
||||
) : null}
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersFetched &&
|
||||
!!unmappedFolders.length &&
|
||||
scrollerRef.current ? (
|
||||
<ImportSeriesTable
|
||||
unmappedFolders={unmappedFolders}
|
||||
scrollerRef={scrollerRef}
|
||||
/>
|
||||
<ImportSeriesTable items={items} scrollerRef={scrollerRef} />
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
{!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
!!unmappedFolders.length ? (
|
||||
{!rootFoldersError && rootFoldersFetched && !!unmappedFolders.length ? (
|
||||
<ImportSeriesFooter />
|
||||
) : null}
|
||||
</PageContent>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import {
|
||||
AddSeriesOptions,
|
||||
setAddSeriesOption,
|
||||
useAddSeriesOptions,
|
||||
} from 'AddSeries/addSeriesOptionsStore';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -17,20 +15,22 @@ import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||
import {
|
||||
cancelLookupSeries,
|
||||
importSeries,
|
||||
lookupUnsearchedSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import {
|
||||
ImportSeriesItem,
|
||||
startProcessing,
|
||||
stopProcessing,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItems,
|
||||
useLookupQueueHasItems,
|
||||
} from './importSeriesStore';
|
||||
import { useImportSeries } from './useImportSeries';
|
||||
import styles from './ImportSeriesFooter.css';
|
||||
|
||||
type MixedType = 'mixed';
|
||||
|
||||
function ImportSeriesFooter() {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
monitor: defaultMonitor,
|
||||
qualityProfileId: defaultQualityProfileId,
|
||||
@@ -38,9 +38,8 @@ function ImportSeriesFooter() {
|
||||
seasonFolder: defaultSeasonFolder,
|
||||
} = useAddSeriesOptions();
|
||||
|
||||
const { isLookingUpSeries, isImporting, items, importError } = useSelector(
|
||||
(state: AppState) => state.importSeries
|
||||
);
|
||||
const items = useImportSeriesItems();
|
||||
const isLookingUpSeries = useLookupQueueHasItems();
|
||||
|
||||
const [monitor, setMonitor] = useState<SeriesMonitor | MixedType>(
|
||||
defaultMonitor
|
||||
@@ -55,7 +54,9 @@ function ImportSeriesFooter() {
|
||||
defaultSeasonFolder
|
||||
);
|
||||
|
||||
const { selectedCount, getSelectedIds } = useSelect();
|
||||
const { selectedCount, getSelectedIds } = useSelect<ImportSeriesItem>();
|
||||
|
||||
const { importSeries, isImporting, importError } = useImportSeries();
|
||||
|
||||
const {
|
||||
hasUnsearchedItems,
|
||||
@@ -87,7 +88,7 @@ function ImportSeriesFooter() {
|
||||
isSeasonFolderMixed = true;
|
||||
}
|
||||
|
||||
if (!item.isPopulated) {
|
||||
if (!item.hasSearched) {
|
||||
hasUnsearchedItems = true;
|
||||
}
|
||||
});
|
||||
@@ -123,29 +124,26 @@ function ImportSeriesFooter() {
|
||||
setAddSeriesOption(name as keyof AddSeriesOptions, value);
|
||||
|
||||
getSelectedIds().forEach((id) => {
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
[name]: value,
|
||||
});
|
||||
});
|
||||
},
|
||||
[getSelectedIds, dispatch]
|
||||
[getSelectedIds]
|
||||
);
|
||||
|
||||
const handleLookupPress = useCallback(() => {
|
||||
dispatch(lookupUnsearchedSeries());
|
||||
}, [dispatch]);
|
||||
startProcessing();
|
||||
}, []);
|
||||
|
||||
const handleCancelLookupPress = useCallback(() => {
|
||||
dispatch(cancelLookupSeries());
|
||||
}, [dispatch]);
|
||||
stopProcessing();
|
||||
}, []);
|
||||
|
||||
const handleImportPress = useCallback(() => {
|
||||
dispatch(importSeries({ ids: getSelectedIds() }));
|
||||
}, [getSelectedIds, dispatch]);
|
||||
importSeries(getSelectedIds());
|
||||
}, [importSeries, getSelectedIds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isMonitorMixed && monitor !== 'mixed') {
|
||||
@@ -286,12 +284,12 @@ function ImportSeriesFooter() {
|
||||
title={translate('ImportErrors')}
|
||||
body={
|
||||
<ul>
|
||||
{Array.isArray(importError.responseJSON) ? (
|
||||
importError.responseJSON.map((error, index) => {
|
||||
{Array.isArray(importError.statusBody) ? (
|
||||
importError.statusBody.map((error, index) => {
|
||||
return <li key={index}>{error.errorMessage}</li>;
|
||||
})
|
||||
) : (
|
||||
<li>{JSON.stringify(importError.responseJSON)}</li>
|
||||
<li>{JSON.stringify(importError.statusBody)}</li>
|
||||
)}
|
||||
</ul>
|
||||
}
|
||||
|
||||
@@ -1,39 +1,29 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import useExistingSeries from 'Series/useExistingSeries';
|
||||
import { setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import {
|
||||
ImportSeriesItem,
|
||||
UnamppedFolderItem,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItem,
|
||||
} from './importSeriesStore';
|
||||
import ImportSeriesSelectSeries from './SelectSeries/ImportSeriesSelectSeries';
|
||||
import styles from './ImportSeriesRow.css';
|
||||
|
||||
function createItemSelector(id: string) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.importSeries.items,
|
||||
(items) => {
|
||||
return (
|
||||
items.find((item) => {
|
||||
return item.id === id;
|
||||
}) || ({} as ImportSeries)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ImportSeriesRowProps {
|
||||
id: string;
|
||||
unmappedFolder: UnamppedFolderItem;
|
||||
}
|
||||
|
||||
function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
||||
const dispatch = useDispatch();
|
||||
function ImportSeriesRow({ unmappedFolder }: ImportSeriesRowProps) {
|
||||
const id = unmappedFolder.id;
|
||||
|
||||
const item = useImportSeriesItem(unmappedFolder.id);
|
||||
|
||||
const {
|
||||
relativePath,
|
||||
@@ -42,24 +32,18 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
||||
seasonFolder,
|
||||
seriesType,
|
||||
selectedSeries,
|
||||
} = useSelector(createItemSelector(id));
|
||||
} = item ?? {};
|
||||
|
||||
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
|
||||
|
||||
const { getIsSelected, toggleSelected, toggleDisabled } =
|
||||
useSelect<ImportSeries>();
|
||||
useSelect<ImportSeriesItem>();
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
[name]: value,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({ id, [name]: value });
|
||||
},
|
||||
[id, dispatch]
|
||||
[id]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
@@ -77,6 +61,10 @@ function ImportSeriesRow({ id }: ImportSeriesRowProps) {
|
||||
toggleDisabled(id, !selectedSeries || isExistingSeries);
|
||||
}, [id, selectedSeries, isExistingSeries, toggleDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
toggleSelected({ id, isSelected: !!selectedSeries, shiftKey: false });
|
||||
}, [id, selectedSeries, toggleSelected]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableSelectCell<string>
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
import React, { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import React, { RefObject, useCallback, useRef } from 'react';
|
||||
import { FixedSizeList, ListChildComponentProps } from 'react-window';
|
||||
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
||||
import { useAppDimension } from 'App/appStore';
|
||||
import { useSelect } from 'App/Select/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import { UnmappedFolder } from 'typings/RootFolder';
|
||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||
import ImportSeriesRow from './ImportSeriesRow';
|
||||
import {
|
||||
UnamppedFolderItem,
|
||||
useEnsureImportSeriesItems,
|
||||
} from './importSeriesStore';
|
||||
import styles from './ImportSeriesTable.css';
|
||||
|
||||
const ROW_HEIGHT = 52;
|
||||
|
||||
interface RowItemData {
|
||||
items: ImportSeries[];
|
||||
items: UnamppedFolderItem[];
|
||||
}
|
||||
|
||||
interface ImportSeriesTableProps {
|
||||
unmappedFolders: UnmappedFolder[];
|
||||
items: UnamppedFolderItem[];
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
@@ -49,42 +41,17 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
}}
|
||||
className={styles.row}
|
||||
>
|
||||
<ImportSeriesRow key={item.id} id={item.id} />
|
||||
<ImportSeriesRow key={item.id} unmappedFolder={item} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImportSeriesTable({
|
||||
unmappedFolders,
|
||||
scrollerRef,
|
||||
}: ImportSeriesTableProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { monitor, qualityProfileId, seriesType, seasonFolder } =
|
||||
useAddSeriesOptions();
|
||||
|
||||
const items = useSelector((state: AppState) => state.importSeries.items);
|
||||
function ImportSeriesTable({ items, scrollerRef }: ImportSeriesTableProps) {
|
||||
const isSmallScreen = useAppDimension('isSmallScreen');
|
||||
const { data: allSeries } = useSeries();
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
getIsSelected,
|
||||
toggleSelected,
|
||||
selectAll,
|
||||
unselectAll,
|
||||
} = useSelect();
|
||||
|
||||
const defaultValues = useRef({
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
});
|
||||
const { allSelected, allUnselected, selectAll, unselectAll, useHasItems } =
|
||||
useSelect();
|
||||
|
||||
const listRef = useRef<FixedSizeList<RowItemData>>(null);
|
||||
const initialUnmappedFolders = useRef(unmappedFolders);
|
||||
const previousItems = usePrevious(items);
|
||||
|
||||
const handleSelectAllChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
@@ -97,95 +64,11 @@ function ImportSeriesTable({
|
||||
[selectAll, unselectAll]
|
||||
);
|
||||
|
||||
const handleSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }: SelectStateInputProps<string>) => {
|
||||
toggleSelected({
|
||||
id,
|
||||
isSelected: value,
|
||||
shiftKey,
|
||||
});
|
||||
},
|
||||
[toggleSelected]
|
||||
);
|
||||
const hasSelectItems = useHasItems();
|
||||
|
||||
// TODO: Check if this is still needed
|
||||
const handleRemoveSelectedStateItem = useCallback((_id: string) => {
|
||||
// selectDispatch({
|
||||
// type: 'removeItem',
|
||||
// id,
|
||||
// });
|
||||
}, []);
|
||||
useEnsureImportSeriesItems(items);
|
||||
|
||||
useEffect(() => {
|
||||
initialUnmappedFolders.current.forEach(({ name, path, relativePath }) => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name,
|
||||
path,
|
||||
relativePath,
|
||||
term: name,
|
||||
})
|
||||
);
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id: name,
|
||||
...defaultValues.current,
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
previousItems?.forEach((prevItem) => {
|
||||
const { id } = prevItem;
|
||||
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
handleRemoveSelectedStateItem(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedSeries = item.selectedSeries;
|
||||
const isSelected = getIsSelected(id);
|
||||
|
||||
const isExistingSeries =
|
||||
!!selectedSeries &&
|
||||
allSeries.some((s) => s.tvdbId === selectedSeries.tvdbId);
|
||||
|
||||
if (
|
||||
(!selectedSeries && prevItem.selectedSeries) ||
|
||||
(isExistingSeries && !prevItem.selectedSeries)
|
||||
) {
|
||||
handleSelectedChange({ id, value: false, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSelected && (!selectedSeries || isExistingSeries)) {
|
||||
handleSelectedChange({ id, value: false, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSeries && selectedSeries !== prevItem.selectedSeries) {
|
||||
handleSelectedChange({ id, value: true, shiftKey: false });
|
||||
|
||||
return;
|
||||
}
|
||||
});
|
||||
}, [
|
||||
allSeries,
|
||||
items,
|
||||
previousItems,
|
||||
handleRemoveSelectedStateItem,
|
||||
handleSelectedChange,
|
||||
getIsSelected,
|
||||
]);
|
||||
|
||||
if (!items.length) {
|
||||
if (!items.length || !hasSelectItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+56
-66
@@ -7,23 +7,27 @@ import {
|
||||
useFloating,
|
||||
useInteractions,
|
||||
} from '@floating-ui/react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import useImportSeriesItem from 'AddSeries/ImportSeries/Import/useImportSeriesItem';
|
||||
import AppState from 'App/State/AppState';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useLookupSeries } from 'AddSeries/AddNewSeries/useAddSeries';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import useDebounce from 'Helpers/Hooks/useDebounce';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
queueLookupSeries,
|
||||
setImportSeriesValue,
|
||||
} from 'Store/Actions/importSeriesActions';
|
||||
import useExistingSeries from 'Series/useExistingSeries';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import {
|
||||
addToLookupQueue,
|
||||
removeFromLookupQueue,
|
||||
updateImportSeriesItem,
|
||||
useImportSeriesItem,
|
||||
useIsCurrentedItemQueued,
|
||||
useIsCurrentLookupQueueItem,
|
||||
} from '../importSeriesStore';
|
||||
import ImportSeriesSearchResult from './ImportSeriesSearchResult';
|
||||
import ImportSeriesTitle from './ImportSeriesTitle';
|
||||
import styles from './ImportSeriesSelectSeries.css';
|
||||
@@ -37,28 +41,23 @@ function ImportSeriesSelectSeries({
|
||||
id,
|
||||
onInputChange,
|
||||
}: ImportSeriesSelectSeriesProps) {
|
||||
const dispatch = useDispatch();
|
||||
const isLookingUpSeries = useSelector(
|
||||
(state: AppState) => state.importSeries.isLookingUpSeries
|
||||
const importSeriesItem = useImportSeriesItem(id);
|
||||
const { selectedSeries, name } = importSeriesItem ?? {};
|
||||
const isExistingSeries = useExistingSeries(selectedSeries?.tvdbId);
|
||||
|
||||
const [term, setTerm] = useState(name);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const query = useDebounce(term, term ? 300 : 0);
|
||||
const isCurrentLookupQueueItem = useIsCurrentLookupQueueItem(id);
|
||||
const isQueued = useIsCurrentedItemQueued(id);
|
||||
|
||||
const { isFetching, isFetched, error, data, refetch } = useLookupSeries(
|
||||
query,
|
||||
isCurrentLookupQueueItem
|
||||
);
|
||||
|
||||
const {
|
||||
error,
|
||||
isFetching = true,
|
||||
isPopulated = false,
|
||||
items = [],
|
||||
isQueued = true,
|
||||
selectedSeries,
|
||||
isExistingSeries,
|
||||
term: itemTerm,
|
||||
} = useImportSeriesItem(id);
|
||||
|
||||
const seriesLookupTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const [term, setTerm] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const isLookingUpSeries = isFetching || isQueued;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||
@@ -66,48 +65,26 @@ function ImportSeriesSelectSeries({
|
||||
|
||||
const handleSearchInputChange = useCallback(
|
||||
({ value }: InputChanged<string>) => {
|
||||
if (seriesLookupTimeout.current) {
|
||||
clearTimeout(seriesLookupTimeout.current);
|
||||
}
|
||||
|
||||
setTerm(value);
|
||||
|
||||
seriesLookupTimeout.current = setTimeout(() => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name: id,
|
||||
term: value,
|
||||
topOfQueue: true,
|
||||
})
|
||||
);
|
||||
}, 200);
|
||||
addToLookupQueue(id);
|
||||
},
|
||||
[id, dispatch]
|
||||
[id]
|
||||
);
|
||||
|
||||
const handleRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
queueLookupSeries({
|
||||
name: id,
|
||||
term,
|
||||
topOfQueue: true,
|
||||
})
|
||||
);
|
||||
}, [id, term, dispatch]);
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const handleSeriesSelect = useCallback(
|
||||
(tvdbId: number) => {
|
||||
setIsOpen(false);
|
||||
|
||||
const selectedSeries = items.find((item) => item.tvdbId === tvdbId)!;
|
||||
const selectedSeries = data.find((item) => item.tvdbId === tvdbId)!;
|
||||
|
||||
dispatch(
|
||||
// @ts-expect-error - actions are not typed
|
||||
setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries,
|
||||
})
|
||||
);
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
selectedSeries,
|
||||
});
|
||||
|
||||
if (selectedSeries.seriesType !== 'standard') {
|
||||
onInputChange({
|
||||
@@ -116,12 +93,24 @@ function ImportSeriesSelectSeries({
|
||||
});
|
||||
}
|
||||
},
|
||||
[id, items, dispatch, onInputChange]
|
||||
[id, data, onInputChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(itemTerm);
|
||||
}, [itemTerm]);
|
||||
if (isFetched) {
|
||||
updateImportSeriesItem({
|
||||
id,
|
||||
hasSearched: isFetched,
|
||||
selectedSeries: data[0],
|
||||
});
|
||||
|
||||
removeFromLookupQueue(id);
|
||||
}
|
||||
}, [id, isFetched, data]);
|
||||
|
||||
useEffect(() => {
|
||||
setTerm(name);
|
||||
}, [name]);
|
||||
|
||||
const { refs, context, floatingStyles } = useFloating({
|
||||
middleware: [
|
||||
@@ -148,11 +137,11 @@ function ImportSeriesSelectSeries({
|
||||
<>
|
||||
<div ref={refs.setReference} {...getReferenceProps()}>
|
||||
<Link className={styles.button} component="div" onPress={handlePress}>
|
||||
{isLookingUpSeries && isQueued && !isPopulated ? (
|
||||
{isLookingUpSeries && isQueued && !isFetched ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
|
||||
{isPopulated && selectedSeries && isExistingSeries ? (
|
||||
{isFetched && selectedSeries && isExistingSeries ? (
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
@@ -160,7 +149,7 @@ function ImportSeriesSelectSeries({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isPopulated && selectedSeries ? (
|
||||
{isFetched && selectedSeries ? (
|
||||
<ImportSeriesTitle
|
||||
title={selectedSeries.title}
|
||||
year={selectedSeries.year}
|
||||
@@ -169,7 +158,7 @@ function ImportSeriesSelectSeries({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !selectedSeries ? (
|
||||
{isFetched && !selectedSeries ? (
|
||||
<div>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
@@ -199,6 +188,7 @@ function ImportSeriesSelectSeries({
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{isOpen ? (
|
||||
<FloatingPortal id="portal-root">
|
||||
<div
|
||||
@@ -233,7 +223,7 @@ function ImportSeriesSelectSeries({
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
{items.map((item) => {
|
||||
{data.map((item) => {
|
||||
return (
|
||||
<ImportSeriesSearchResult
|
||||
key={item.tvdbId}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import { useEffect } from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useAddSeriesOptions } from 'AddSeries/addSeriesOptionsStore';
|
||||
import { UnmappedFolder } from 'RootFolder/useRootFolders';
|
||||
import Series, { SeriesMonitor, SeriesType } from 'Series/Series';
|
||||
|
||||
export interface UnamppedFolderItem extends UnmappedFolder {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ImportSeriesItem {
|
||||
id: string;
|
||||
monitor: SeriesMonitor;
|
||||
path: string;
|
||||
qualityProfileId: number;
|
||||
relativePath: string;
|
||||
seasonFolder: boolean;
|
||||
selectedSeries?: Series;
|
||||
seriesType: SeriesType;
|
||||
name: string;
|
||||
hasSearched: boolean;
|
||||
}
|
||||
|
||||
interface ImportSeriesState {
|
||||
items: Record<string, ImportSeriesItem>;
|
||||
lookupQueue: string[];
|
||||
isProcessing: boolean;
|
||||
}
|
||||
|
||||
const defaultState: ImportSeriesState = {
|
||||
items: {},
|
||||
lookupQueue: [],
|
||||
isProcessing: false,
|
||||
};
|
||||
|
||||
const importSeriesStore = create<ImportSeriesState>()(() => defaultState);
|
||||
|
||||
export const useEnsureImportSeriesItems = (
|
||||
unmappedFolders: UnamppedFolderItem[]
|
||||
) => {
|
||||
const { monitor, qualityProfileId, seriesType, seasonFolder } =
|
||||
useAddSeriesOptions();
|
||||
|
||||
useEffect(() => {
|
||||
unmappedFolders.forEach((unmappedFolder) => {
|
||||
const existingItem =
|
||||
importSeriesStore.getState().items[unmappedFolder.id];
|
||||
|
||||
if (existingItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newItem: ImportSeriesItem = {
|
||||
...unmappedFolder,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
hasSearched: false,
|
||||
};
|
||||
|
||||
importSeriesStore.setState((state) => ({
|
||||
items: {
|
||||
...state.items,
|
||||
[unmappedFolder.id]: newItem,
|
||||
},
|
||||
}));
|
||||
});
|
||||
}, [unmappedFolders, monitor, qualityProfileId, seriesType, seasonFolder]);
|
||||
};
|
||||
|
||||
export const updateImportSeriesItem = (
|
||||
itemData: Partial<ImportSeriesItem> & Pick<ImportSeriesItem, 'id'>
|
||||
) => {
|
||||
console.info('\x1b[36m[MarkTest] updating item\x1b[0m', itemData);
|
||||
|
||||
importSeriesStore.setState((state) => {
|
||||
const existingItem = state.items[itemData.id];
|
||||
|
||||
if (existingItem) {
|
||||
return {
|
||||
items: {
|
||||
...state.items,
|
||||
[itemData.id]: {
|
||||
...existingItem,
|
||||
...itemData,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
};
|
||||
|
||||
export const removeImportSeriesItemByPath = (path: string) => {
|
||||
importSeriesStore.setState((state) => {
|
||||
const item = Object.values(state.items).find((i) => i.path === path);
|
||||
|
||||
if (!item) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const { [item.id]: removed, ...items } = state.items;
|
||||
|
||||
return { items };
|
||||
});
|
||||
};
|
||||
|
||||
export const clearImportSeries = () => {
|
||||
importSeriesStore.setState(defaultState);
|
||||
};
|
||||
|
||||
export const startProcessing = () => {
|
||||
importSeriesStore.setState((state) => {
|
||||
const items = Object.values(state.items).reduce<string[]>((acc, item) => {
|
||||
if (!item.hasSearched) {
|
||||
acc.push(item.id);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return { isProcessing: true, lookupQueue: items };
|
||||
});
|
||||
};
|
||||
|
||||
export const stopProcessing = () => {
|
||||
importSeriesStore.setState({ isProcessing: false, lookupQueue: [] });
|
||||
};
|
||||
|
||||
export const addToLookupQueue = (id: string) => {
|
||||
importSeriesStore.setState((state) => ({
|
||||
lookupQueue: [...state.lookupQueue, id],
|
||||
}));
|
||||
};
|
||||
|
||||
export const removeFromLookupQueue = (id: string) => {
|
||||
importSeriesStore.setState((state) => ({
|
||||
lookupQueue: state.lookupQueue.filter((queuedId) => queuedId !== id),
|
||||
}));
|
||||
};
|
||||
|
||||
export const useIsCurrentLookupQueueItem = (id: string) => {
|
||||
return importSeriesStore((state) => state.lookupQueue[0] === id);
|
||||
};
|
||||
|
||||
export const useIsCurrentedItemQueued = (id: string) => {
|
||||
return importSeriesStore((state) => state.lookupQueue.includes(id));
|
||||
};
|
||||
|
||||
export const useLookupQueueHasItems = () => {
|
||||
return importSeriesStore((state) => state.lookupQueue.length > 0);
|
||||
};
|
||||
|
||||
export const useImportSeriesItem = (id: string) => {
|
||||
return importSeriesStore((state) => state.items[id]);
|
||||
};
|
||||
|
||||
export const useImportSeriesItems = () => {
|
||||
return importSeriesStore(useShallow((state) => Object.values(state.items)));
|
||||
};
|
||||
|
||||
export const getImportSeriesItems = (ids: string[]) => {
|
||||
const state = importSeriesStore.getState();
|
||||
|
||||
return ids.reduce<ImportSeriesItem[]>((acc, id) => {
|
||||
const item = state.items[id];
|
||||
|
||||
if (item != null) {
|
||||
acc.push(item);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback } from 'react';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import Series from 'Series/Series';
|
||||
import {
|
||||
getImportSeriesItems,
|
||||
removeImportSeriesItemByPath,
|
||||
} from './importSeriesStore';
|
||||
|
||||
export const useImportSeries = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { isPending, error, mutate } = useApiMutation<Series[], Series[]>({
|
||||
path: '/series/import',
|
||||
method: 'POST',
|
||||
mutationOptions: {
|
||||
onSuccess: (data, newSeries) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/rootFolder'] });
|
||||
queryClient.setQueryData<Series[]>(['/series'], (oldSeries) => {
|
||||
if (!oldSeries) {
|
||||
return data;
|
||||
}
|
||||
|
||||
return [...oldSeries, ...data];
|
||||
});
|
||||
|
||||
newSeries.forEach((series) => {
|
||||
removeImportSeriesItemByPath(series.path);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const importSeries = useCallback(
|
||||
(ids: string[]) => {
|
||||
const items = getImportSeriesItems(ids);
|
||||
const addedIds: string[] = [];
|
||||
|
||||
const allNewSeries = ids.reduce<Series[]>((acc, id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
const selectedSeries = item?.selectedSeries;
|
||||
|
||||
// Make sure we have a selected series and the same series hasn't been added yet.
|
||||
if (
|
||||
selectedSeries &&
|
||||
!acc.some((a) => a.tvdbId === selectedSeries.tvdbId)
|
||||
) {
|
||||
const newSeries: Series = {
|
||||
...selectedSeries,
|
||||
monitored: true,
|
||||
monitorNewItems: 'all',
|
||||
qualityProfileId: item.qualityProfileId,
|
||||
path: item.path,
|
||||
seriesType: item.seriesType,
|
||||
seasonFolder: item.seasonFolder,
|
||||
addOptions: {
|
||||
monitor: item.monitor,
|
||||
searchForMissingEpisodes: false,
|
||||
searchForCutoffUnmetEpisodes: false,
|
||||
},
|
||||
tags: [],
|
||||
};
|
||||
|
||||
newSeries.path = item.path;
|
||||
|
||||
addedIds.push(id);
|
||||
acc.push(newSeries);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (allNewSeries.length > 0) {
|
||||
mutate(allNewSeries);
|
||||
}
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
isImporting: isPending,
|
||||
importError: error,
|
||||
importSeries,
|
||||
};
|
||||
};
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { ImportSeries } from 'App/State/ImportSeriesAppState';
|
||||
import useSeries from 'Series/useSeries';
|
||||
|
||||
function useImportSeriesItem(id: string) {
|
||||
const { data: series = [] } = useSeries();
|
||||
const importSeries = useSelector((state: AppState) => state.importSeries);
|
||||
|
||||
return useMemo(() => {
|
||||
const item =
|
||||
importSeries.items.find((item) => {
|
||||
return item.id === id;
|
||||
}) ?? ({} as ImportSeries);
|
||||
|
||||
const selectedSeries = item && item.selectedSeries;
|
||||
const isExistingSeries =
|
||||
!!selectedSeries &&
|
||||
series.some((s) => {
|
||||
return s.tvdbId === selectedSeries.tvdbId;
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
isExistingSeries,
|
||||
};
|
||||
}, [id, importSeries.items, series]);
|
||||
}
|
||||
|
||||
export default useImportSeriesItem;
|
||||
Reference in New Issue
Block a user