mirror of
https://github.com/Sonarr/Sonarr
synced 2026-01-06 23:58:27 +01:00
Use react-query for episode files
This commit is contained in:
parent
324d477376
commit
7daee3f63d
27 changed files with 343 additions and 424 deletions
|
|
@ -20,7 +20,6 @@ import createEpisodesFetchingSelector from 'Episode/createEpisodesFetchingSelect
|
|||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import HistoryItem from 'typings/History';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
|
|
@ -95,7 +94,6 @@ function History() {
|
|||
useEffect(() => {
|
||||
return () => {
|
||||
dispatch(clearEpisodes());
|
||||
dispatch(clearEpisodeFiles());
|
||||
};
|
||||
}, [requestCurrentPage, dispatch]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,9 @@
|
|||
import React, { createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import Queue from 'typings/Queue';
|
||||
|
||||
|
|
@ -16,16 +21,12 @@ interface AllDetails {
|
|||
|
||||
type QueueDetailsFilter = AllDetails | EpisodeDetails | SeriesDetails;
|
||||
|
||||
interface QueueDetailsProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const QueueDetailsContext = createContext<Queue[] | undefined>(undefined);
|
||||
|
||||
export default function QueueDetailsProvider({
|
||||
children,
|
||||
...filter
|
||||
}: QueueDetailsProps & QueueDetailsFilter) {
|
||||
}: PropsWithChildren<QueueDetailsFilter>) {
|
||||
const { data } = useApiQuery<Queue[]>({
|
||||
path: '/queue/details',
|
||||
queryParams: { ...filter },
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import BlocklistAppState from './BlocklistAppState';
|
|||
import CaptchaAppState from './CaptchaAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import CustomFiltersAppState from './CustomFiltersAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import HistoryAppState, { SeriesHistoryAppState } from './HistoryAppState';
|
||||
import ImportSeriesAppState from './ImportSeriesAppState';
|
||||
|
|
@ -80,7 +79,6 @@ interface AppState {
|
|||
captcha: CaptchaAppState;
|
||||
commands: CommandAppState;
|
||||
customFilters: CustomFiltersAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
episodeHistory: HistoryAppState;
|
||||
episodes: EpisodesAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
|
||||
interface EpisodeFilesAppState
|
||||
extends AppSectionState<EpisodeFile>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export default EpisodeFilesAppState;
|
||||
|
|
@ -10,7 +10,7 @@ import Link from 'Components/Link/Link';
|
|||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
|
|
|||
|
|
@ -3,16 +3,10 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Episode from 'Episode/Episode';
|
||||
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearEpisodeFiles,
|
||||
fetchEpisodeFiles,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import {
|
||||
registerPagePopulator,
|
||||
unregisterPagePopulator,
|
||||
|
|
@ -33,7 +27,7 @@ function Calendar() {
|
|||
const requestCurrentPage = useCurrentPage();
|
||||
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const { data, isFetching, isLoading, error, refetch } = useCalendar();
|
||||
const { isFetching, isLoading, error, refetch } = useCalendar();
|
||||
const view = useCalendarOption('view');
|
||||
|
||||
const isRefreshingSeries = useSelector(
|
||||
|
|
@ -57,10 +51,9 @@ function Calendar() {
|
|||
handleScheduleUpdate();
|
||||
|
||||
return () => {
|
||||
dispatch(clearEpisodeFiles());
|
||||
clearTimeout(updateTimeout.current);
|
||||
};
|
||||
}, [dispatch, handleScheduleUpdate]);
|
||||
}, [handleScheduleUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestCurrentPage) {
|
||||
|
|
@ -93,17 +86,6 @@ function Calendar() {
|
|||
}
|
||||
}, [isRefreshingSeries, wasRefreshingSeries, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
const episodeFileIds = selectUniqueIds<Episode, number>(
|
||||
data,
|
||||
'episodeFileId'
|
||||
);
|
||||
|
||||
if (episodeFileIds.length) {
|
||||
dispatch(fetchEpisodeFiles({ episodeFileIds }));
|
||||
}
|
||||
}, [data, dispatch]);
|
||||
|
||||
return (
|
||||
<div className={styles.calendar}>
|
||||
{isLoading ? <LoadingIndicator /> : null}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,12 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import QueueDetails from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
|
@ -10,6 +16,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Episode from 'Episode/Episode';
|
||||
import EpisodeFileProvider from 'EpisodeFile/EpisodeFileProvider';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
|
|
@ -88,6 +95,10 @@ function CalendarPage() {
|
|||
return selectUniqueIds<Episode, number>(data, 'id');
|
||||
}, [data]);
|
||||
|
||||
const episodeFileIds = useMemo(() => {
|
||||
return selectUniqueIds<Episode, number>(data, 'episodeFileId');
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (width === 0) {
|
||||
return;
|
||||
|
|
@ -102,7 +113,10 @@ function CalendarPage() {
|
|||
}, [width]);
|
||||
|
||||
return (
|
||||
<QueueDetails episodeIds={episodeIds}>
|
||||
<CalendarPageProvider
|
||||
episodeIds={episodeIds}
|
||||
episodeFileIds={episodeFileIds}
|
||||
>
|
||||
<PageContent title={translate('Calendar')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
|
|
@ -162,8 +176,22 @@ function CalendarPage() {
|
|||
onModalClose={handleOptionsModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
</QueueDetails>
|
||||
</CalendarPageProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default CalendarPage;
|
||||
|
||||
function CalendarPageProvider({
|
||||
episodeIds,
|
||||
episodeFileIds,
|
||||
children,
|
||||
}: PropsWithChildren<{ episodeIds: number[]; episodeFileIds: number[] }>) {
|
||||
return (
|
||||
<QueueDetailsProvider episodeIds={episodeIds}>
|
||||
<EpisodeFileProvider episodeFileIds={episodeFileIds}>
|
||||
{children}
|
||||
</EpisodeFileProvider>
|
||||
</QueueDetailsProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
|
|||
import EpisodeDetailsModal from 'Episode/EpisodeDetailsModal';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import getFinaleTypeName from 'Episode/getFinaleTypeName';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useDispatch } from 'react-redux';
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Command from 'Commands/Command';
|
||||
import Episode from 'Episode/Episode';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
import { PagedQueryResponse } from 'Helpers/Hooks/usePagedApiQuery';
|
||||
import { setAppValue, setVersion } from 'Store/Actions/appActions';
|
||||
import { removeItem, updateItem } from 'Store/Actions/baseActions';
|
||||
|
|
@ -164,15 +165,61 @@ function SignalRListener() {
|
|||
}
|
||||
|
||||
if (name === 'episodefile') {
|
||||
const section = 'episodeFiles';
|
||||
if (version < 5) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.action === 'updated') {
|
||||
dispatch(updateItem({ section, ...body.resource }));
|
||||
const updatedItem = body.resource as EpisodeFile;
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['/episodeFile'] },
|
||||
(oldData: EpisodeFile[] | undefined) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
const itemIndex = oldData.findIndex(
|
||||
(item) => item.id === updatedItem.id
|
||||
);
|
||||
|
||||
// Add episode file to the end
|
||||
if (itemIndex === -1) {
|
||||
return [...oldData, updatedItem];
|
||||
}
|
||||
|
||||
return oldData.map((item) => {
|
||||
if (item.id === updatedItem.id) {
|
||||
return updatedItem;
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Repopulate the page to handle recently imported file
|
||||
repopulatePage('episodeFileUpdated');
|
||||
} else if (body.action === 'deleted') {
|
||||
dispatch(removeItem({ section, id: body.resource.id }));
|
||||
const id = body.resource.id;
|
||||
|
||||
queryClient.setQueriesData(
|
||||
{ queryKey: ['/episodeFile'] },
|
||||
(oldData: EpisodeFile[] | undefined) => {
|
||||
if (!oldData) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
const itemIndex = oldData.findIndex((item) => item.id === id);
|
||||
|
||||
// Add episode file to the end
|
||||
if (itemIndex === -1) {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
return oldData.filter((item) => item.id !== id);
|
||||
}
|
||||
);
|
||||
|
||||
repopulatePage('episodeFileDeleted');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import QueueDetails from 'Activity/Queue/QueueDetails';
|
|||
import Icon from 'Components/Icon';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Column from 'Components/Table/Column';
|
||||
|
|
@ -7,15 +7,12 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import Episode from 'Episode/Episode';
|
||||
import useEpisode, { EpisodeEntity } from 'Episode/useEpisode';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import { useDeleteEpisodeFile } from 'EpisodeFile/useEpisodeFiles';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import {
|
||||
deleteEpisodeFile,
|
||||
fetchEpisodeFile,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EpisodeAiring from './EpisodeAiring';
|
||||
import EpisodeFileRow from './EpisodeFileRow';
|
||||
|
|
@ -76,11 +73,13 @@ interface EpisodeSummaryProps {
|
|||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
function EpisodeSummary(props: EpisodeSummaryProps) {
|
||||
const { seriesId, episodeId, episodeEntity, episodeFileId } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
function EpisodeSummary({
|
||||
seriesId,
|
||||
episodeId,
|
||||
episodeEntity,
|
||||
episodeFileId,
|
||||
}: EpisodeSummaryProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const { qualityProfileId, network } = useSeries(seriesId) as Series;
|
||||
|
||||
const { airDateUtc, overview } = useEpisode(
|
||||
|
|
@ -97,22 +96,22 @@ function EpisodeSummary(props: EpisodeSummaryProps) {
|
|||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
} = useEpisodeFile(episodeFileId) || {};
|
||||
} = useEpisodeFile(episodeFileId) ?? {};
|
||||
|
||||
const { deleteEpisodeFile } = useDeleteEpisodeFile(
|
||||
episodeFileId!,
|
||||
episodeEntity
|
||||
);
|
||||
|
||||
const handleDeleteEpisodeFile = useCallback(() => {
|
||||
dispatch(
|
||||
deleteEpisodeFile({
|
||||
id: episodeFileId,
|
||||
episodeEntity,
|
||||
})
|
||||
);
|
||||
}, [episodeFileId, episodeEntity, dispatch]);
|
||||
deleteEpisodeFile();
|
||||
}, [deleteEpisodeFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (episodeFileId && !path) {
|
||||
dispatch(fetchEpisodeFile({ id: episodeFileId }));
|
||||
queryClient.invalidateQueries({ queryKey: ['/episodeFile'] });
|
||||
}
|
||||
}, [episodeFileId, path, dispatch]);
|
||||
}, [episodeFileId, path, queryClient]);
|
||||
|
||||
const hasOverview = !!overview;
|
||||
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function createNoOpEpisodeSelector(_episodeId?: number) {
|
|||
);
|
||||
}
|
||||
|
||||
const getQueryKey = (episodeEntity: EpisodeEntity) => {
|
||||
export const getQueryKey = (episodeEntity: EpisodeEntity) => {
|
||||
switch (episodeEntity) {
|
||||
case 'calendar':
|
||||
return episodeQueryKeyStore.getState().calendar;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import useEpisodeFile from './useEpisodeFile';
|
||||
import { useEpisodeFile } from './EpisodeFileProvider';
|
||||
|
||||
interface EpisodeFileLanguagesProps {
|
||||
episodeFileId: number | undefined;
|
||||
|
|
|
|||
42
frontend/src/EpisodeFile/EpisodeFileProvider.tsx
Normal file
42
frontend/src/EpisodeFile/EpisodeFileProvider.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import React, {
|
||||
createContext,
|
||||
PropsWithChildren,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { EpisodeFile } from './EpisodeFile';
|
||||
import useEpisodeFiles, { EpisodeFileFilter } from './useEpisodeFiles';
|
||||
|
||||
export const EpisodeFileContext = createContext<EpisodeFile[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
export default function EpisodeFileProvider({
|
||||
children,
|
||||
...filter
|
||||
}: PropsWithChildren<EpisodeFileFilter>) {
|
||||
const { data } = useEpisodeFiles(filter);
|
||||
|
||||
return (
|
||||
<EpisodeFileContext.Provider value={data}>
|
||||
{children}
|
||||
</EpisodeFileContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useEpisodeFile(id: number | undefined) {
|
||||
const episodeFiles = useContext(EpisodeFileContext);
|
||||
|
||||
return useMemo(() => {
|
||||
if (id === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return episodeFiles?.find((item) => item.id === id);
|
||||
}, [id, episodeFiles]);
|
||||
}
|
||||
|
||||
export interface SeriesEpisodeFile {
|
||||
count: number;
|
||||
episodesWithFiles: number;
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import useEpisodeFile from './useEpisodeFile';
|
||||
import { useEpisodeFile } from './EpisodeFileProvider';
|
||||
|
||||
function formatLanguages(languages: string | undefined) {
|
||||
if (!languages) {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
export const AUDIO = 'audio';
|
||||
export const AUDIO_LANGUAGES = 'audioLanguages';
|
||||
export const SUBTITLES = 'subtitles';
|
||||
export const VIDEO = 'video';
|
||||
export const VIDEO_DYNAMIC_RANGE_TYPE = 'videoDynamicRangeType';
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createEpisodeFileSelector(episodeFileId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodeFiles.items,
|
||||
(episodeFiles) => {
|
||||
return episodeFiles.find(({ id }) => id === episodeFileId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function useEpisodeFile(episodeFileId: number | undefined) {
|
||||
return useSelector(createEpisodeFileSelector(episodeFileId));
|
||||
}
|
||||
|
||||
export default useEpisodeFile;
|
||||
107
frontend/src/EpisodeFile/useEpisodeFiles.ts
Normal file
107
frontend/src/EpisodeFile/useEpisodeFiles.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { EpisodeEntity, getQueryKey } from 'Episode/useEpisode';
|
||||
import useApiMutation from 'Helpers/Hooks/useApiMutation';
|
||||
import useApiQuery from 'Helpers/Hooks/useApiQuery';
|
||||
import { EpisodeFile } from './EpisodeFile';
|
||||
|
||||
const DEFAULT_EPISODE_FILES: EpisodeFile[] = [];
|
||||
|
||||
interface SeriesEpisodeFiles {
|
||||
seriesId: number;
|
||||
}
|
||||
|
||||
interface EpisodeFileIds {
|
||||
episodeFileIds: number[];
|
||||
}
|
||||
|
||||
export type EpisodeFileFilter = SeriesEpisodeFiles | EpisodeFileIds;
|
||||
|
||||
const useEpisodeFiles = (params: EpisodeFileFilter) => {
|
||||
const result = useApiQuery<EpisodeFile[]>({
|
||||
path: '/episodeFile',
|
||||
queryParams: { ...params },
|
||||
queryOptions: {
|
||||
enabled:
|
||||
('seriesId' in params && params.seriesId !== undefined) ||
|
||||
('episodeFileIds' in params && params.episodeFileIds?.length > 0),
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ?? DEFAULT_EPISODE_FILES,
|
||||
hasEpisodeFiles: !!result.data?.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEpisodeFiles;
|
||||
|
||||
export const useDeleteEpisodeFile = (
|
||||
id: number,
|
||||
episodeEntity: EpisodeEntity
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, error, isPending } = useApiMutation<unknown, void>({
|
||||
path: `/episodeFile/${id}`,
|
||||
method: 'DELETE',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/episodeFile'] });
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [getQueryKey(episodeEntity)],
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteEpisodeFile: mutate,
|
||||
isDeleting: isPending,
|
||||
deleteError: error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDeleteEpisodeFiles = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, error, isPending } = useApiMutation<unknown, EpisodeFileIds>({
|
||||
path: '/episodeFile/bulk',
|
||||
method: 'DELETE',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/episodeFile'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['/episode'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
deleteEpisodeFiles: mutate,
|
||||
isDeleting: isPending,
|
||||
deleteError: error,
|
||||
};
|
||||
};
|
||||
|
||||
export const useUpdateEpisodeFiles = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate, isPending, error } = useApiMutation<
|
||||
unknown,
|
||||
Partial<EpisodeFile>[]
|
||||
>({
|
||||
path: '/episodeFile/bulk',
|
||||
method: 'PUT',
|
||||
mutationOptions: {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['/episodeFile'] });
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
updateEpisodeFiles: mutate,
|
||||
isUpdating: isPending,
|
||||
updateError: error,
|
||||
};
|
||||
};
|
||||
|
|
@ -24,6 +24,10 @@ import Column from 'Components/Table/Column';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
import {
|
||||
useDeleteEpisodeFiles,
|
||||
useUpdateEpisodeFiles,
|
||||
} from 'EpisodeFile/useEpisodeFiles';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
|
|
@ -43,10 +47,6 @@ import Language from 'Language/Language';
|
|||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
deleteEpisodeFiles,
|
||||
updateEpisodeFiles,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import {
|
||||
clearInteractiveImport,
|
||||
fetchInteractiveImportItems,
|
||||
|
|
@ -200,17 +200,6 @@ function isSameEpisodeFile(
|
|||
return !hasDifferentItems(originalFile.episodes, episodes);
|
||||
}
|
||||
|
||||
const episodeFilesInfoSelector = createSelector(
|
||||
(state: AppState) => state.episodeFiles.isDeleting,
|
||||
(state: AppState) => state.episodeFiles.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const importModeSelector = createSelector(
|
||||
(state: AppState) => state.interactiveImport.importMode,
|
||||
(importMode) => {
|
||||
|
|
@ -269,7 +258,11 @@ function InteractiveImportModalContentInner(
|
|||
createClientSideCollectionSelector('interactiveImport')
|
||||
);
|
||||
|
||||
const { isDeleting, deleteError } = useSelector(episodeFilesInfoSelector);
|
||||
const { isDeleting, deleteEpisodeFiles, deleteError } =
|
||||
useDeleteEpisodeFiles();
|
||||
|
||||
const { updateEpisodeFiles } = useUpdateEpisodeFiles();
|
||||
|
||||
const importMode = useSelector(importModeSelector);
|
||||
|
||||
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
|
||||
|
|
@ -492,8 +485,8 @@ function InteractiveImportModalContentInner(
|
|||
return acc;
|
||||
}, []);
|
||||
|
||||
dispatch(deleteEpisodeFiles({ episodeFileIds }));
|
||||
}, [items, selectedIds, setIsConfirmDeleteModalOpen, dispatch]);
|
||||
deleteEpisodeFiles({ episodeFileIds });
|
||||
}, [items, selectedIds, setIsConfirmDeleteModalOpen, deleteEpisodeFiles]);
|
||||
|
||||
const onConfirmDeleteModalClose = useCallback(() => {
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
|
|
@ -602,11 +595,7 @@ function InteractiveImportModalContentInner(
|
|||
let shouldClose = false;
|
||||
|
||||
if (existingFiles.length) {
|
||||
dispatch(
|
||||
updateEpisodeFiles({
|
||||
files: existingFiles,
|
||||
})
|
||||
);
|
||||
updateEpisodeFiles(existingFiles);
|
||||
|
||||
shouldClose = true;
|
||||
}
|
||||
|
|
@ -635,6 +624,7 @@ function InteractiveImportModalContentInner(
|
|||
selectedIds,
|
||||
onModalClose,
|
||||
dispatch,
|
||||
updateEpisodeFiles,
|
||||
]);
|
||||
|
||||
const onSortPress = useCallback<SortCallback>(
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ import EpisodeStatus from 'Episode/EpisodeStatus';
|
|||
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
|
||||
import IndexerFlags from 'Episode/IndexerFlags';
|
||||
import EpisodeFileLanguages from 'EpisodeFile/EpisodeFileLanguages';
|
||||
import { useEpisodeFile } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import MediaInfo from 'EpisodeFile/MediaInfo';
|
||||
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
|
||||
import useEpisodeFile from 'EpisodeFile/useEpisodeFile';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import useSeries from 'Series/useSeries';
|
||||
import MediaInfoModel from 'typings/MediaInfo';
|
||||
|
|
@ -223,10 +222,7 @@ function EpisodeRow({
|
|||
if (name === 'audioInfo') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.audio}>
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.AUDIO}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
<MediaInfo type="audio" episodeFileId={episodeFileId} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
@ -234,10 +230,7 @@ function EpisodeRow({
|
|||
if (name === 'audioLanguages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.audioLanguages}>
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
<MediaInfo type="audioLanguages" episodeFileId={episodeFileId} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
@ -245,10 +238,7 @@ function EpisodeRow({
|
|||
if (name === 'subtitleLanguages') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.subtitles}>
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.SUBTITLES}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
<MediaInfo type="subtitles" episodeFileId={episodeFileId} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
@ -256,10 +246,7 @@ function EpisodeRow({
|
|||
if (name === 'videoCodec') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.video}>
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.VIDEO}
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
<MediaInfo type="video" episodeFileId={episodeFileId} />
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
|
@ -268,7 +255,7 @@ function EpisodeRow({
|
|||
return (
|
||||
<TableRowCell key={name} className={styles.videoDynamicRangeType}>
|
||||
<MediaInfo
|
||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||
type="videoDynamicRangeType"
|
||||
episodeFileId={episodeFileId}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import moment from 'moment';
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import Alert from 'Components/Alert';
|
||||
|
|
@ -21,6 +20,7 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import useEpisodeFiles from 'EpisodeFile/useEpisodeFiles';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
align,
|
||||
|
|
@ -44,10 +44,6 @@ import useSeries from 'Series/useSeries';
|
|||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import {
|
||||
clearEpisodeFiles,
|
||||
fetchEpisodeFiles,
|
||||
} from 'Store/Actions/episodeFileActions';
|
||||
import { toggleSeriesMonitored } from 'Store/Actions/seriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
|
|
@ -63,6 +59,7 @@ import translate from 'Utilities/String/translate';
|
|||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import SeriesAlternateTitles from './SeriesAlternateTitles';
|
||||
import SeriesDetailsLinks from './SeriesDetailsLinks';
|
||||
import SeriesDetailsProvider from './SeriesDetailsProvider';
|
||||
import SeriesDetailsSeason from './SeriesDetailsSeason';
|
||||
import SeriesProgressLabel from './SeriesProgressLabel';
|
||||
import SeriesTags from './SeriesTags';
|
||||
|
|
@ -98,24 +95,6 @@ function createEpisodesSelector() {
|
|||
);
|
||||
}
|
||||
|
||||
function createEpisodeFilesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.episodeFiles,
|
||||
(episodeFiles) => {
|
||||
const { items, isFetching, isPopulated, error } = episodeFiles;
|
||||
|
||||
const hasEpisodeFiles = !!items.length;
|
||||
|
||||
return {
|
||||
isEpisodeFilesFetching: isFetching,
|
||||
isEpisodeFilesPopulated: isPopulated,
|
||||
episodeFilesError: error,
|
||||
hasEpisodeFiles,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface ExpandedState {
|
||||
allExpanded: boolean;
|
||||
allCollapsed: boolean;
|
||||
|
|
@ -139,12 +118,13 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
hasEpisodes,
|
||||
hasMonitoredEpisodes,
|
||||
} = useSelector(createEpisodesSelector());
|
||||
|
||||
const {
|
||||
isEpisodeFilesFetching,
|
||||
isEpisodeFilesPopulated,
|
||||
episodeFilesError,
|
||||
isFetching: isEpisodeFilesFetching,
|
||||
isFetched: isEpisodeFilesFetched,
|
||||
error: episodeFilesError,
|
||||
hasEpisodeFiles,
|
||||
} = useSelector(createEpisodeFilesSelector());
|
||||
} = useEpisodeFiles({ seriesId });
|
||||
|
||||
const commands = useSelector(createCommandsSelector());
|
||||
|
||||
|
|
@ -379,7 +359,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
|
||||
const populate = useCallback(() => {
|
||||
dispatch(fetchEpisodes({ seriesId }));
|
||||
dispatch(fetchEpisodeFiles({ seriesId }));
|
||||
}, [seriesId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -392,7 +371,6 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
return () => {
|
||||
unregisterPagePopulator(populate);
|
||||
dispatch(clearEpisodes());
|
||||
dispatch(clearEpisodeFiles());
|
||||
};
|
||||
}, [populate, dispatch]);
|
||||
|
||||
|
|
@ -461,10 +439,10 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
|
||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesFetched;
|
||||
|
||||
return (
|
||||
<QueueDetailsProvider seriesId={seriesId}>
|
||||
<SeriesDetailsProvider seriesId={seriesId}>
|
||||
<PageContent title={title}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
|
|
@ -892,7 +870,7 @@ function SeriesDetails({ seriesId }: SeriesDetailsProps) {
|
|||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
</QueueDetailsProvider>
|
||||
</SeriesDetailsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
21
frontend/src/Series/Details/SeriesDetailsProvider.tsx
Normal file
21
frontend/src/Series/Details/SeriesDetailsProvider.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React, { PropsWithChildren } from 'react';
|
||||
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import { EpisodeFileContext } from 'EpisodeFile/EpisodeFileProvider';
|
||||
import useEpisodeFiles from 'EpisodeFile/useEpisodeFiles';
|
||||
|
||||
function SeriesDetailsProvider({
|
||||
seriesId,
|
||||
children,
|
||||
}: PropsWithChildren<{ seriesId: number }>) {
|
||||
const { data } = useEpisodeFiles({ seriesId });
|
||||
|
||||
return (
|
||||
<QueueDetailsProvider seriesId={seriesId}>
|
||||
<EpisodeFileContext.Provider value={data}>
|
||||
{children}
|
||||
</EpisodeFileContext.Provider>
|
||||
</QueueDetailsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default SeriesDetailsProvider;
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { removeItem, set, updateItem } from './baseActions';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'episodeFiles';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_EPISODE_FILE = 'episodeFiles/fetchEpisodeFile';
|
||||
export const FETCH_EPISODE_FILES = 'episodeFiles/fetchEpisodeFiles';
|
||||
export const DELETE_EPISODE_FILE = 'episodeFiles/deleteEpisodeFile';
|
||||
export const DELETE_EPISODE_FILES = 'episodeFiles/deleteEpisodeFiles';
|
||||
export const UPDATE_EPISODE_FILES = 'episodeFiles/updateEpisodeFiles';
|
||||
export const CLEAR_EPISODE_FILES = 'episodeFiles/clearEpisodeFiles';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchEpisodeFile = createThunk(FETCH_EPISODE_FILE);
|
||||
export const fetchEpisodeFiles = createThunk(FETCH_EPISODE_FILES);
|
||||
export const deleteEpisodeFile = createThunk(DELETE_EPISODE_FILE);
|
||||
export const deleteEpisodeFiles = createThunk(DELETE_EPISODE_FILES);
|
||||
export const updateEpisodeFiles = createThunk(UPDATE_EPISODE_FILES);
|
||||
export const clearEpisodeFiles = createAction(CLEAR_EPISODE_FILES);
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
||||
const deleteEpisodeFileHelper = createRemoveItemHandler(section, '/episodeFile');
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_EPISODE_FILE]: createFetchHandler(section, '/episodeFile'),
|
||||
[FETCH_EPISODE_FILES]: createFetchHandler(section, '/episodeFile'),
|
||||
|
||||
[DELETE_EPISODE_FILE]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
id: episodeFileId,
|
||||
episodeEntity = episodeEntities.EPISODES
|
||||
} = payload;
|
||||
|
||||
const episodeSection = _.last(episodeEntity.split('.'));
|
||||
const deletePromise = deleteEpisodeFileHelper(getState, payload, dispatch);
|
||||
|
||||
deletePromise.done(() => {
|
||||
const episodes = getState().episodes.items;
|
||||
const episodesWithRemovedFiles = _.filter(episodes, { episodeFileId });
|
||||
|
||||
dispatch(batchActions([
|
||||
...episodesWithRemovedFiles.map((episode) => {
|
||||
return updateItem({
|
||||
section: episodeSection,
|
||||
...episode,
|
||||
episodeFileId: 0,
|
||||
hasFile: false
|
||||
});
|
||||
})
|
||||
]));
|
||||
});
|
||||
},
|
||||
|
||||
[DELETE_EPISODE_FILES]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
episodeFileIds
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isDeleting: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/episodeFile/bulk',
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({ episodeFileIds })
|
||||
}).request;
|
||||
|
||||
promise.done(() => {
|
||||
const episodes = getState().episodes.items;
|
||||
const episodesWithRemovedFiles = episodeFileIds.reduce((acc, episodeFileId) => {
|
||||
acc.push(..._.filter(episodes, { episodeFileId }));
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
dispatch(batchActions([
|
||||
...episodeFileIds.map((id) => {
|
||||
return removeItem({ section, id });
|
||||
}),
|
||||
|
||||
...episodesWithRemovedFiles.map((episode) => {
|
||||
return updateItem({
|
||||
section: 'episodes',
|
||||
...episode,
|
||||
episodeFileId: 0,
|
||||
hasFile: false
|
||||
});
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[UPDATE_EPISODE_FILES]: function(getState, payload, dispatch) {
|
||||
const { files } = payload;
|
||||
|
||||
dispatch(set({ section, isSaving: true }));
|
||||
|
||||
const requestData = files;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/episodeFile/bulk',
|
||||
method: 'PUT',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(requestData)
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
...files.map((file) => {
|
||||
const id = file.id;
|
||||
const props = {};
|
||||
const episodeFile = data.find((f) => f.id === id);
|
||||
|
||||
props.qualityCutoffNotMet = episodeFile.qualityCutoffNotMet;
|
||||
props.customFormats = episodeFile.customFormats;
|
||||
props.customFormatScore = episodeFile.customFormatScore;
|
||||
props.languages = file.languages;
|
||||
props.quality = file.quality;
|
||||
props.releaseGroup = file.releaseGroup;
|
||||
props.indexerFlags = file.indexerFlags;
|
||||
|
||||
return updateItem({
|
||||
section,
|
||||
id,
|
||||
...props
|
||||
});
|
||||
}),
|
||||
|
||||
set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isSaving: false,
|
||||
saveError: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_EPISODE_FILES]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -3,7 +3,6 @@ import * as captcha from './captchaActions';
|
|||
import * as commands from './commandActions';
|
||||
import * as customFilters from './customFilterActions';
|
||||
import * as episodes from './episodeActions';
|
||||
import * as episodeFiles from './episodeFileActions';
|
||||
import * as episodeHistory from './episodeHistoryActions';
|
||||
import * as episodeSelection from './episodeSelectionActions';
|
||||
import * as importSeries from './importSeriesActions';
|
||||
|
|
@ -25,7 +24,6 @@ export default [
|
|||
commands,
|
||||
customFilters,
|
||||
episodes,
|
||||
episodeFiles,
|
||||
episodeHistory,
|
||||
episodeSelection,
|
||||
importSeries,
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createEpisodeFileSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { episodeFileId }: { episodeFileId: number }) =>
|
||||
episodeFileId,
|
||||
(state: AppState) => state.episodeFiles,
|
||||
(episodeFileId, episodeFiles) => {
|
||||
if (!episodeFileId) {
|
||||
return;
|
||||
}
|
||||
|
||||
return episodeFiles.items.find(
|
||||
(episodeFile) => episodeFile.id === episodeFileId
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createEpisodeFileSelector;
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import episodeEntities from 'Episode/episodeEntities';
|
||||
|
||||
function createEpisodeSelector() {
|
||||
return createSelector(
|
||||
(state, { episodeId }) => episodeId,
|
||||
(state, { episodeEntity = episodeEntities.EPISODES }) => _.get(state, episodeEntity, { items: [] }),
|
||||
(episodeId, episodes) => {
|
||||
return _.find(episodes.items, { id: episodeId });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createEpisodeSelector;
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import QueueDetailsProvider from 'Activity/Queue/Details/QueueDetailsProvider';
|
||||
import { SelectProvider, useSelect } from 'App/Select/SelectContext';
|
||||
|
|
@ -20,9 +26,9 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||
import TablePager from 'Components/Table/TablePager';
|
||||
import Episode from 'Episode/Episode';
|
||||
import { useToggleEpisodesMonitored } from 'Episode/useEpisode';
|
||||
import EpisodeFileProvider from 'EpisodeFile/EpisodeFileProvider';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { TableOptionsChangePayload } from 'typings/Table';
|
||||
|
|
@ -187,14 +193,11 @@ function CutoffUnmetContent() {
|
|||
};
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (episodeFileIds.length) {
|
||||
dispatch(fetchEpisodeFiles({ episodeFileIds }));
|
||||
}
|
||||
}, [episodeFileIds, dispatch]);
|
||||
|
||||
return (
|
||||
<QueueDetailsProvider episodeIds={episodeIds}>
|
||||
<CutoffUnmetProvider
|
||||
episodeIds={episodeIds}
|
||||
episodeFileIds={episodeFileIds}
|
||||
>
|
||||
<PageContent title={translate('CutoffUnmet')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
|
|
@ -320,7 +323,7 @@ function CutoffUnmetContent() {
|
|||
) : null}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
</QueueDetailsProvider>
|
||||
</CutoffUnmetProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -333,3 +336,17 @@ export default function CutoffUnmet() {
|
|||
</SelectProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function CutoffUnmetProvider({
|
||||
episodeIds,
|
||||
episodeFileIds,
|
||||
children,
|
||||
}: PropsWithChildren<{ episodeIds: number[]; episodeFileIds: number[] }>) {
|
||||
return (
|
||||
<QueueDetailsProvider episodeIds={episodeIds}>
|
||||
<EpisodeFileProvider episodeFileIds={episodeFileIds}>
|
||||
{children}
|
||||
</EpisodeFileProvider>
|
||||
</QueueDetailsProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue