mirror of
https://github.com/Sonarr/Sonarr
synced 2025-12-06 08:28:37 +01:00
Use react-query for episodes
This commit is contained in:
parent
a97f2c016b
commit
1178c98341
21 changed files with 400 additions and 566 deletions
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
|
@ -12,12 +11,10 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
import useEpisodes from 'Episode/useEpisodes';
|
||||||
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
||||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import HistoryItem from 'typings/History';
|
import HistoryItem from 'typings/History';
|
||||||
import { TableOptionsChangePayload } from 'typings/Table';
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
|
@ -53,17 +50,22 @@ function History() {
|
||||||
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
|
||||||
useHistoryOptions();
|
useHistoryOptions();
|
||||||
|
|
||||||
|
const episodeIds = useMemo(() => {
|
||||||
|
return selectUniqueIds<HistoryItem, number>(records, 'episodeId');
|
||||||
|
}, [records]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching: isEpisodesFetching,
|
||||||
|
isFetched: isEpisodesFetched,
|
||||||
|
error: episodesError,
|
||||||
|
} = useEpisodes({ episodeIds });
|
||||||
|
|
||||||
const filters = useFilters();
|
const filters = useFilters();
|
||||||
|
|
||||||
const requestCurrentPage = useCurrentPage();
|
|
||||||
|
|
||||||
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
|
||||||
useSelector(createEpisodesFetchingSelector());
|
|
||||||
const customFilters = useCustomFiltersList('history');
|
const customFilters = useCustomFiltersList('history');
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const isFetchingAny = isLoading || isEpisodesFetching;
|
const isFetchingAny = isLoading || isEpisodesFetching;
|
||||||
const isAllPopulated = isFetched && (isEpisodesPopulated || !records.length);
|
const isAllPopulated = isFetched && (isEpisodesFetched || !records.length);
|
||||||
const hasError = error || episodesError;
|
const hasError = error || episodesError;
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
|
|
@ -99,25 +101,6 @@ function History() {
|
||||||
refetch();
|
refetch();
|
||||||
}, [goToPage, refetch]);
|
}, [goToPage, refetch]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
dispatch(clearEpisodes());
|
|
||||||
};
|
|
||||||
}, [requestCurrentPage, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const episodeIds = selectUniqueIds<HistoryItem, number>(
|
|
||||||
records,
|
|
||||||
'episodeId'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (episodeIds.length) {
|
|
||||||
dispatch(fetchEpisodes({ episodeIds }));
|
|
||||||
} else {
|
|
||||||
dispatch(clearEpisodes());
|
|
||||||
}
|
|
||||||
}, [records, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const repopulate = () => {
|
const repopulate = () => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,11 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelector';
|
import useEpisodes from 'Episode/useEpisodes';
|
||||||
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
import { useCustomFiltersList } from 'Filters/useCustomFilters';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { CheckInputChanged } from 'typings/inputs';
|
||||||
import QueueModel from 'typings/Queue';
|
import QueueModel from 'typings/Queue';
|
||||||
|
|
@ -79,8 +78,17 @@ function QueueContent() {
|
||||||
const { isGrabbing, grabQueueItems } = useGrabQueueItems();
|
const { isGrabbing, grabQueueItems } = useGrabQueueItems();
|
||||||
|
|
||||||
const { count } = useQueueStatus();
|
const { count } = useQueueStatus();
|
||||||
const { isEpisodesFetching, isEpisodesPopulated, episodesError } =
|
|
||||||
useSelector(createEpisodesFetchingSelector());
|
const episodeIds = useMemo(() => {
|
||||||
|
return selectUniqueIds<QueueModel, number>(records, 'episodeIds');
|
||||||
|
}, [records]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching: isEpisodesFetching,
|
||||||
|
isFetched: isEpisodesFetched,
|
||||||
|
error: episodesError,
|
||||||
|
} = useEpisodes({ episodeIds });
|
||||||
|
|
||||||
const customFilters = useCustomFiltersList('queue');
|
const customFilters = useCustomFiltersList('queue');
|
||||||
|
|
||||||
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
const isRefreshMonitoredDownloadsExecuting = useSelector(
|
||||||
|
|
@ -109,7 +117,7 @@ function QueueContent() {
|
||||||
// Use isLoading over isFetched to avoid losing the table UI when switching pages
|
// Use isLoading over isFetched to avoid losing the table UI when switching pages
|
||||||
const isAllPopulated =
|
const isAllPopulated =
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
(isEpisodesPopulated ||
|
(isEpisodesFetched ||
|
||||||
!records.length ||
|
!records.length ||
|
||||||
records.every((e) => !e.episodeIds?.length));
|
records.every((e) => !e.episodeIds?.length));
|
||||||
const hasError = error || episodesError;
|
const hasError = error || episodesError;
|
||||||
|
|
@ -187,16 +195,6 @@ function QueueContent() {
|
||||||
[goToPage]
|
[goToPage]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const episodeIds = selectUniqueIds(records, 'episodeIds');
|
|
||||||
|
|
||||||
if (episodeIds.length) {
|
|
||||||
dispatch(fetchEpisodes({ episodeIds }));
|
|
||||||
} else {
|
|
||||||
dispatch(clearEpisodes());
|
|
||||||
}
|
|
||||||
}, [records, dispatch]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const repopulate = () => {
|
const repopulate = () => {
|
||||||
refetch();
|
refetch();
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
import EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
import useEpisodesWithIds from 'Episode/useEpisodesWithIds';
|
import { useEpisodesWithIds } from 'Episode/useEpisode';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
|
|
@ -7,14 +7,13 @@ import { Store } from 'redux';
|
||||||
import Page from 'Components/Page/Page';
|
import Page from 'Components/Page/Page';
|
||||||
import ApplyTheme from './ApplyTheme';
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
import { queryClient } from './queryClient';
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
store: Store;
|
store: Store;
|
||||||
history: ConnectedRouterProps['history'];
|
history: ConnectedRouterProps['history'];
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
function App({ store, history }: AppProps) {
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Sonarr.instanceName}>
|
<DocumentTitle title={window.Sonarr.instanceName}>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { Error } from './AppSectionState';
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CaptchaAppState from './CaptchaAppState';
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import EpisodesAppState from './EpisodesAppState';
|
|
||||||
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
||||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
|
|
@ -41,7 +40,6 @@ interface AppState {
|
||||||
captcha: CaptchaAppState;
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
episodeHistory: HistoryAppState;
|
episodeHistory: HistoryAppState;
|
||||||
episodes: EpisodesAppState;
|
|
||||||
importSeries: ImportSeriesAppState;
|
importSeries: ImportSeriesAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
oAuth: OAuthAppState;
|
oAuth: OAuthAppState;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import AppSectionState from 'App/State/AppSectionState';
|
|
||||||
import Column from 'Components/Table/Column';
|
|
||||||
import Episode from 'Episode/Episode';
|
|
||||||
|
|
||||||
interface EpisodesAppState extends AppSectionState<Episode> {
|
|
||||||
columns: Column[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EpisodesAppState;
|
|
||||||
3
frontend/src/App/queryClient.ts
Normal file
3
frontend/src/App/queryClient.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient();
|
||||||
|
|
@ -150,13 +150,37 @@ function SignalRListener() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'episode') {
|
if (name === 'episode') {
|
||||||
|
if (version < 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (body.action === 'updated') {
|
if (body.action === 'updated') {
|
||||||
dispatch(
|
const updatedItem = body.resource as Episode;
|
||||||
updateItem({
|
|
||||||
section: 'episodes',
|
queryClient.setQueriesData(
|
||||||
updateOnly: true,
|
{ queryKey: ['/episode'] },
|
||||||
...body.resource,
|
(oldData: Episode[] | undefined) => {
|
||||||
})
|
if (!oldData) {
|
||||||
|
return oldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemIndex = oldData.findIndex(
|
||||||
|
(item) => item.id === updatedItem.id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Don't add episode if not found
|
||||||
|
if (itemIndex === -1) {
|
||||||
|
return oldData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldData.map((item) => {
|
||||||
|
if (item.id === updatedItem.id) {
|
||||||
|
return updatedItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
|
@ -10,10 +9,13 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import Episode from 'Episode/Episode';
|
import Episode from 'Episode/Episode';
|
||||||
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
|
import EpisodeDetailsTab from 'Episode/EpisodeDetailsTab';
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
import episodeEntities from 'Episode/episodeEntities';
|
||||||
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
|
import useEpisode, {
|
||||||
|
EpisodeEntity,
|
||||||
|
getQueryKey,
|
||||||
|
useToggleEpisodesMonitored,
|
||||||
|
} from 'Episode/useEpisode';
|
||||||
import Series from 'Series/Series';
|
import Series from 'Series/Series';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import EpisodeHistory from './History/EpisodeHistory';
|
import EpisodeHistory from './History/EpisodeHistory';
|
||||||
import EpisodeSearch from './Search/EpisodeSearch';
|
import EpisodeSearch from './Search/EpisodeSearch';
|
||||||
|
|
@ -28,7 +30,6 @@ export interface EpisodeDetailsModalContentProps {
|
||||||
episodeEntity: EpisodeEntity;
|
episodeEntity: EpisodeEntity;
|
||||||
seriesId: number;
|
seriesId: number;
|
||||||
episodeTitle: string;
|
episodeTitle: string;
|
||||||
isSaving?: boolean;
|
|
||||||
showOpenSeriesButton?: boolean;
|
showOpenSeriesButton?: boolean;
|
||||||
selectedTab?: EpisodeDetailsTab;
|
selectedTab?: EpisodeDetailsTab;
|
||||||
startInteractiveSearch?: boolean;
|
startInteractiveSearch?: boolean;
|
||||||
|
|
@ -36,22 +37,17 @@ export interface EpisodeDetailsModalContentProps {
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
function EpisodeDetailsModalContent({
|
||||||
const {
|
episodeId,
|
||||||
episodeId,
|
episodeEntity = episodeEntities.EPISODES,
|
||||||
episodeEntity = episodeEntities.EPISODES,
|
seriesId,
|
||||||
seriesId,
|
episodeTitle,
|
||||||
episodeTitle,
|
showOpenSeriesButton = false,
|
||||||
isSaving = false,
|
startInteractiveSearch = false,
|
||||||
showOpenSeriesButton = false,
|
selectedTab = 'details',
|
||||||
startInteractiveSearch = false,
|
onTabChange,
|
||||||
selectedTab = 'details',
|
onModalClose,
|
||||||
onTabChange,
|
}: EpisodeDetailsModalContentProps) {
|
||||||
onModalClose,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [currentlySelectedTab, setCurrentlySelectedTab] = useState(selectedTab);
|
const [currentlySelectedTab, setCurrentlySelectedTab] = useState(selectedTab);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -70,6 +66,10 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||||
monitored,
|
monitored,
|
||||||
} = useEpisode(episodeId, episodeEntity) as Episode;
|
} = useEpisode(episodeId, episodeEntity) as Episode;
|
||||||
|
|
||||||
|
const { toggleEpisodesMonitored, isToggling } = useToggleEpisodesMonitored(
|
||||||
|
getQueryKey(episodeEntity)!
|
||||||
|
);
|
||||||
|
|
||||||
const handleTabSelect = useCallback(
|
const handleTabSelect = useCallback(
|
||||||
(selectedIndex: number) => {
|
(selectedIndex: number) => {
|
||||||
const tab = TABS[selectedIndex];
|
const tab = TABS[selectedIndex];
|
||||||
|
|
@ -81,15 +81,12 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||||
|
|
||||||
const handleMonitorEpisodePress = useCallback(
|
const handleMonitorEpisodePress = useCallback(
|
||||||
(monitored: boolean) => {
|
(monitored: boolean) => {
|
||||||
dispatch(
|
toggleEpisodesMonitored({
|
||||||
toggleEpisodeMonitored({
|
episodeIds: [episodeId],
|
||||||
episodeEntity,
|
monitored,
|
||||||
episodeId,
|
});
|
||||||
monitored,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[episodeEntity, episodeId, dispatch]
|
[episodeId, toggleEpisodesMonitored]
|
||||||
);
|
);
|
||||||
|
|
||||||
const seriesLink = `/series/${titleSlug}`;
|
const seriesLink = `/series/${titleSlug}`;
|
||||||
|
|
@ -101,7 +98,7 @@ function EpisodeDetailsModalContent(props: EpisodeDetailsModalContentProps) {
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
size={18}
|
size={18}
|
||||||
isDisabled={!seriesMonitored}
|
isDisabled={!seriesMonitored}
|
||||||
isSaving={isSaving}
|
isSaving={isToggling}
|
||||||
onPress={handleMonitorEpisodePress}
|
onPress={handleMonitorEpisodePress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
|
|
||||||
function createEpisodesFetchingSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.episodes,
|
|
||||||
(episodes) => {
|
|
||||||
return {
|
|
||||||
isEpisodesFetching: episodes.isFetching,
|
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
|
||||||
episodesError: episodes.error,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default createEpisodesFetchingSelector;
|
|
||||||
149
frontend/src/Episode/episodeOptionsStore.ts
Normal file
149
frontend/src/Episode/episodeOptionsStore.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { createElement } from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import { createOptionsStore } from 'Helpers/Hooks/useOptionsStore';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface EpisodeSelectOptions {
|
||||||
|
sortKey: string;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { useOptions, useOption, setOptions, setOption, setSort } =
|
||||||
|
createOptionsStore<EpisodeSelectOptions>('episode_options', () => {
|
||||||
|
return {
|
||||||
|
sortKey: 'episodeNumber',
|
||||||
|
sortDirection: 'descending',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'monitored',
|
||||||
|
label: '',
|
||||||
|
columnLabel: () => translate('Monitored'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'episodeNumber',
|
||||||
|
label: '#',
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'title',
|
||||||
|
label: () => translate('Title'),
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'path',
|
||||||
|
label: () => translate('Path'),
|
||||||
|
isVisible: false,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'relativePath',
|
||||||
|
label: () => translate('RelativePath'),
|
||||||
|
isVisible: false,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'airDateUtc',
|
||||||
|
label: () => translate('AirDate'),
|
||||||
|
isVisible: true,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'runtime',
|
||||||
|
label: () => translate('Runtime'),
|
||||||
|
isVisible: false,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'audioInfo',
|
||||||
|
label: () => translate('AudioInfo'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'videoCodec',
|
||||||
|
label: () => translate('VideoCodec'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'videoDynamicRangeType',
|
||||||
|
label: () => translate('VideoDynamicRange'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'audioLanguages',
|
||||||
|
label: () => translate('AudioLanguages'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'subtitleLanguages',
|
||||||
|
label: () => translate('SubtitleLanguages'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'size',
|
||||||
|
label: () => translate('Size'),
|
||||||
|
isVisible: false,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'releaseGroup',
|
||||||
|
label: () => translate('ReleaseGroup'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormats',
|
||||||
|
label: () => translate('Formats'),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'customFormatScore',
|
||||||
|
columnLabel: () => translate('CustomFormatScore'),
|
||||||
|
label: createElement(Icon, {
|
||||||
|
name: icons.SCORE,
|
||||||
|
title: () => translate('CustomFormatScore'),
|
||||||
|
}),
|
||||||
|
isVisible: false,
|
||||||
|
isSortable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerFlags',
|
||||||
|
columnLabel: () => translate('IndexerFlags'),
|
||||||
|
label: createElement(Icon, {
|
||||||
|
name: icons.FLAG,
|
||||||
|
title: () => translate('IndexerFlags'),
|
||||||
|
}),
|
||||||
|
isVisible: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useEpisodeOptions = useOptions;
|
||||||
|
export const setEpisodeOptions = setOptions;
|
||||||
|
export const useEpisodeOption = useOption;
|
||||||
|
export const setEpisodeOption = setOption;
|
||||||
|
export const setEpisodeSort = setSort;
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||||
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
||||||
import { CalendarItem } from 'typings/Calendar';
|
import { CalendarItem } from 'typings/Calendar';
|
||||||
|
|
@ -17,39 +14,24 @@ export type EpisodeEntity =
|
||||||
|
|
||||||
interface EpisodeQueryKeyStore {
|
interface EpisodeQueryKeyStore {
|
||||||
calendar: QueryKey | null;
|
calendar: QueryKey | null;
|
||||||
|
episodes: QueryKey | null;
|
||||||
cutoffUnmet: QueryKey | null;
|
cutoffUnmet: QueryKey | null;
|
||||||
missing: QueryKey | null;
|
missing: QueryKey | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const episodeQueryKeyStore = create<EpisodeQueryKeyStore>(() => ({
|
const episodeQueryKeyStore = create<EpisodeQueryKeyStore>(() => ({
|
||||||
calendar: null,
|
calendar: null,
|
||||||
|
episodes: null,
|
||||||
cutoffUnmet: null,
|
cutoffUnmet: null,
|
||||||
missing: null,
|
missing: null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function createEpisodeSelector(episodeId?: number) {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.episodes.items,
|
|
||||||
(episodes) => {
|
|
||||||
return episodes.find(({ id }) => id === episodeId);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No-op...ish
|
|
||||||
function createNoOpEpisodeSelector(_episodeId?: number) {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.episodes.items,
|
|
||||||
(_episodes) => {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getQueryKey = (episodeEntity: EpisodeEntity) => {
|
export const getQueryKey = (episodeEntity: EpisodeEntity) => {
|
||||||
switch (episodeEntity) {
|
switch (episodeEntity) {
|
||||||
case 'calendar':
|
case 'calendar':
|
||||||
return episodeQueryKeyStore.getState().calendar;
|
return episodeQueryKeyStore.getState().calendar;
|
||||||
|
case 'episodes':
|
||||||
|
return episodeQueryKeyStore.getState().episodes;
|
||||||
case 'wanted.cutoffUnmet':
|
case 'wanted.cutoffUnmet':
|
||||||
return episodeQueryKeyStore.getState().cutoffUnmet;
|
return episodeQueryKeyStore.getState().cutoffUnmet;
|
||||||
case 'wanted.missing':
|
case 'wanted.missing':
|
||||||
|
|
@ -67,6 +49,9 @@ export const setEpisodeQueryKey = (
|
||||||
case 'calendar':
|
case 'calendar':
|
||||||
episodeQueryKeyStore.setState({ calendar: queryKey });
|
episodeQueryKeyStore.setState({ calendar: queryKey });
|
||||||
break;
|
break;
|
||||||
|
case 'episodes':
|
||||||
|
episodeQueryKeyStore.setState({ episodes: queryKey });
|
||||||
|
break;
|
||||||
case 'wanted.cutoffUnmet':
|
case 'wanted.cutoffUnmet':
|
||||||
episodeQueryKeyStore.setState({ cutoffUnmet: queryKey });
|
episodeQueryKeyStore.setState({ cutoffUnmet: queryKey });
|
||||||
break;
|
break;
|
||||||
|
|
@ -83,19 +68,6 @@ const useEpisode = (
|
||||||
episodeEntity: EpisodeEntity
|
episodeEntity: EpisodeEntity
|
||||||
) => {
|
) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
let selector = createEpisodeSelector;
|
|
||||||
|
|
||||||
switch (episodeEntity) {
|
|
||||||
case 'calendar':
|
|
||||||
case 'wanted.cutoffUnmet':
|
|
||||||
case 'wanted.missing':
|
|
||||||
selector = createNoOpEpisodeSelector;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = useSelector(selector(episodeId));
|
|
||||||
const queryKey = getQueryKey(episodeEntity);
|
const queryKey = getQueryKey(episodeEntity);
|
||||||
|
|
||||||
if (episodeEntity === 'calendar') {
|
if (episodeEntity === 'calendar') {
|
||||||
|
|
@ -104,7 +76,17 @@ const useEpisode = (
|
||||||
.getQueryData<CalendarItem[]>(queryKey)
|
.getQueryData<CalendarItem[]>(queryKey)
|
||||||
?.find((e) => e.id === episodeId)
|
?.find((e) => e.id === episodeId)
|
||||||
: undefined;
|
: undefined;
|
||||||
} else if (
|
}
|
||||||
|
|
||||||
|
if (episodeEntity === 'episodes') {
|
||||||
|
return queryKey
|
||||||
|
? queryClient
|
||||||
|
.getQueryData<Episode[]>(queryKey)
|
||||||
|
?.find((e) => e.id === episodeId)
|
||||||
|
: undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
episodeEntity === 'wanted.cutoffUnmet' ||
|
episodeEntity === 'wanted.cutoffUnmet' ||
|
||||||
episodeEntity === 'wanted.missing'
|
episodeEntity === 'wanted.missing'
|
||||||
) {
|
) {
|
||||||
|
|
@ -115,7 +97,7 @@ const useEpisode = (
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useEpisode;
|
export default useEpisode;
|
||||||
|
|
@ -128,15 +110,33 @@ interface ToggleEpisodesMonitored {
|
||||||
export const useToggleEpisodesMonitored = (queryKey: QueryKey) => {
|
export const useToggleEpisodesMonitored = (queryKey: QueryKey) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const { mutate, isPending } = useApiMutation<
|
const { mutate, isPending, variables } = useApiMutation<
|
||||||
unknown,
|
unknown,
|
||||||
ToggleEpisodesMonitored
|
ToggleEpisodesMonitored
|
||||||
>({
|
>({
|
||||||
path: '/episode/monitor',
|
path: '/episode/monitor',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
mutationOptions: {
|
mutationOptions: {
|
||||||
onSuccess: () => {
|
onSuccess: (_data, variables) => {
|
||||||
queryClient.invalidateQueries({ queryKey });
|
queryClient.setQueryData<Episode[] | undefined>(
|
||||||
|
queryKey,
|
||||||
|
(oldEpisodes) => {
|
||||||
|
if (!oldEpisodes) {
|
||||||
|
return oldEpisodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldEpisodes.map((oldEpisode) => {
|
||||||
|
if (variables.episodeIds.includes(oldEpisode.id)) {
|
||||||
|
return {
|
||||||
|
...oldEpisode,
|
||||||
|
monitored: variables.monitored,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldEpisode;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -144,5 +144,20 @@ export const useToggleEpisodesMonitored = (queryKey: QueryKey) => {
|
||||||
return {
|
return {
|
||||||
toggleEpisodesMonitored: mutate,
|
toggleEpisodesMonitored: mutate,
|
||||||
isToggling: isPending,
|
isToggling: isPending,
|
||||||
|
togglingEpisodeIds: variables?.episodeIds ?? [],
|
||||||
|
togglingMonitored: variables?.monitored,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DEFAULT_EPISODES: Episode[] = [];
|
||||||
|
|
||||||
|
export const useEpisodesWithIds = (episodeIds: number[]) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const queryKey = getQueryKey('episodes');
|
||||||
|
|
||||||
|
return queryKey
|
||||||
|
? queryClient
|
||||||
|
.getQueryData<Episode[]>(queryKey)
|
||||||
|
?.filter((e) => episodeIds.includes(e.id)) ?? DEFAULT_EPISODES
|
||||||
|
: DEFAULT_EPISODES;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||||
|
import clientSideFilterAndSort from 'Utilities/Filter/clientSideFilterAndSort';
|
||||||
import Episode from './Episode';
|
import Episode from './Episode';
|
||||||
|
import { useEpisodeOptions } from './episodeOptionsStore';
|
||||||
|
import { setEpisodeQueryKey } from './useEpisode';
|
||||||
|
|
||||||
const DEFAULT_EPISODES: Episode[] = [];
|
const DEFAULT_EPISODES: Episode[] = [];
|
||||||
|
|
||||||
|
|
@ -10,6 +14,7 @@ interface SeriesEpisodes {
|
||||||
interface SeasonEpisodes {
|
interface SeasonEpisodes {
|
||||||
seriesId: number | undefined;
|
seriesId: number | undefined;
|
||||||
seasonNumber: number | undefined;
|
seasonNumber: number | undefined;
|
||||||
|
isSelection: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EpisodeIds {
|
interface EpisodeIds {
|
||||||
|
|
@ -27,9 +32,17 @@ export type EpisodeFilter =
|
||||||
| EpisodeFileId;
|
| EpisodeFileId;
|
||||||
|
|
||||||
const useEpisodes = (params: EpisodeFilter) => {
|
const useEpisodes = (params: EpisodeFilter) => {
|
||||||
const result = useApiQuery<Episode[]>({
|
const setQueryKey = !('isSelection' in params);
|
||||||
|
|
||||||
|
const { isPlaceholderData, queryKey, ...result } = useApiQuery<Episode[]>({
|
||||||
path: '/episode',
|
path: '/episode',
|
||||||
queryParams: { ...params },
|
queryParams:
|
||||||
|
'isSelection' in params
|
||||||
|
? {
|
||||||
|
seriesId: params.seriesId,
|
||||||
|
seasonNumber: params.seasonNumber,
|
||||||
|
}
|
||||||
|
: { ...params },
|
||||||
queryOptions: {
|
queryOptions: {
|
||||||
enabled:
|
enabled:
|
||||||
('seriesId' in params && params.seriesId !== undefined) ||
|
('seriesId' in params && params.seriesId !== undefined) ||
|
||||||
|
|
@ -38,10 +51,39 @@ const useEpisodes = (params: EpisodeFilter) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setQueryKey && !isPlaceholderData) {
|
||||||
|
setEpisodeQueryKey('episodes', queryKey);
|
||||||
|
}
|
||||||
|
}, [setQueryKey, isPlaceholderData, queryKey]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...result,
|
...result,
|
||||||
|
queryKey,
|
||||||
data: result.data ?? DEFAULT_EPISODES,
|
data: result.data ?? DEFAULT_EPISODES,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useEpisodes;
|
export default useEpisodes;
|
||||||
|
|
||||||
|
export const useSeasonEpisodes = (seriesId: number, seasonNumber: number) => {
|
||||||
|
const { data, ...result } = useEpisodes({ seriesId });
|
||||||
|
const { sortKey, sortDirection } = useEpisodeOptions();
|
||||||
|
|
||||||
|
const seasonEpisodes = useMemo(() => {
|
||||||
|
const { data: seasonEpisodes } = clientSideFilterAndSort(
|
||||||
|
data.filter((episode) => episode.seasonNumber === seasonNumber),
|
||||||
|
{
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return seasonEpisodes;
|
||||||
|
}, [data, seasonNumber, sortKey, sortDirection]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
data: seasonEpisodes,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import Episode from './Episode';
|
|
||||||
|
|
||||||
function getEpisodes(episodeIds: number[], episodes: Episode[]) {
|
|
||||||
return episodeIds.reduce<Episode[]>((acc, id) => {
|
|
||||||
const episode = episodes.find((episode) => episode.id === id);
|
|
||||||
|
|
||||||
if (episode) {
|
|
||||||
acc.push(episode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createEpisodeSelector(episodeIds: number[]) {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.episodes.items,
|
|
||||||
(episodes) => {
|
|
||||||
return getEpisodes(episodeIds, episodes);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function useEpisodesWithIds(episodeIds: number[]) {
|
|
||||||
return useSelector(createEpisodeSelector(episodeIds));
|
|
||||||
}
|
|
||||||
|
|
@ -77,6 +77,7 @@ function SelectEpisodeModalContentInner(props: SelectEpisodeModalContentProps) {
|
||||||
const { isFetching, isFetched, data, error } = useEpisodes({
|
const { isFetching, isFetched, data, error } = useEpisodes({
|
||||||
seriesId,
|
seriesId,
|
||||||
seasonNumber,
|
seasonNumber,
|
||||||
|
isSelection: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { sortKey, sortDirection } = useEpisodeSelectionOptions();
|
const { sortKey, sortDirection } = useEpisodeSelectionOptions();
|
||||||
|
|
@ -255,6 +256,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
||||||
const { data } = useEpisodes({
|
const { data } = useEpisodes({
|
||||||
seriesId: props.seriesId,
|
seriesId: props.seriesId,
|
||||||
seasonNumber: props.seasonNumber,
|
seasonNumber: props.seasonNumber,
|
||||||
|
isSelection: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import AppState from 'App/State/AppState';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
|
|
@ -20,6 +18,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import useEpisodes from 'Episode/useEpisodes';
|
||||||
import useEpisodeFiles from 'EpisodeFile/useEpisodeFiles';
|
import useEpisodeFiles from 'EpisodeFile/useEpisodeFiles';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import {
|
import {
|
||||||
|
|
@ -43,7 +42,6 @@ import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
|
||||||
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
|
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
|
@ -75,26 +73,6 @@ function getDateYear(date: string | undefined) {
|
||||||
return dateDate.format('YYYY');
|
return dateDate.format('YYYY');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEpisodesSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state: AppState) => state.episodes,
|
|
||||||
(episodes) => {
|
|
||||||
const { items, isFetching, isPopulated, error } = episodes;
|
|
||||||
|
|
||||||
const hasEpisodes = !!items.length;
|
|
||||||
const hasMonitoredEpisodes = items.some((e) => e.monitored);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isEpisodesFetching: isFetching,
|
|
||||||
isEpisodesPopulated: isPopulated,
|
|
||||||
episodesError: error,
|
|
||||||
hasEpisodes,
|
|
||||||
hasMonitoredEpisodes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExpandedState {
|
interface ExpandedState {
|
||||||
allExpanded: boolean;
|
allExpanded: boolean;
|
||||||
allCollapsed: boolean;
|
allCollapsed: boolean;
|
||||||
|
|
@ -112,12 +90,19 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||||
const allSeries = useSelector(createAllSeriesSelector());
|
const allSeries = useSelector(createAllSeriesSelector());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isEpisodesFetching,
|
isFetching: isEpisodesFetching,
|
||||||
isEpisodesPopulated,
|
isFetched: isEpisodesFetched,
|
||||||
episodesError,
|
error: episodesError,
|
||||||
hasEpisodes,
|
data,
|
||||||
hasMonitoredEpisodes,
|
refetch: refetchEpisodes,
|
||||||
} = useSelector(createEpisodesSelector());
|
} = useEpisodes({ seriesId });
|
||||||
|
|
||||||
|
const { hasEpisodes, hasMonitoredEpisodes } = useMemo(() => {
|
||||||
|
return {
|
||||||
|
hasEpisodes: data.length > 0,
|
||||||
|
hasMonitoredEpisodes: data.some((e) => e.monitored),
|
||||||
|
};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isFetching: isEpisodeFilesFetching,
|
isFetching: isEpisodeFilesFetching,
|
||||||
|
|
@ -358,8 +343,8 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||||
}, [seriesId, dispatch]);
|
}, [seriesId, dispatch]);
|
||||||
|
|
||||||
const populate = useCallback(() => {
|
const populate = useCallback(() => {
|
||||||
dispatch(fetchEpisodes({ seriesId }));
|
refetchEpisodes();
|
||||||
}, [seriesId, dispatch]);
|
}, [refetchEpisodes]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
populate();
|
populate();
|
||||||
|
|
@ -370,7 +355,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unregisterPagePopulator(populate);
|
unregisterPagePopulator(populate);
|
||||||
dispatch(clearEpisodes());
|
|
||||||
};
|
};
|
||||||
}, [populate, dispatch]);
|
}, [populate, dispatch]);
|
||||||
|
|
||||||
|
|
@ -439,7 +423,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
||||||
|
|
||||||
const fanartUrl = getFanartUrl(images);
|
const fanartUrl = getFanartUrl(images);
|
||||||
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
||||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesFetched;
|
const isPopulated = isEpisodesFetched && isEpisodeFilesFetched;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SeriesDetailsProvider seriesId={seriesId}>
|
<SeriesDetailsProvider seriesId={seriesId}>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import EpisodesAppState from 'App/State/EpisodesAppState';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
|
@ -18,6 +17,13 @@ import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import Episode from 'Episode/Episode';
|
import Episode from 'Episode/Episode';
|
||||||
|
import {
|
||||||
|
setEpisodeOptions,
|
||||||
|
setEpisodeSort,
|
||||||
|
useEpisodeOptions,
|
||||||
|
} from 'Episode/episodeOptionsStore';
|
||||||
|
import { getQueryKey, useToggleEpisodesMonitored } from 'Episode/useEpisode';
|
||||||
|
import { useSeasonEpisodes } from 'Episode/useEpisodes';
|
||||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
|
import { align, icons, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
|
@ -27,13 +33,7 @@ import SeriesHistoryModal from 'Series/History/SeriesHistoryModal';
|
||||||
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
|
import SeasonInteractiveSearchModal from 'Series/Search/SeasonInteractiveSearchModal';
|
||||||
import { Statistics } from 'Series/Series';
|
import { Statistics } from 'Series/Series';
|
||||||
import useSeries from 'Series/useSeries';
|
import useSeries from 'Series/useSeries';
|
||||||
import {
|
|
||||||
setEpisodesSort,
|
|
||||||
setEpisodesTableOption,
|
|
||||||
toggleEpisodesMonitored,
|
|
||||||
} from 'Store/Actions/episodeActions';
|
|
||||||
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
import { toggleSeasonMonitored } from 'Store/Actions/seriesActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { TableOptionsChangePayload } from 'typings/Table';
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
|
@ -86,21 +86,6 @@ function getSeasonStatistics(episodes: Episode[]) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEpisodesSelector(seasonNumber: number) {
|
|
||||||
return createSelector(
|
|
||||||
createClientSideCollectionSelector('episodes'),
|
|
||||||
(episodes: EpisodesAppState) => {
|
|
||||||
const { items, columns, sortKey, sortDirection } = episodes;
|
|
||||||
|
|
||||||
const episodesInSeason = items.filter(
|
|
||||||
(episode) => episode.seasonNumber === seasonNumber
|
|
||||||
);
|
|
||||||
|
|
||||||
return { items: episodesInSeason, columns, sortKey, sortDirection };
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIsSearchingSelector(seriesId: number, seasonNumber: number) {
|
function createIsSearchingSelector(seriesId: number, seasonNumber: number) {
|
||||||
return createSelector(createCommandsSelector(), (commands) => {
|
return createSelector(createCommandsSelector(), (commands) => {
|
||||||
return isCommandExecuting(
|
return isCommandExecuting(
|
||||||
|
|
@ -134,10 +119,9 @@ function SeriesDetailsSeason({
|
||||||
}: SeriesDetailsSeasonProps) {
|
}: SeriesDetailsSeasonProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { monitored: seriesMonitored, path } = useSeries(seriesId)!;
|
const { monitored: seriesMonitored, path } = useSeries(seriesId)!;
|
||||||
|
const { data: items } = useSeasonEpisodes(seriesId, seasonNumber);
|
||||||
|
|
||||||
const { items, columns, sortKey, sortDirection } = useSelector(
|
const { columns, sortKey, sortDirection } = useEpisodeOptions();
|
||||||
createEpisodesSelector(seasonNumber)
|
|
||||||
);
|
|
||||||
|
|
||||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
const isSearching = useSelector(
|
const isSearching = useSelector(
|
||||||
|
|
@ -162,10 +146,11 @@ function SeriesDetailsSeason({
|
||||||
const [isInteractiveSearchModalOpen, setIsInteractiveSearchModalOpen] =
|
const [isInteractiveSearchModalOpen, setIsInteractiveSearchModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const lastToggledEpisode = useRef<number | null>(null);
|
const { toggleEpisodesMonitored, isToggling, togglingEpisodeIds } =
|
||||||
const itemsRef = useRef(items);
|
useToggleEpisodesMonitored(getQueryKey('episodes')!);
|
||||||
|
|
||||||
itemsRef.current = items;
|
const lastToggledEpisode = useRef<number | null>(null);
|
||||||
|
const hasSetInitalExpand = useRef(false);
|
||||||
|
|
||||||
const seasonNumberTitle =
|
const seasonNumberTitle =
|
||||||
seasonNumber === 0
|
seasonNumber === 0
|
||||||
|
|
@ -196,25 +181,23 @@ function SeriesDetailsSeason({
|
||||||
{ shiftKey }: { shiftKey: boolean }
|
{ shiftKey }: { shiftKey: boolean }
|
||||||
) => {
|
) => {
|
||||||
const lastToggled = lastToggledEpisode.current;
|
const lastToggled = lastToggledEpisode.current;
|
||||||
const episodeIds = [episodeId];
|
const episodeIds = new Set([episodeId]);
|
||||||
|
|
||||||
if (shiftKey && lastToggled) {
|
if (shiftKey && lastToggled) {
|
||||||
const { lower, upper } = getToggledRange(items, episodeId, lastToggled);
|
const { lower, upper } = getToggledRange(items, episodeId, lastToggled);
|
||||||
for (let i = lower; i < upper; i++) {
|
for (let i = lower; i < upper; i++) {
|
||||||
episodeIds.push(items[i].id);
|
episodeIds.add(items[i].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastToggledEpisode.current = episodeId;
|
lastToggledEpisode.current = episodeId;
|
||||||
|
|
||||||
dispatch(
|
toggleEpisodesMonitored({
|
||||||
toggleEpisodesMonitored({
|
episodeIds: Array.from(episodeIds),
|
||||||
episodeIds,
|
monitored: value,
|
||||||
monitored: value,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[items, dispatch]
|
[items, toggleEpisodesMonitored]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSearchPress = useCallback(() => {
|
const handleSearchPress = useCallback(() => {
|
||||||
|
|
@ -259,32 +242,36 @@ function SeriesDetailsSeason({
|
||||||
|
|
||||||
const handleSortPress = useCallback(
|
const handleSortPress = useCallback(
|
||||||
(sortKey: string, sortDirection?: SortDirection) => {
|
(sortKey: string, sortDirection?: SortDirection) => {
|
||||||
dispatch(
|
setEpisodeSort({
|
||||||
setEpisodesSort({
|
sortKey,
|
||||||
sortKey,
|
sortDirection,
|
||||||
sortDirection,
|
});
|
||||||
})
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[dispatch]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTableOptionChange = useCallback(
|
const handleTableOptionChange = useCallback(
|
||||||
(payload: TableOptionsChangePayload) => {
|
(payload: TableOptionsChangePayload) => {
|
||||||
dispatch(setEpisodesTableOption(payload));
|
setEpisodeOptions(payload);
|
||||||
},
|
},
|
||||||
[dispatch]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (hasSetInitalExpand.current || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSetInitalExpand.current = true;
|
||||||
|
|
||||||
const expand =
|
const expand =
|
||||||
itemsRef.current.some(
|
items.some(
|
||||||
(item) =>
|
(item) =>
|
||||||
isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })
|
isAfter(item.airDateUtc) || isAfter(item.airDateUtc, { days: -30 })
|
||||||
) || itemsRef.current.every((item) => !item.airDateUtc);
|
) || items.every((item) => !item.airDateUtc);
|
||||||
|
|
||||||
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
onExpandPress(seasonNumber, expand && seasonNumber > 0);
|
||||||
}, [seriesId, seasonNumber, onExpandPress]);
|
}, [items, seriesId, seasonNumber, onExpandPress]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if ((previousEpisodeFileCount ?? 0) > 0 && episodeFileCount === 0) {
|
if ((previousEpisodeFileCount ?? 0) > 0 && episodeFileCount === 0) {
|
||||||
|
|
@ -505,6 +492,9 @@ function SeriesDetailsSeason({
|
||||||
key={item.id}
|
key={item.id}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
{...item}
|
{...item}
|
||||||
|
isSaving={
|
||||||
|
isToggling && togglingEpisodeIds.includes(item.id)
|
||||||
|
}
|
||||||
onMonitorEpisodePress={handleMonitorEpisodePress}
|
onMonitorEpisodePress={handleMonitorEpisodePress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import React from 'react';
|
|
||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { batchActions } from 'redux-batched-actions';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import episodeEntities from 'Episode/episodeEntities';
|
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import { updateItem } from './baseActions';
|
|
||||||
import createFetchHandler from './Creators/createFetchHandler';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
|
||||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'episodes';
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
sortKey: 'episodeNumber',
|
|
||||||
sortDirection: sortDirections.DESCENDING,
|
|
||||||
items: [],
|
|
||||||
|
|
||||||
columns: [
|
|
||||||
{
|
|
||||||
name: 'monitored',
|
|
||||||
columnLabel: () => translate('Monitored'),
|
|
||||||
isVisible: true,
|
|
||||||
isModifiable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'episodeNumber',
|
|
||||||
label: '#',
|
|
||||||
isVisible: true,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'title',
|
|
||||||
label: () => translate('Title'),
|
|
||||||
isVisible: true,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'path',
|
|
||||||
label: () => translate('Path'),
|
|
||||||
isVisible: false,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'relativePath',
|
|
||||||
label: () => translate('RelativePath'),
|
|
||||||
isVisible: false,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'airDateUtc',
|
|
||||||
label: () => translate('AirDate'),
|
|
||||||
isVisible: true,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'runtime',
|
|
||||||
label: () => translate('Runtime'),
|
|
||||||
isVisible: false,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'languages',
|
|
||||||
label: () => translate('Languages'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'audioInfo',
|
|
||||||
label: () => translate('AudioInfo'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'videoCodec',
|
|
||||||
label: () => translate('VideoCodec'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'videoDynamicRangeType',
|
|
||||||
label: () => translate('VideoDynamicRange'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'audioLanguages',
|
|
||||||
label: () => translate('AudioLanguages'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'subtitleLanguages',
|
|
||||||
label: () => translate('SubtitleLanguages'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'size',
|
|
||||||
label: () => translate('Size'),
|
|
||||||
isVisible: false,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'releaseGroup',
|
|
||||||
label: () => translate('ReleaseGroup'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'customFormats',
|
|
||||||
label: () => translate('Formats'),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'customFormatScore',
|
|
||||||
columnLabel: () => translate('CustomFormatScore'),
|
|
||||||
label: React.createElement(Icon, {
|
|
||||||
name: icons.SCORE,
|
|
||||||
title: () => translate('CustomFormatScore')
|
|
||||||
}),
|
|
||||||
isVisible: false,
|
|
||||||
isSortable: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'indexerFlags',
|
|
||||||
columnLabel: () => translate('IndexerFlags'),
|
|
||||||
label: React.createElement(Icon, {
|
|
||||||
name: icons.FLAG,
|
|
||||||
title: () => translate('IndexerFlags')
|
|
||||||
}),
|
|
||||||
isVisible: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
label: () => translate('Status'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'actions',
|
|
||||||
columnLabel: () => translate('Actions'),
|
|
||||||
isVisible: true,
|
|
||||||
isModifiable: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const persistState = [
|
|
||||||
'episodes.columns',
|
|
||||||
'episodes.sortDirection',
|
|
||||||
'episodes.sortKey'
|
|
||||||
];
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const FETCH_EPISODES = 'episodes/fetchEpisodes';
|
|
||||||
export const SET_EPISODES_SORT = 'episodes/setEpisodesSort';
|
|
||||||
export const SET_EPISODES_TABLE_OPTION = 'episodes/setEpisodesTableOption';
|
|
||||||
export const CLEAR_EPISODES = 'episodes/clearEpisodes';
|
|
||||||
export const TOGGLE_EPISODE_MONITORED = 'episodes/toggleEpisodeMonitored';
|
|
||||||
export const TOGGLE_EPISODES_MONITORED = 'episodes/toggleEpisodesMonitored';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const fetchEpisodes = createThunk(FETCH_EPISODES);
|
|
||||||
export const setEpisodesSort = createAction(SET_EPISODES_SORT);
|
|
||||||
export const setEpisodesTableOption = createAction(SET_EPISODES_TABLE_OPTION);
|
|
||||||
export const clearEpisodes = createAction(CLEAR_EPISODES);
|
|
||||||
export const toggleEpisodeMonitored = createThunk(TOGGLE_EPISODE_MONITORED);
|
|
||||||
export const toggleEpisodesMonitored = createThunk(TOGGLE_EPISODES_MONITORED);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
[FETCH_EPISODES]: createFetchHandler(section, '/episode'),
|
|
||||||
|
|
||||||
[TOGGLE_EPISODE_MONITORED]: function(getState, payload, dispatch) {
|
|
||||||
const {
|
|
||||||
episodeId: id,
|
|
||||||
episodeEntity = episodeEntities.EPISODES,
|
|
||||||
monitored
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section: episodeEntity,
|
|
||||||
isSaving: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: `/episode/${id}`,
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({ monitored }),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section: episodeEntity,
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(updateItem({
|
|
||||||
id,
|
|
||||||
section: episodeEntity,
|
|
||||||
isSaving: false
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[TOGGLE_EPISODES_MONITORED]: function(getState, payload, dispatch) {
|
|
||||||
const {
|
|
||||||
episodeIds,
|
|
||||||
episodeEntity = episodeEntities.EPISODES,
|
|
||||||
monitored
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const episodeSection = _.last(episodeEntity.split('.'));
|
|
||||||
|
|
||||||
dispatch(batchActions(
|
|
||||||
episodeIds.map((episodeId) => {
|
|
||||||
return updateItem({
|
|
||||||
id: episodeId,
|
|
||||||
section: episodeSection,
|
|
||||||
isSaving: true
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/episode/monitor',
|
|
||||||
method: 'PUT',
|
|
||||||
data: JSON.stringify({ episodeIds, monitored }),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(batchActions(
|
|
||||||
episodeIds.map((episodeId) => {
|
|
||||||
return updateItem({
|
|
||||||
id: episodeId,
|
|
||||||
section: episodeSection,
|
|
||||||
isSaving: false,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(batchActions(
|
|
||||||
episodeIds.map((episodeId) => {
|
|
||||||
return updateItem({
|
|
||||||
id: episodeId,
|
|
||||||
section: episodeSection,
|
|
||||||
isSaving: false
|
|
||||||
});
|
|
||||||
})
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
|
||||||
|
|
||||||
[SET_EPISODES_TABLE_OPTION]: createSetTableOptionReducer(section),
|
|
||||||
|
|
||||||
[CLEAR_EPISODES]: (state) => {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
isFetching: false,
|
|
||||||
isPopulated: false,
|
|
||||||
error: null,
|
|
||||||
items: []
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
[SET_EPISODES_SORT]: createSetClientSideCollectionSortReducer(section)
|
|
||||||
|
|
||||||
}, defaultState, section);
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as captcha from './captchaActions';
|
import * as captcha from './captchaActions';
|
||||||
import * as commands from './commandActions';
|
import * as commands from './commandActions';
|
||||||
import * as episodes from './episodeActions';
|
|
||||||
import * as episodeHistory from './episodeHistoryActions';
|
import * as episodeHistory from './episodeHistoryActions';
|
||||||
import * as importSeries from './importSeriesActions';
|
import * as importSeries from './importSeriesActions';
|
||||||
import * as interactiveImportActions from './interactiveImportActions';
|
import * as interactiveImportActions from './interactiveImportActions';
|
||||||
|
|
@ -17,7 +16,6 @@ export default [
|
||||||
app,
|
app,
|
||||||
captcha,
|
captcha,
|
||||||
commands,
|
commands,
|
||||||
episodes,
|
|
||||||
episodeHistory,
|
episodeHistory,
|
||||||
importSeries,
|
importSeries,
|
||||||
interactiveImportActions,
|
interactiveImportActions,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import { queryClient } from 'App/queryClient';
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
||||||
import getFilterTypePredicate from 'Helpers/Props/getFilterTypePredicate';
|
import getFilterTypePredicate from 'Helpers/Props/getFilterTypePredicate';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
|
@ -14,7 +15,6 @@ import createHandleActions from './Creators/createHandleActions';
|
||||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||||
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
import createSetSettingValueReducer from './Creators/Reducers/createSetSettingValueReducer';
|
||||||
import { fetchEpisodes } from './episodeActions';
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Local
|
// Local
|
||||||
|
|
@ -720,7 +720,7 @@ export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
promise.done((data) => {
|
promise.done((data) => {
|
||||||
if (shouldFetchEpisodesAfterUpdate) {
|
if (shouldFetchEpisodesAfterUpdate) {
|
||||||
dispatch(fetchEpisodes({ seriesId: seriesIds[0] }));
|
queryClient.invalidateQueries({ queryKey: ['/episode'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import KeysMatching from 'typings/Helpers/KeysMatching';
|
import KeysMatching from 'typings/Helpers/KeysMatching';
|
||||||
|
|
||||||
function selectUniqueIds<T, K>(items: T[], idProp: KeysMatching<T, K>) {
|
function selectUniqueIds<T, K>(
|
||||||
|
items: T[],
|
||||||
|
idProp: KeysMatching<T, K | K[]>
|
||||||
|
): K[] {
|
||||||
const result = items.reduce((acc: Set<K>, item) => {
|
const result = items.reduce((acc: Set<K>, item) => {
|
||||||
if (!item[idProp]) {
|
if (!item[idProp]) {
|
||||||
return acc;
|
return acc;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue