mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
import AppState, { Filter } from 'App/State/AppState';
|
|
import * as commandNames from 'Commands/commandNames';
|
|
import Alert from 'Components/Alert';
|
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
import PageContent from 'Components/Page/PageContent';
|
|
import PageContentBody from 'Components/Page/PageContentBody';
|
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
import Table from 'Components/Table/Table';
|
|
import TableBody from 'Components/Table/TableBody';
|
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
import TablePager from 'Components/Table/TablePager';
|
|
import usePaging from 'Components/Table/usePaging';
|
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
|
import useSelectState from 'Helpers/Hooks/useSelectState';
|
|
import { align, icons, kinds } from 'Helpers/Props';
|
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
|
import Movie from 'Movie/Movie';
|
|
import { executeCommand } from 'Store/Actions/commandActions';
|
|
import {
|
|
clearQueueDetails,
|
|
fetchQueueDetails,
|
|
} from 'Store/Actions/queueActions';
|
|
import {
|
|
batchToggleMissingMovies,
|
|
clearMissing,
|
|
fetchMissing,
|
|
gotoMissingPage,
|
|
setMissingFilter,
|
|
setMissingSort,
|
|
setMissingTableOption,
|
|
} from 'Store/Actions/wantedActions';
|
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
import { CheckInputChanged } from 'typings/inputs';
|
|
import { SelectStateInputProps } from 'typings/props';
|
|
import { TableOptionsChangePayload } from 'typings/Table';
|
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
import {
|
|
registerPagePopulator,
|
|
unregisterPagePopulator,
|
|
} from 'Utilities/pagePopulator';
|
|
import translate from 'Utilities/String/translate';
|
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
import MissingRow from './MissingRow';
|
|
|
|
function getMonitoredValue(
|
|
filters: Filter[],
|
|
selectedFilterKey: string
|
|
): boolean {
|
|
return !!getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
|
}
|
|
|
|
function Missing() {
|
|
const dispatch = useDispatch();
|
|
const requestCurrentPage = useCurrentPage();
|
|
|
|
const {
|
|
isFetching,
|
|
isPopulated,
|
|
error,
|
|
items,
|
|
columns,
|
|
selectedFilterKey,
|
|
filters,
|
|
sortKey,
|
|
sortDirection,
|
|
page,
|
|
pageSize,
|
|
totalPages,
|
|
totalRecords = 0,
|
|
} = useSelector((state: AppState) => state.wanted.missing);
|
|
|
|
const isSearchingForAllMovies = useSelector(
|
|
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH)
|
|
);
|
|
const isSearchingForSelectedMovies = useSelector(
|
|
createCommandExecutingSelector(commandNames.MOVIE_SEARCH)
|
|
);
|
|
|
|
const [selectState, setSelectState] = useSelectState();
|
|
const { allSelected, allUnselected, selectedState } = selectState;
|
|
|
|
const [isConfirmSearchAllModalOpen, setIsConfirmSearchAllModalOpen] =
|
|
useState(false);
|
|
|
|
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
|
|
useState(false);
|
|
|
|
const {
|
|
handleFirstPagePress,
|
|
handlePreviousPagePress,
|
|
handleNextPagePress,
|
|
handleLastPagePress,
|
|
handlePageSelect,
|
|
} = usePaging({
|
|
page,
|
|
totalPages,
|
|
gotoPage: gotoMissingPage,
|
|
});
|
|
|
|
const selectedIds = useMemo(() => {
|
|
return getSelectedIds(selectedState);
|
|
}, [selectedState]);
|
|
|
|
const isSaving = useMemo(() => {
|
|
return items.filter((m) => m.isSaving).length > 1;
|
|
}, [items]);
|
|
|
|
const itemsSelected = !!selectedIds.length;
|
|
const isShowingMonitored = getMonitoredValue(filters, selectedFilterKey);
|
|
const isSearchingForMovies =
|
|
isSearchingForAllMovies || isSearchingForSelectedMovies;
|
|
|
|
const previousItems = usePrevious(items);
|
|
|
|
const handleSelectAllChange = useCallback(
|
|
({ value }: CheckInputChanged) => {
|
|
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
|
},
|
|
[items, setSelectState]
|
|
);
|
|
|
|
const handleSelectedChange = useCallback(
|
|
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
|
setSelectState({
|
|
type: 'toggleSelected',
|
|
items,
|
|
id,
|
|
isSelected: value,
|
|
shiftKey,
|
|
});
|
|
},
|
|
[items, setSelectState]
|
|
);
|
|
|
|
const handleSearchSelectedPress = useCallback(() => {
|
|
dispatch(
|
|
executeCommand({
|
|
name: commandNames.MOVIE_SEARCH,
|
|
movieIds: selectedIds,
|
|
commandFinished: () => {
|
|
dispatch(fetchMissing());
|
|
},
|
|
})
|
|
);
|
|
}, [selectedIds, dispatch]);
|
|
|
|
const handleSearchAllPress = useCallback(() => {
|
|
setIsConfirmSearchAllModalOpen(true);
|
|
}, []);
|
|
|
|
const handleConfirmSearchAllMissingModalClose = useCallback(() => {
|
|
setIsConfirmSearchAllModalOpen(false);
|
|
}, []);
|
|
|
|
const handleSearchAllMissingConfirmed = useCallback(() => {
|
|
dispatch(
|
|
executeCommand({
|
|
name: commandNames.MISSING_MOVIES_SEARCH,
|
|
commandFinished: () => {
|
|
dispatch(fetchMissing());
|
|
},
|
|
})
|
|
);
|
|
|
|
setIsConfirmSearchAllModalOpen(false);
|
|
}, [dispatch]);
|
|
|
|
const handleToggleSelectedPress = useCallback(() => {
|
|
dispatch(
|
|
batchToggleMissingMovies({
|
|
movieIds: selectedIds,
|
|
monitored: !isShowingMonitored,
|
|
})
|
|
);
|
|
}, [isShowingMonitored, selectedIds, dispatch]);
|
|
|
|
const handleInteractiveImportPress = useCallback(() => {
|
|
setIsInteractiveImportModalOpen(true);
|
|
}, []);
|
|
|
|
const handleInteractiveImportModalClose = useCallback(() => {
|
|
setIsInteractiveImportModalOpen(false);
|
|
}, []);
|
|
|
|
const handleFilterSelect = useCallback(
|
|
(filterKey: number | string) => {
|
|
dispatch(setMissingFilter({ selectedFilterKey: filterKey }));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleSortPress = useCallback(
|
|
(sortKey: string) => {
|
|
dispatch(setMissingSort({ sortKey }));
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
const handleTableOptionChange = useCallback(
|
|
(payload: TableOptionsChangePayload) => {
|
|
dispatch(setMissingTableOption(payload));
|
|
|
|
if (payload.pageSize) {
|
|
dispatch(gotoMissingPage({ page: 1 }));
|
|
}
|
|
},
|
|
[dispatch]
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (requestCurrentPage) {
|
|
dispatch(fetchMissing());
|
|
} else {
|
|
dispatch(gotoMissingPage({ page: 1 }));
|
|
}
|
|
|
|
return () => {
|
|
dispatch(clearMissing());
|
|
dispatch(clearQueueDetails());
|
|
};
|
|
}, [requestCurrentPage, dispatch]);
|
|
|
|
useEffect(() => {
|
|
const repopulate = () => {
|
|
dispatch(fetchMissing());
|
|
};
|
|
|
|
registerPagePopulator(repopulate, [
|
|
'movieUpdated',
|
|
'movieFileUpdated',
|
|
'movieFileDeleted',
|
|
]);
|
|
|
|
return () => {
|
|
unregisterPagePopulator(repopulate);
|
|
};
|
|
}, [dispatch]);
|
|
|
|
useEffect(() => {
|
|
if (!previousItems || hasDifferentItems(items, previousItems)) {
|
|
const movieIds = selectUniqueIds<Movie, number>(items, 'id');
|
|
|
|
if (movieIds.length) {
|
|
dispatch(fetchQueueDetails({ movieIds }));
|
|
}
|
|
}
|
|
}, [items, previousItems, dispatch]);
|
|
|
|
return (
|
|
<PageContent title={translate('Missing')}>
|
|
<PageToolbar>
|
|
<PageToolbarSection>
|
|
<PageToolbarButton
|
|
label={
|
|
itemsSelected
|
|
? translate('SearchSelected')
|
|
: translate('SearchAll')
|
|
}
|
|
iconName={icons.SEARCH}
|
|
isDisabled={isSearchingForMovies}
|
|
isSpinning={isSearchingForMovies}
|
|
onPress={
|
|
itemsSelected ? handleSearchSelectedPress : handleSearchAllPress
|
|
}
|
|
/>
|
|
|
|
<PageToolbarSeparator />
|
|
|
|
<PageToolbarButton
|
|
label={
|
|
isShowingMonitored
|
|
? translate('UnmonitorSelected')
|
|
: translate('MonitorSelected')
|
|
}
|
|
iconName={icons.MONITORED}
|
|
isDisabled={!itemsSelected}
|
|
isSpinning={isSaving}
|
|
onPress={handleToggleSelectedPress}
|
|
/>
|
|
|
|
<PageToolbarSeparator />
|
|
|
|
<PageToolbarButton
|
|
label={translate('ManualImport')}
|
|
iconName={icons.INTERACTIVE}
|
|
onPress={handleInteractiveImportPress}
|
|
/>
|
|
</PageToolbarSection>
|
|
|
|
<PageToolbarSection alignContent={align.RIGHT}>
|
|
<TableOptionsModalWrapper
|
|
columns={columns}
|
|
pageSize={pageSize}
|
|
onTableOptionChange={handleTableOptionChange}
|
|
>
|
|
<PageToolbarButton
|
|
label={translate('Options')}
|
|
iconName={icons.TABLE}
|
|
/>
|
|
</TableOptionsModalWrapper>
|
|
|
|
<FilterMenu
|
|
alignMenu={align.RIGHT}
|
|
selectedFilterKey={selectedFilterKey}
|
|
filters={filters}
|
|
customFilters={[]}
|
|
onFilterSelect={handleFilterSelect}
|
|
/>
|
|
</PageToolbarSection>
|
|
</PageToolbar>
|
|
|
|
<PageContentBody>
|
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
|
|
|
{!isFetching && error ? (
|
|
<Alert kind={kinds.DANGER}>{translate('MissingLoadError')}</Alert>
|
|
) : null}
|
|
|
|
{isPopulated && !error && !items.length ? (
|
|
<Alert kind={kinds.INFO}>{translate('MissingNoItems')}</Alert>
|
|
) : null}
|
|
|
|
{isPopulated && !error && !!items.length ? (
|
|
<div>
|
|
<Table
|
|
selectAll={true}
|
|
allSelected={allSelected}
|
|
allUnselected={allUnselected}
|
|
columns={columns}
|
|
pageSize={pageSize}
|
|
sortKey={sortKey}
|
|
sortDirection={sortDirection}
|
|
onTableOptionChange={handleTableOptionChange}
|
|
onSelectAllChange={handleSelectAllChange}
|
|
onSortPress={handleSortPress}
|
|
>
|
|
<TableBody>
|
|
{items.map((item) => {
|
|
return (
|
|
<MissingRow
|
|
key={item.id}
|
|
isSelected={selectedState[item.id]}
|
|
columns={columns}
|
|
{...item}
|
|
onSelectedChange={handleSelectedChange}
|
|
/>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
<TablePager
|
|
page={page}
|
|
totalPages={totalPages}
|
|
totalRecords={totalRecords}
|
|
isFetching={isFetching}
|
|
onFirstPagePress={handleFirstPagePress}
|
|
onPreviousPagePress={handlePreviousPagePress}
|
|
onNextPagePress={handleNextPagePress}
|
|
onLastPagePress={handleLastPagePress}
|
|
onPageSelect={handlePageSelect}
|
|
/>
|
|
|
|
<ConfirmModal
|
|
isOpen={isConfirmSearchAllModalOpen}
|
|
kind={kinds.DANGER}
|
|
title={translate('SearchForAllMissingMovies')}
|
|
message={
|
|
<div>
|
|
<div>
|
|
{translate('SearchForAllMissingMoviesConfirmationCount', {
|
|
totalRecords,
|
|
})}
|
|
</div>
|
|
<div>{translate('MassSearchCancelWarning')}</div>
|
|
</div>
|
|
}
|
|
confirmLabel={translate('Search')}
|
|
onConfirm={handleSearchAllMissingConfirmed}
|
|
onCancel={handleConfirmSearchAllMissingModalClose}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</PageContentBody>
|
|
|
|
<InteractiveImportModal
|
|
isOpen={isInteractiveImportModalOpen}
|
|
onModalClose={handleInteractiveImportModalClose}
|
|
/>
|
|
</PageContent>
|
|
);
|
|
}
|
|
|
|
export default Missing;
|