Typings cleanup and improvements

pull/5547/head
Mark McDowall 1 year ago
parent 5326a102e2
commit b2c43fb2a6

@ -0,0 +1,48 @@
import SortDirection from 'Helpers/Props/SortDirection';
export interface Error {
responseJSON: {
message: string;
};
}
export interface AppSectionDeleteState {
isDeleting: boolean;
deleteError: Error;
}
export interface AppSectionSaveState {
isSaving: boolean;
saveError: Error;
}
export interface PagedAppSectionState {
pageSize: number;
}
export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
}
export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
item: T;
}
interface AppSectionState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: T[];
sortKey: string;
sortDirection: SortDirection;
}
export default AppSectionState;

@ -0,0 +1,52 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import EpisodeFilesAppState from './EpisodeFilesAppState';
import EpisodesAppState from './EpisodesAppState';
import QueueAppState from './QueueAppState';
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
id: string;
name: string;
}
export interface FilterBuilderProp<T> {
name: string;
label: string;
type: string;
valueType?: string;
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
}
export interface PropertyFilter {
key: string;
value: boolean | string | number | string[] | number[];
type: string;
}
export interface Filter {
key: string;
label: string;
filers: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
filers: PropertyFilter[];
}
interface AppState {
episodesSelection: EpisodesAppState;
episodeFiles: EpisodeFilesAppState;
interactiveImport: InteractiveImportAppState;
seriesIndex: SeriesIndexAppState;
settings: SettingsAppState;
series: SeriesAppState;
tags: TagsAppState;
queue: QueueAppState;
}
export default AppState;

@ -0,0 +1,8 @@
import { CustomFilter } from './AppState';
interface ClientSideCollectionAppState {
totalItems: number;
customFilters: CustomFilter[];
}
export default ClientSideCollectionAppState;

@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
import { CustomFilter } from './AppState';
interface CustomFiltersAppState
extends AppSectionState<CustomFilter>,
AppSectionDeleteState {}
export default CustomFiltersAppState;

@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
interface EpisodeFilesAppState
extends AppSectionState<EpisodeFile>,
AppSectionDeleteState {}
export default EpisodeFilesAppState;

@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Episode from 'Episode/Episode';
type EpisodesAppState = AppSectionState<Episode>;
export default EpisodesAppState;

@ -0,0 +1,12 @@
import AppSectionState from 'App/State/AppSectionState';
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
import ImportMode from '../../InteractiveImport/ImportMode';
import InteractiveImport from '../../InteractiveImport/InteractiveImport';
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
originalItems: InteractiveImport[];
importMode: ImportMode;
recentFolders: RecentFolder[];
}
export default InteractiveImportAppState;

@ -0,0 +1,53 @@
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
export interface StatusMessage {
title: string;
messages: string[];
}
export interface Queue extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
statusMessages: StatusMessage[];
errorMessage: string;
downloadId: string;
protocol: string;
downloadClient: string;
outputPath: string;
episodeHasFile: boolean;
seriesId?: number;
episodeId?: number;
seasonNumber?: number;
}
export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown;
}
export interface QueuePagedAppState extends AppSectionState<Queue> {
isGrabbing: boolean;
grabError: Error;
isRemoving: boolean;
removeError: Error;
}
interface QueueAppState {
status: AppSectionItemState<Queue>;
details: QueueDetailsAppState;
paged: QueuePagedAppState;
}
export default QueueAppState;

@ -0,0 +1,62 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Series from 'Series/Series';
import { Filter, FilterBuilderProp } from './AppState';
export interface SeriesIndexAppState {
sortKey: string;
sortDirection: SortDirection;
secondarySortKey: string;
secondarySortDirection: SortDirection;
view: string;
posterOptions: {
detailedProgressBar: boolean;
size: string;
showTitle: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showSearchAction: boolean;
};
overviewOptions: {
detailedProgressBar: boolean;
size: string;
showMonitored: boolean;
showNetwork: boolean;
showQualityProfile: boolean;
showPreviousAiring: boolean;
showAdded: boolean;
showSeasonCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
showSearchAction: boolean;
};
tableOptions: {
showBanners: boolean;
showSearchAction: boolean;
};
selectedFilterKey: string;
filterBuilderProps: FilterBuilderProp<Series>[];
filters: Filter[];
columns: Column[];
}
interface SeriesAppState
extends AppSectionState<Series>,
AppSectionDeleteState,
AppSectionSaveState {
itemMap: Record<number, number>;
deleteOptions: {
addImportListExclusion: boolean;
};
}
export default SeriesAppState;

@ -0,0 +1,28 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionSchemaState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import DownloadClient from 'typings/DownloadClient';
import QualityProfile from 'typings/QualityProfile';
import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState {}
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionState<UiSettings>;
interface SettingsAppState {
downloadClients: DownloadClientAppState;
language: LanguageSettingsAppState;
uiSettings: UiSettingsAppState;
qualityProfiles: QualityProfilesAppState;
}
export default SettingsAppState;

@ -0,0 +1,12 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export default TagsAppState;

@ -0,0 +1,37 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
seriesId?: number;
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;

@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
info,
} = props;
const [detailedError, setDetailedError] = useState(null);
const [detailedError, setDetailedError] = useState<
StackTrace.StackFrame[] | null
>(null);
useEffect(() => {
if (error) {

@ -1,5 +1,10 @@
import classNames from 'classnames';
import React, { ComponentClass, FunctionComponent, useCallback } from 'react';
import React, {
ComponentClass,
FunctionComponent,
SyntheticEvent,
useCallback,
} from 'react';
import { Link as RouterLink } from 'react-router-dom';
import styles from './Link.css';
@ -17,7 +22,7 @@ export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
target?: string;
isDisabled?: boolean;
noRouter?: boolean;
onPress?(event: Event): void;
onPress?(event: SyntheticEvent): void;
}
function Link(props: LinkProps) {
const {
@ -33,7 +38,7 @@ function Link(props: LinkProps) {
} = props;
const onClick = useCallback(
(event) => {
(event: SyntheticEvent) => {
if (!isDisabled && onPress) {
onPress(event);
}
@ -57,6 +62,8 @@ function Link(props: LinkProps) {
linkProps.href = to;
linkProps.target = target || '_self';
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
el = RouterLink;
linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`;
linkProps.target = target;

@ -1,5 +1,5 @@
import React, { forwardRef, ReactNode, useCallback } from 'react';
import Scroller from 'Components/Scroller/Scroller';
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import { isLocked } from 'Utilities/scrollLock';
import styles from './PageContentBody.css';
@ -9,14 +9,11 @@ interface PageContentBodyProps {
innerClassName: string;
children: ReactNode;
initialScrollTop?: number;
onScroll?: (payload) => void;
onScroll?: (payload: OnScroll) => void;
}
const PageContentBody = forwardRef(
(
props: PageContentBodyProps,
ref: React.MutableRefObject<HTMLDivElement>
) => {
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className = styles.contentBody,
innerClassName = styles.innerContentBody,
@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
} = props;
const onScrollWrapper = useCallback(
(payload) => {
(payload: OnScroll) => {
if (onScroll && !isLocked()) {
onScroll(payload);
}

@ -1,9 +1,21 @@
import classNames from 'classnames';
import { throttle } from 'lodash';
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
import React, {
ForwardedRef,
forwardRef,
MutableRefObject,
ReactNode,
useEffect,
useRef,
} from 'react';
import ScrollDirection from 'Helpers/Props/ScrollDirection';
import styles from './Scroller.css';
export interface OnScroll {
scrollLeft: number;
scrollTop: number;
}
interface ScrollerProps {
className?: string;
scrollDirection?: ScrollDirection;
@ -12,11 +24,11 @@ interface ScrollerProps {
scrollTop?: number;
initialScrollTop?: number;
children?: ReactNode;
onScroll?: (payload) => void;
onScroll?: (payload: OnScroll) => void;
}
const Scroller = forwardRef(
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
const {
className,
autoFocus = false,
@ -30,7 +42,7 @@ const Scroller = forwardRef(
} = props;
const internalRef = useRef();
const currentRef = ref ?? internalRef;
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
useEffect(
() => {

@ -1,8 +1,10 @@
import React from 'react';
interface Column {
name: string;
label: string;
columnLabel: string;
isSortable: boolean;
label: string | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;
isModifiable?: boolean;
}

@ -1,24 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import scrollPositions from 'Store/scrollPositions';
function withScrollPosition(WrappedComponent, scrollPositionKey) {
function ScrollPosition(props) {
interface WrappedComponentProps {
initialScrollTop: number;
}
interface ScrollPositionProps {
history: RouteComponentProps['history'];
location: RouteComponentProps['location'];
match: RouteComponentProps['match'];
}
function withScrollPosition(
WrappedComponent: React.FC<WrappedComponentProps>,
scrollPositionKey: string
) {
function ScrollPosition(props: ScrollPositionProps) {
const { history } = props;
const initialScrollTop =
history.action === 'POP' ||
(history.location.state && history.location.state.restoreScrollPosition)
? scrollPositions[scrollPositionKey]
: 0;
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
}
ScrollPosition.propTypes = {
history: PropTypes.object.isRequired,
};
return ScrollPosition;
}

@ -0,0 +1,20 @@
import ModelBase from 'App/ModelBase';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import MediaInfo from 'typings/MediaInfo';
export interface EpisodeFile extends ModelBase {
seriesId: number;
seasonNumber: number;
relativePath: string;
path: string;
size: number;
dateAdded: string;
sceneName: string;
releaseGroup: string;
languages: CustomFormat[];
quality: QualityModel;
customFormats: CustomFormat[];
mediaInfo: MediaInfo;
qualityCutoffNotMet: boolean;
}

@ -5,7 +5,7 @@ import areAllSelected from 'Utilities/Table/areAllSelected';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
type SelectedState = Record<number, boolean>;
export type SelectedState = Record<number, boolean>;
export interface SelectState {
selectedState: SelectedState;

@ -7,8 +7,8 @@ import SelectEpisodeModalContent, {
interface SelectEpisodeModalProps {
isOpen: boolean;
selectedIds: number[] | string[];
seriesId: number;
seasonNumber: number;
seriesId?: number;
seasonNumber?: number;
selectedDetails?: string;
isAnime: boolean;
modalTitle: string;

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import EpisodesAppState from 'App/State/EpisodesAppState';
import TextInput from 'Components/Form/TextInput';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@ -14,12 +15,15 @@ import TableBody from 'Components/Table/TableBody';
import Episode from 'Episode/Episode';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { kinds, scrollDirections } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
import {
clearEpisodes,
fetchEpisodes,
setEpisodesSort,
} from 'Store/Actions/episodeSelectionActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import SelectEpisodeRow from './SelectEpisodeRow';
@ -47,7 +51,7 @@ const columns = [
function episodesSelector() {
return createSelector(
createClientSideCollectionSelector('episodeSelection'),
(episodes) => {
(episodes: EpisodesAppState) => {
return episodes;
}
);
@ -60,8 +64,8 @@ export interface SelectedEpisode {
interface SelectEpisodeModalContentProps {
selectedIds: number[] | string[];
seriesId: number;
seasonNumber: number;
seriesId?: number;
seasonNumber?: number;
selectedDetails?: string;
isAnime: boolean;
sortKey?: string;
@ -100,26 +104,26 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
const filterEpisodeNumber = parseInt(filter);
const errorMessage = getErrorMessage(error, 'Unable to load episodes');
const selectedCount = selectedIds.length;
const selectedEpisodesCount = getSelectedIds(selectState).length;
const selectedEpisodesCount = getSelectedIds(selectedState).length;
const selectionIsValid =
selectedEpisodesCount > 0 && selectedEpisodesCount % selectedCount === 0;
const onFilterChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setFilter(value.toLowerCase());
},
[setFilter]
);
const onSelectAllChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback(
({ id, value, shiftKey = false }) => {
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
@ -132,7 +136,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
);
const onSortPress = useCallback(
(newSortKey, newSortDirection) => {
(newSortKey: string, newSortDirection: SortDirection) => {
dispatch(
setEpisodesSort({
sortKey: newSortKey,
@ -144,9 +148,9 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
);
const onEpisodesSelectWrapper = useCallback(() => {
const episodeIds = getSelectedIds(selectedState);
const episodeIds: number[] = getSelectedIds(selectedState);
const selectedEpisodes = items.reduce((acc, item) => {
const selectedEpisodes = items.reduce((acc: Episode[], item) => {
if (episodeIds.indexOf(item.id) > -1) {
acc.push(item);
}
@ -167,7 +171,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
);
return {
fileId,
fileId: fileId as number,
episodes,
};
});

@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import PathInputConnector from 'Components/Form/PathInputConnector';
import Icon from 'Components/Icon';
@ -18,7 +19,6 @@ import {
removeRecentFolder,
} from 'Store/Actions/interactiveImportActions';
import translate from 'Utilities/String/translate';
import RecentFolder from './RecentFolder';
import RecentFolderRow from './RecentFolderRow';
import styles from './InteractiveImportSelectFolderModalContent.css';
@ -49,9 +49,9 @@ function InteractiveImportSelectFolderModalContent(
const { modalTitle, onFolderSelect, onModalClose } = props;
const [folder, setFolder] = useState('');
const dispatch = useDispatch();
const recentFolders: RecentFolder[] = useSelector(
const recentFolders = useSelector(
createSelector(
(state) => state.interactiveImport.recentFolders,
(state: AppState) => state.interactiveImport.recentFolders,
(recentFolders) => {
return recentFolders;
}
@ -59,14 +59,14 @@ function InteractiveImportSelectFolderModalContent(
);
const onPathChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setFolder(value);
},
[setFolder]
);
const onRecentPathPress = useCallback(
(value) => {
(value: string) => {
setFolder(value);
},
[setFolder]
@ -91,8 +91,8 @@ function InteractiveImportSelectFolderModalContent(
}, [folder, onFolderSelect, dispatch]);
const onRemoveRecentFolderPress = useCallback(
(f) => {
dispatch(removeRecentFolder({ folder: f }));
(folderToRemove: string) => {
dispatch(removeRecentFolder({ folder: folderToRemove }));
},
[dispatch]
);

@ -1,7 +1,3 @@
enum ImportMode {
Auto = 'auto',
Move = 'move',
Copy = 'copy',
}
type ImportMode = 'auto' | 'move' | 'copy' | 'chooseImportMode';
export default ImportMode;

@ -2,6 +2,8 @@ import { cloneDeep, without } from 'lodash';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import * as commandNames from 'Commands/commandNames';
import SelectInput from 'Components/Form/SelectInput';
import Icon from 'Components/Icon';
@ -20,16 +22,24 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import ImportMode from 'InteractiveImport/ImportMode';
import InteractiveImport, {
InteractiveImportCommandOptions,
} from 'InteractiveImport/InteractiveImport';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import { executeCommand } from 'Store/Actions/commandActions';
import {
deleteEpisodeFiles,
@ -44,6 +54,8 @@ import {
updateInteractiveImportItems,
} from 'Store/Actions/interactiveImportActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { SortCallback } from 'typings/callbacks';
import { SelectStateInputProps } from 'typings/props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
@ -59,6 +71,13 @@ type SelectType =
| 'quality'
| 'language';
type FilterExistingFiles = 'all' | 'new';
// TODO: This feels janky to do, but not sure of a better way currently
type OnSelectedChangeCallback = React.ComponentProps<
typeof InteractiveImportRow
>['onSelectedChange'];
const COLUMNS = [
{
name: 'relativePath',
@ -125,25 +144,23 @@ const COLUMNS = [
},
];
const filterExistingFilesOptions = {
ALL: 'all',
NEW: 'new',
};
const importModeOptions = [
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
{ key: 'move', value: 'Move Files' },
{ key: 'copy', value: 'Hardlink/Copy Files' },
];
function isSameEpisodeFile(file, originalFile) {
function isSameEpisodeFile(
file: InteractiveImport,
originalFile?: InteractiveImport
) {
const { series, seasonNumber, episodes } = file;
if (!originalFile) {
return false;
}
if (!originalFile.series || series.id !== originalFile.series.id) {
if (!originalFile.series || series?.id !== originalFile.series.id) {
return false;
}
@ -155,8 +172,8 @@ function isSameEpisodeFile(file, originalFile) {
}
const episodeFilesInfoSelector = createSelector(
(state) => state.episodeFiles.isDeleting,
(state) => state.episodeFiles.deleteError,
(state: AppState) => state.episodeFiles.isDeleting,
(state: AppState) => state.episodeFiles.deleteError,
(isDeleting, deleteError) => {
return {
isDeleting,
@ -166,7 +183,7 @@ const episodeFilesInfoSelector = createSelector(
);
const importModeSelector = createSelector(
(state) => state.interactiveImport.importMode,
(state: AppState) => state.interactiveImport.importMode,
(importMode) => {
return importMode;
}
@ -178,7 +195,6 @@ interface InteractiveImportModalContentProps {
seasonNumber?: number;
showSeries?: boolean;
allowSeriesChange?: boolean;
autoSelectRow?: boolean;
showDelete?: boolean;
showImportMode?: boolean;
showFilterExistingFiles?: boolean;
@ -200,7 +216,6 @@ function InteractiveImportModalContent(
seriesId,
seasonNumber,
allowSeriesChange = true,
autoSelectRow = true,
showSeries = true,
showFilterExistingFiles = false,
showDelete = false,
@ -221,16 +236,18 @@ function InteractiveImportModalContent(
originalItems,
sortKey,
sortDirection,
} = useSelector(createClientSideCollectionSelector('interactiveImport'));
}: InteractiveImportAppState = useSelector(
createClientSideCollectionSelector('interactiveImport')
);
const { isDeleting, deleteError } = useSelector(episodeFilesInfoSelector);
const importMode = useSelector(importModeSelector);
const [invalidRowsSelected, setInvalidRowsSelected] = useState([]);
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
const [
withoutEpisodeFileIdRowsSelected,
setWithoutEpisodeFileIdRowsSelected,
] = useState([]);
] = useState<number[]>([]);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
);
@ -253,16 +270,20 @@ function InteractiveImportModalContent(
const dispatch = useDispatch();
const columns: Column[] = useMemo(() => {
const result = cloneDeep(COLUMNS);
const result: Column[] = cloneDeep(COLUMNS);
if (!showSeries) {
result.find((c) => c.name === 'series').isVisible = false;
const seriesColumn = result.find((c) => c.name === 'series');
if (seriesColumn) {
seriesColumn.isVisible = false;
}
}
return result;
}, [showSeries]);
const selectedIds = useMemo(() => {
const selectedIds: number[] = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
@ -317,13 +338,13 @@ function InteractiveImportModalContent(
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
const onSelectAllChange = useCallback(
({ value }) => {
({ value }: SelectStateInputProps) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const onSelectedChange = useCallback(
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
({ id, value, hasEpisodeFileId, shiftKey = false }) => {
setSelectState({
type: 'toggleSelected',
@ -365,7 +386,7 @@ function InteractiveImportModalContent(
const onConfirmDelete = useCallback(() => {
setIsConfirmDeleteModalOpen(false);
const episodeFileIds = items.reduce((acc, item) => {
const episodeFileIds = items.reduce((acc: number[], item) => {
if (selectedIds.indexOf(item.id) > -1 && item.episodeFileId) {
acc.push(item.episodeFileId);
}
@ -381,11 +402,10 @@ function InteractiveImportModalContent(
}, [setIsConfirmDeleteModalOpen]);
const onImportSelectedPress = useCallback(() => {
const finalImportMode =
downloadId || !showImportMode ? ImportMode.Auto : importMode;
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
const existingFiles = [];
const files = [];
const existingFiles: Partial<EpisodeFile>[] = [];
const files: InteractiveImportCommandOptions[] = [];
if (finalImportMode === 'chooseImportMode') {
setInteractiveImportErrorMessage('An import mode must be selected');
@ -511,16 +531,18 @@ function InteractiveImportModalContent(
dispatch,
]);
const onSortPress = useCallback(
const onSortPress = useCallback<SortCallback>(
(sortKey, sortDirection) => {
dispatch(setInteractiveImportSort({ sortKey, sortDirection }));
},
[dispatch]
);
const onFilterExistingFilesChange = useCallback(
const onFilterExistingFilesChange = useCallback<
(value: FilterExistingFiles) => void
>(
(value) => {
const filter = value !== filterExistingFilesOptions.ALL;
const filter = value !== 'all';
setFilterExistingFiles(filter);
@ -536,14 +558,18 @@ function InteractiveImportModalContent(
[downloadId, seriesId, folder, setFilterExistingFiles, dispatch]
);
const onImportModeChange = useCallback(
const onImportModeChange = useCallback<
({ value }: { value: ImportMode }) => void
>(
({ value }) => {
dispatch(setInteractiveImportMode({ importMode: value }));
},
[dispatch]
);
const onSelectModalSelect = useCallback(
const onSelectModalSelect = useCallback<
({ value }: { value: SelectType }) => void
>(
({ value }) => {
setSelectModalOpen(value);
},
@ -555,7 +581,7 @@ function InteractiveImportModalContent(
}, [setSelectModalOpen]);
const onSeriesSelect = useCallback(
(series) => {
(series: Series) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -573,7 +599,7 @@ function InteractiveImportModalContent(
);
const onSeasonSelect = useCallback(
(seasonNumber) => {
(seasonNumber: number) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -590,7 +616,7 @@ function InteractiveImportModalContent(
);
const onEpisodesSelect = useCallback(
(episodes) => {
(episodes: SelectedEpisode[]) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -606,7 +632,7 @@ function InteractiveImportModalContent(
);
const onReleaseGroupSelect = useCallback(
(releaseGroup) => {
(releaseGroup: string) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -622,7 +648,7 @@ function InteractiveImportModalContent(
);
const onLanguagesSelect = useCallback(
(newLanguages) => {
(newLanguages: Language[]) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -638,7 +664,7 @@ function InteractiveImportModalContent(
);
const onQualitySelect = useCallback(
(quality) => {
(quality: QualityModel) => {
dispatch(
updateInteractiveImportItems({
ids: selectedIds,
@ -653,7 +679,7 @@ function InteractiveImportModalContent(
[selectedIds, dispatch]
);
const orderedSelectedIds = items.reduce((acc, file) => {
const orderedSelectedIds = items.reduce((acc: number[], file) => {
if (selectedIds.includes(file.id)) {
acc.push(file.id);
}
@ -690,7 +716,7 @@ function InteractiveImportModalContent(
<MenuContent>
<SelectedMenuItem
name={filterExistingFilesOptions.ALL}
name={'all'}
isSelected={!filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@ -698,7 +724,7 @@ function InteractiveImportModalContent(
</SelectedMenuItem>
<SelectedMenuItem
name={filterExistingFilesOptions.NEW}
name={'new'}
isSelected={filterExistingFiles}
onPress={onFilterExistingFilesChange}
>
@ -733,7 +759,6 @@ function InteractiveImportModalContent(
isSelected={selectedState[item.id]}
{...item}
allowSeriesChange={allowSeriesChange}
autoSelectRow={autoSelectRow}
columns={columns}
modalTitle={modalTitle}
onSelectedChange={onSelectedChange}

@ -27,6 +27,7 @@ import {
reprocessInteractiveImportItems,
updateInteractiveImportItem,
} from 'Store/Actions/interactiveImportActions';
import { SelectStateInputProps } from 'typings/props';
import Rejection from 'typings/Rejection';
import formatBytes from 'Utilities/Number/formatBytes';
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
@ -40,6 +41,10 @@ type SelectType =
| 'quality'
| 'language';
type SelectedChangeProps = SelectStateInputProps & {
hasEpisodeFileId: boolean;
};
interface InteractiveImportRowProps {
id: number;
allowSeriesChange: boolean;
@ -58,7 +63,7 @@ interface InteractiveImportRowProps {
isReprocessing?: boolean;
isSelected?: boolean;
modalTitle: string;
onSelectedChange(...args: unknown[]): void;
onSelectedChange(result: SelectedChangeProps): void;
onValidRowChange(id: number, isValid: boolean): void;
}
@ -88,7 +93,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
const dispatch = useDispatch();
const isSeriesColumnVisible = useMemo(
() => columns.find((c) => c.name === 'series').isVisible,
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
[columns]
);
@ -110,6 +115,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
id,
hasEpisodeFileId: !!episodeFileId,
value: true,
shiftKey: false,
});
}
},
@ -143,7 +149,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
]);
const onSelectedChangeWrapper = useCallback(
(result) => {
(result: SelectedChangeProps) => {
onSelectedChange({
...result,
hasEpisodeFileId: !!episodeFileId,
@ -158,6 +164,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
id,
hasEpisodeFileId: !!episodeFileId,
value: true,
shiftKey: false,
});
}
}, [id, episodeFileId, isSelected, onSelectedChange]);
@ -312,9 +319,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
);
});
const requiresSeasonNumber = isNaN(Number(seasonNumber));
const showSeriesPlaceholder = isSelected && !series;
const showSeasonNumberPlaceholder =
isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
isSelected && !!series && requiresSeasonNumber && !isReprocessing;
const showEpisodeNumbersPlaceholder =
isSelected && Number.isInteger(seasonNumber) && !episodes.length;
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
@ -364,9 +372,11 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
</TableRowCellButton>
<TableRowCellButton
isDisabled={!series || isNaN(seasonNumber)}
isDisabled={!series || requiresSeasonNumber}
title={
series && !isNaN(seasonNumber) ? 'Click to change episode' : undefined
series && !requiresSeasonNumber
? 'Click to change episode'
: undefined
}
onPress={onSelectEpisodePress}
>
@ -456,7 +466,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
<SelectSeasonModal
isOpen={selectModalOpen === 'season'}
seriesId={series && series.id}
seriesId={series?.id}
modalTitle={modalTitle}
onSeasonSelect={onSeasonSelect}
onModalClose={onSelectModalClose}
@ -465,7 +475,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
<SelectEpisodeModal
isOpen={selectModalOpen === 'episode'}
selectedIds={[id]}
seriesId={series && series.id}
seriesId={series?.id}
isAnime={isAnime}
seasonNumber={seasonNumber}
selectedDetails={relativePath}

@ -3,6 +3,19 @@ import Episode from 'Episode/Episode';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series';
import Rejection from 'typings/Rejection';
export interface InteractiveImportCommandOptions {
path: string;
folderName: string;
seriesId: number;
episodeIds: number[];
releaseGroup?: string;
quality: QualityModel;
languages: Language[];
downloadId?: string;
episodeFileId?: number;
}
interface InteractiveImport extends ModelBase {
path: string;
@ -18,7 +31,7 @@ interface InteractiveImport extends ModelBase {
episodes: Episode[];
qualityWeight: number;
customFormats: object[];
rejections: string[];
rejections: Rejection[];
episodeFileId?: number;
}

@ -27,8 +27,8 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
const previousIsOpen = usePrevious(isOpen);
const onFolderSelect = useCallback(
(f) => {
setFolderPath(f);
(path: string) => {
setFolderPath(path);
},
[setFolderPath]
);

@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -25,11 +26,12 @@ interface SelectLanguageModalContentProps {
function createFilteredLanguagesSelector() {
return createSelector(createLanguagesSelector(), (languages) => {
const { isFetching, isPopulated, error, items } = languages;
const { isFetching, isPopulated, error, items } =
languages as LanguageSettingsAppState;
const filterItems = ['Any', 'Original'];
const filteredLanguages = items.filter(
(lang) => !filterItems.includes(lang.name)
(lang: Language) => !filterItems.includes(lang.name)
);
return {
@ -51,7 +53,7 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
const [languageIds, setLanguageIds] = useState(props.languageIds);
const onLanguageChange = useCallback(
({ value, name }) => {
({ name, value }: { name: string; value: boolean }) => {
const changedId = parseInt(name);
let newLanguages = [...languageIds];

@ -1,6 +1,8 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { Error } from 'App/State/AppSectionState';
import AppState from 'App/State/AppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -12,22 +14,32 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import { QualityModel } from 'Quality/Quality';
import Quality, { QualityModel } from 'Quality/Quality';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { CheckInputChanged } from 'typings/inputs';
import getQualities from 'Utilities/Quality/getQualities';
function createQualitySchemeSelctor() {
interface QualitySchemaState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: Quality[];
}
function createQualitySchemaSelector() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
(state: AppState) => state.settings.qualityProfiles,
(qualityProfiles): QualitySchemaState => {
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
qualityProfiles;
const items = getQualities(schema.items) as Quality[];
return {
isFetching: isSchemaFetching,
isPopulated: isSchemaPopulated,
error: schemaError,
items: getQualities(schema.items),
items,
};
}
);
@ -50,7 +62,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
const [real, setReal] = useState(props.real);
const { isFetching, isPopulated, error, items } = useSelector(
createQualitySchemeSelctor()
createQualitySchemaSelector()
);
const dispatch = useDispatch();
@ -72,28 +84,28 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
}, [items]);
const onQualityChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setQualityId(parseInt(value));
},
[setQualityId]
);
const onProperChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
setProper(value);
},
[setProper]
);
const onRealChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
setReal(value);
},
[setReal]
);
const onQualitySelectWrapper = useCallback(() => {
const quality = items.find((item) => item.id === qualityId);
const quality = items.find((item) => item.id === qualityId) as Quality;
const revision = {
version: proper ? 2 : 1,

@ -25,7 +25,7 @@ function SelectReleaseGroupModalContent(
const [releaseGroup, setReleaseGroup] = useState(props.releaseGroup);
const onReleaseGroupChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setReleaseGroup(value);
},
[setReleaseGroup]

@ -5,8 +5,8 @@ import SelectSeasonModalContent from './SelectSeasonModalContent';
interface SelectSeasonModalProps {
isOpen: boolean;
modalTitle: string;
seriesId: number;
onSeasonSelect(seasonNumber): void;
seriesId?: number;
onSeasonSelect(seasonNumber: number): void;
onModalClose(): void;
}

@ -5,20 +5,21 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { Season } from 'Series/Series';
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
import SelectSeasonRow from './SelectSeasonRow';
interface SelectSeasonModalContentProps {
seriesId: number;
seriesId?: number;
modalTitle: string;
onSeasonSelect(seasonNumber): void;
onSeasonSelect(seasonNumber: number): void;
onModalClose(): void;
}
function SelectSeasonModalContent(props: SelectSeasonModalContentProps) {
const { seriesId, modalTitle, onSeasonSelect, onModalClose } = props;
const series = useSelector(createSeriesSelectorForHook(seriesId));
const seasons = useMemo(() => {
const seasons = useMemo<Season[]>(() => {
return series.seasons.slice(0).reverse();
}, [series]);

@ -22,11 +22,11 @@ interface SelectSeriesModalContentProps {
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
const { modalTitle, onSeriesSelect, onModalClose } = props;
const allSeries = useSelector(createAllSeriesSelector());
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const [filter, setFilter] = useState('');
const onFilterChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setFilter(value);
},
[setFilter]
@ -34,7 +34,7 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
const onSeriesSelectWrapper = useCallback(
(seriesId: number) => {
const series = allSeries.find((s) => s.id === seriesId);
const series = allSeries.find((s) => s.id === seriesId) as Series;
onSeriesSelect(series);
},

@ -15,6 +15,7 @@ import EpisodeQuality from 'Episode/EpisodeQuality';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
@ -25,7 +26,11 @@ import ReleaseEpisode from './ReleaseEpisode';
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
import styles from './InteractiveSearchRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
function getDownloadIcon(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return icons.SPINNER;
} else if (isGrabbed) {
@ -37,7 +42,11 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
function getDownloadTooltip(
isGrabbing: boolean,
isGrabbed: boolean,
grabError?: string
) {
if (isGrabbing) {
return '';
} else if (isGrabbed) {
@ -65,7 +74,7 @@ interface InteractiveSearchRowProps {
leechers?: number;
quality: QualityModel;
languages: Language[];
customFormats?: object[];
customFormats: CustomFormat[];
customFormatScore: number;
sceneMapping?: object;
seasonNumber?: number;

@ -4,7 +4,7 @@ import styles from './SelectDownloadClientRow.css';
interface SelectSeasonRowProps {
id: number;
name: number;
name: string;
priority: number;
onDownloadClientSelect(downloadClientId: number): unknown;
}

@ -19,7 +19,7 @@ interface OverrideMatchModalProps {
quality: QualityModel;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError: string;
grabError?: string;
onModalClose(): void;
}

@ -12,6 +12,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import EpisodeLanguages from 'Episode/EpisodeLanguages';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
@ -49,7 +50,7 @@ interface OverrideMatchModalContentProps {
quality: QualityModel;
protocol: DownloadProtocol;
isGrabbing: boolean;
grabError: string;
grabError?: string;
onModalClose(): void;
}
@ -70,7 +71,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
const [episodes, setEpisodes] = useState(props.episodes);
const [languages, setLanguages] = useState(props.languages);
const [quality, setQuality] = useState(props.quality);
const [downloadClientId, setDownloadClientId] = useState(null);
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
null
@ -137,7 +138,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
}, [setSelectModalOpen]);
const onEpisodesSelect = useCallback(
(episodeMap) => {
(episodeMap: SelectedEpisode[]) => {
setEpisodes(episodeMap[0].episodes);
setSelectModalOpen(null);
},
@ -149,7 +150,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
}, [setSelectModalOpen]);
const onQualitySelect = useCallback(
(quality) => {
(quality: QualityModel) => {
setQuality(quality);
setSelectModalOpen(null);
},
@ -161,7 +162,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
}, [setSelectModalOpen]);
const onLanguagesSelect = useCallback(
(languages) => {
(languages: Language[]) => {
setLanguages(languages);
setSelectModalOpen(null);
},
@ -173,7 +174,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
}, [setSelectModalOpen]);
const onDownloadClientSelect = useCallback(
(downloadClientId) => {
(downloadClientId: number) => {
setDownloadClientId(downloadClientId);
setSelectModalOpen(null);
},
@ -264,7 +265,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
data={
<OverrideMatchData
value={episodeInfo}
isDisabled={!series || isNaN(seasonNumber)}
isDisabled={!series || isNaN(Number(seasonNumber))}
onPress={onSelectEpisodePress}
/>
}

@ -9,9 +9,9 @@ import { icons, tooltipPositions } from 'Helpers/Props';
import styles from './ReleaseSceneIndicator.css';
function formatReleaseNumber(
seasonNumber,
episodeNumbers,
absoluteEpisodeNumbers
seasonNumber: number | undefined,
episodeNumbers: number[] | undefined,
absoluteEpisodeNumbers: number[] | undefined
) {
if (episodeNumbers && episodeNumbers.length) {
if (episodeNumbers.length > 1) {

@ -652,7 +652,6 @@ class SeriesDetails extends Component {
initialSortDirection={sortDirections.DESCENDING}
showSeries={false}
allowSeriesChange={false}
autoSelectRow={false}
showDelete={true}
showImportMode={false}
modalTitle={'Manage Episodes'}

@ -498,7 +498,6 @@ class SeriesDetailsSeason extends Component {
initialSortDirection={sortDirections.DESCENDING}
showSeries={false}
allowSeriesChange={false}
autoSelectRow={false}
showDelete={true}
showImportMode={false}
modalTitle={'Manage Episodes'}

@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal';
function SeriesIndexFilterMenu(props) {
interface SeriesIndexFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
customFilters: CustomFilter[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function SeriesIndexFilterMenu(props: SeriesIndexFilterMenuProps) {
const {
selectedFilterKey,
filters,
@ -26,15 +34,6 @@ function SeriesIndexFilterMenu(props) {
);
}
SeriesIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired,
};
SeriesIndexFilterMenu.defaultProps = {
showCustomFilters: false,
};

@ -1,11 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props';
import { align } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
interface SeriesIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
}
function SeriesIndexSortMenu(props) {
function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return (
@ -150,11 +157,4 @@ function SeriesIndexSortMenu(props) {
);
}
SeriesIndexSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
};
export default SeriesIndexSortMenu;

@ -1,11 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props';
function SeriesIndexViewMenu(props) {
interface SeriesIndexViewMenuProps {
view: string;
isDisabled: boolean;
onViewSelect(value: string): unknown;
}
function SeriesIndexViewMenu(props: SeriesIndexViewMenuProps) {
const { view, isDisabled, onViewSelect } = props;
return (
@ -31,10 +36,4 @@ function SeriesIndexViewMenu(props) {
);
}
SeriesIndexViewMenu.propTypes = {
view: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired,
onViewSelect: PropTypes.func.isRequired,
};
export default SeriesIndexViewMenu;

@ -45,7 +45,7 @@ function SeriesIndexOverviewOptionsModalContent(
const dispatch = useDispatch();
const onOverviewOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: unknown }) => {
dispatch(setSeriesOverviewOption({ [name]: value }));
},
[dispatch]

@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
import { Statistics } from 'Series/Series';
import SeriesPoster from 'Series/SeriesPoster';
import { executeCommand } from 'Store/Actions/commandActions';
import dimensions from 'Styles/Variables/dimensions';
@ -66,7 +67,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
previousAiring,
added,
overview,
statistics = {},
statistics = {} as Statistics,
images,
network,
} = series;

@ -1,14 +1,50 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React, { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { icons } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import dimensions from 'Styles/Variables/dimensions';
import { UiSettings } from 'typings/UiSettings';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import SeriesIndexOverviewInfoRow from './SeriesIndexOverviewInfoRow';
import styles from './SeriesIndexOverviewInfo.css';
interface RowProps {
name: string;
showProp: string;
valueProp: string;
}
interface RowInfoProps {
title: string;
iconName: IconDefinition;
label: string;
}
interface SeriesIndexOverviewInfoProps {
height: number;
showNetwork: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showPreviousAiring: boolean;
showAdded: boolean;
showSeasonCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAiring?: string;
network?: string;
qualityProfile: object;
previousAiring?: string;
added?: string;
seasonCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
}
const infoRowHeight = parseInt(dimensions.seriesIndexOverviewInfoRowHeight);
const rows = [
@ -54,7 +90,11 @@ const rows = [
},
];
function getInfoRowProps(row, props, uiSettings) {
function getInfoRowProps(
row: RowProps,
props: SeriesIndexOverviewInfoProps,
uiSettings: UiSettings
): RowInfoProps | null {
const { name } = row;
if (name === 'monitored') {
@ -71,7 +111,7 @@ function getInfoRowProps(row, props, uiSettings) {
return {
title: 'Network',
iconName: icons.NETWORK,
label: props.network,
label: props.network ?? '',
};
}
@ -79,6 +119,9 @@ function getInfoRowProps(row, props, uiSettings) {
return {
title: 'Quality Profile',
iconName: icons.PROFILE,
// TODO: Type QualityProfile
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2339)
label: props.qualityProfile.name,
};
}
@ -95,15 +138,11 @@ function getInfoRowProps(row, props, uiSettings) {
timeFormat
)}`,
iconName: icons.CALENDAR,
label: getRelativeDate(
previousAiring,
shortDateFormat,
showRelativeDates,
{
label:
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}
),
}) ?? '',
};
}
@ -115,10 +154,11 @@ function getInfoRowProps(row, props, uiSettings) {
return {
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
iconName: icons.ADD,
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}),
label:
getRelativeDate(added, shortDateFormat, showRelativeDates, {
timeFormat,
timeForToday: true,
}) ?? '',
};
}
@ -154,28 +194,8 @@ function getInfoRowProps(row, props, uiSettings) {
label: formatBytes(props.sizeOnDisk),
};
}
}
interface SeriesIndexOverviewInfoProps {
height: number;
showNetwork: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showPreviousAiring: boolean;
showAdded: boolean;
showSeasonCount: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
monitored: boolean;
nextAiring?: string;
network?: string;
qualityProfile: object;
previousAiring?: string;
added?: string;
seasonCount: number;
path: string;
sizeOnDisk?: number;
sortKey: string;
return null;
}
function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
@ -194,6 +214,8 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
const { name, showProp, valueProp } = row;
const isVisible =
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(7053)
props[valueProp] != null && (props[showProp] || props.sortKey === name);
return {
@ -234,6 +256,10 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
const infoRowProps = getInfoRowProps(row, props, uiSettings);
if (infoRowProps == null) {
return null;
}
return <SeriesIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
})}
</div>

@ -1,11 +1,12 @@
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './SeriesIndexOverviewInfoRow.css';
interface SeriesIndexOverviewInfoRowProps {
title?: string;
iconName: object;
label: string;
iconName?: IconDefinition;
label: string | null;
}
function SeriesIndexOverviewInfoRow(props: SeriesIndexOverviewInfoRowProps) {

@ -1,5 +1,5 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -33,11 +33,11 @@ interface RowItemData {
interface SeriesIndexOverviewsProps {
items: Series[];
sortKey?: string;
sortKey: string;
sortDirection?: string;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
@ -79,7 +79,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
const { size: posterSize, detailedProgressBar } = useSelector(
selectOverviewOptions
);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -136,8 +136,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -146,7 +146,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -175,8 +175,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

@ -1,7 +1,8 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const selectOverviewOptions = createSelector(
(state) => state.seriesIndex.overviewOptions,
(state: AppState) => state.seriesIndex.overviewOptions,
(overviewOptions) => overviewOptions
);

@ -42,7 +42,7 @@ function SeriesIndexPosterOptionsModalContent(
const dispatch = useDispatch();
const onPosterOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: unknown }) => {
dispatch(setSeriesPosterOption({ [name]: value }));
},
[dispatch]

@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
import { Statistics } from 'Series/Series';
import SeriesPoster from 'Series/SeriesPoster';
import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@ -52,7 +53,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
path,
titleSlug,
nextAiring,
statistics = {},
statistics = {} as Statistics,
images,
} = series;

@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import useMeasure from 'Helpers/Hooks/useMeasure';
import SortDirection from 'Helpers/Props/SortDirection';
import SeriesIndexPoster from 'Series/Index/Posters/SeriesIndexPoster';
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
const ADDITIONAL_COLUMN_COUNT = {
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
small: 3,
medium: 2,
large: 1,
@ -41,17 +42,17 @@ interface CellItemData {
interface SeriesIndexPostersProps {
items: Series[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const seriesIndexSelector = createSelector(
(state) => state.seriesIndex.posterOptions,
(state: AppState) => state.seriesIndex.posterOptions,
(posterOptions) => {
return {
posterOptions,
@ -108,7 +109,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
} = props;
const { posterOptions } = useSelector(seriesIndexSelector);
const ref: React.MutableRefObject<Grid> = useRef();
const ref = useRef<Grid>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
@ -210,8 +211,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
}, [isSmallScreen, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -220,7 +221,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -243,8 +244,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
const scrollTop = rowIndex * rowHeight + padding;
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current.scrollTo(0, scrollTop);
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
scrollerRef.current?.scrollTo(0, scrollTop);
}
}
}, [

@ -1,7 +1,8 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const selectPosterOptions = createSelector(
(state) => state.seriesIndex.posterOptions,
(state: AppState) => state.seriesIndex.posterOptions,
(posterOptions) => posterOptions
);

@ -2,6 +2,7 @@ import { orderBy } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
@ -11,8 +12,10 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import Series from 'Series/Series';
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import { CheckInputChanged } from 'typings/inputs';
import styles from './DeleteSeriesModalContent.css';
interface DeleteSeriesModalContentProps {
@ -21,7 +24,7 @@ interface DeleteSeriesModalContentProps {
}
const selectDeleteOptions = createSelector(
(state) => state.series.deleteOptions,
(state: AppState) => state.series.deleteOptions,
(deleteOptions) => deleteOptions
);
@ -29,28 +32,28 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
const { seriesIds, onModalClose } = props;
const { addImportListExclusion } = useSelector(selectDeleteOptions);
const allSeries = useSelector(createAllSeriesSelector());
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const dispatch = useDispatch();
const [deleteFiles, setDeleteFiles] = useState(false);
const series = useMemo(() => {
const series = seriesIds.map((id) => {
const series = useMemo((): Series[] => {
const seriesList = seriesIds.map((id) => {
return allSeries.find((s) => s.id === id);
});
}) as Series[];
return orderBy(series, ['sortTitle']);
return orderBy(seriesList, ['sortTitle']);
}, [seriesIds, allSeries]);
const onDeleteFilesChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
setDeleteFiles(value);
},
[setDeleteFiles]
);
const onDeleteOptionChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: boolean }) => {
dispatch(
setDeleteOption({
[name]: value,

@ -54,7 +54,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
const save = useCallback(
(moveFiles) => {
(moveFiles: boolean) => {
let hasChanges = false;
const payload: SavePayload = {};
@ -102,7 +102,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
);
const onInputChange = useCallback(
({ name, value }) => {
({ name, value }: { name: string; value: string }) => {
switch (name) {
case 'monitored':
setMonitored(value);

@ -10,6 +10,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, kinds } from 'Helpers/Props';
import Series from 'Series/Series';
import { executeCommand } from 'Store/Actions/commandActions';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import styles from './OrganizeSeriesModalContent.css';
@ -22,13 +23,19 @@ interface OrganizeSeriesModalContentProps {
function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) {
const { seriesIds, onModalClose } = props;
const allSeries = useSelector(createAllSeriesSelector());
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const dispatch = useDispatch();
const seriesTitles = useMemo(() => {
const series = seriesIds.map((id) => {
return allSeries.find((s) => s.id === id);
});
const series = seriesIds.reduce((acc: Series[], id) => {
const s = allSeries.find((s) => s.id === id);
if (s) {
acc.push(s);
}
return acc;
}, []);
const sorted = orderBy(series, ['sortTitle']);

@ -32,7 +32,7 @@ function ChangeMonitoringModalContent(
const [monitor, setMonitor] = useState(NO_CHANGE);
const onInputChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setMonitor(value);
},
[setMonitor]

@ -18,7 +18,12 @@ function SeasonDetails(props: SeasonDetailsProps) {
return (
<div className={styles.seasons}>
{latestSeasons.map((season) => {
const { seasonNumber, monitored, statistics, isSaving } = season;
const {
seasonNumber,
monitored,
statistics,
isSaving = false,
} = season;
return (
<SeasonPassSeason

@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React, { SyntheticEvent, useCallback } from 'react';
import { useSelect } from 'App/SelectContext';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
@ -15,8 +15,9 @@ function SeriesIndexPosterSelect(props: SeriesIndexPosterSelectProps) {
const isSelected = selectState.selectedState[seriesId];
const onSelectPress = useCallback(
(event) => {
const shiftKey = event.nativeEvent.shiftKey;
(event: SyntheticEvent) => {
const nativeEvent = event.nativeEvent as PointerEvent;
const shiftKey = nativeEvent.shiftKey;
selectDispatch({
type: 'toggleSelected',

@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
interface SeriesIndexSelectAllButtonProps {
label: string;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
}
function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) {

@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { useSelect } from 'App/SelectContext';
import AppState from 'App/State/AppState';
import { RENAME_SERIES } from 'Commands/commandNames';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
@ -22,7 +23,7 @@ import TagsModal from './Tags/TagsModal';
import styles from './SeriesIndexSelectFooter.css';
const seriesEditorSelector = createSelector(
(state) => state.series,
(state: AppState) => state.series,
(series) => {
const { isSaving, isDeleting, deleteError } = series;
@ -71,7 +72,7 @@ function SeriesIndexSelectFooter() {
}, [setIsEditModalOpen]);
const onSavePress = useCallback(
(payload) => {
(payload: any) => {
setIsSavingSeries(true);
setIsEditModalOpen(false);
@ -102,7 +103,7 @@ function SeriesIndexSelectFooter() {
}, [setIsTagsModalOpen]);
const onApplyTagsPress = useCallback(
(tags, applyTags) => {
(tags: number[], applyTags: string) => {
setIsSavingTags(true);
setIsTagsModalOpen(false);
@ -126,7 +127,7 @@ function SeriesIndexSelectFooter() {
}, [setIsMonitoringModalOpen]);
const onMonitoringSavePress = useCallback(
(monitor) => {
(monitor: string) => {
setIsSavingMonitoring(true);
setIsMonitoringModalOpen(false);

@ -7,7 +7,7 @@ interface SeriesIndexSelectModeButtonProps {
label: string;
iconName: IconDefinition;
isSelectMode: boolean;
overflowComponent: React.FunctionComponent;
overflowComponent: React.FunctionComponent<never>;
onPress: () => void;
}

@ -1,6 +1,7 @@
import { concat, uniq } from 'lodash';
import { uniq } from 'lodash';
import React, { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tag } from 'App/State/TagsAppState';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import Series from 'Series/Series';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import styles from './TagsModalContent.css';
@ -25,29 +27,35 @@ interface TagsModalContentProps {
function TagsModalContent(props: TagsModalContentProps) {
const { seriesIds, onModalClose, onApplyTagsPress } = props;
const allSeries = useSelector(createAllSeriesSelector());
const tagList = useSelector(createTagsSelector());
const allSeries: Series[] = useSelector(createAllSeriesSelector());
const tagList: Tag[] = useSelector(createTagsSelector());
const [tags, setTags] = useState<number[]>([]);
const [applyTags, setApplyTags] = useState('add');
const seriesTags = useMemo(() => {
const series = seriesIds.map((id) => {
return allSeries.find((s) => s.id === id);
});
const tags = seriesIds.reduce((acc: number[], id) => {
const s = allSeries.find((s) => s.id === id);
return uniq(concat(...series.map((s) => s.tags)));
if (s) {
acc.push(...s.tags);
}
return acc;
}, []);
return uniq(tags);
}, [seriesIds, allSeries]);
const onTagsChange = useCallback(
({ value }) => {
({ value }: { value: number[] }) => {
setTags(value);
},
[setTags]
);
const onApplyTagsChange = useCallback(
({ value }) => {
({ value }: { value: string }) => {
setApplyTags(value);
},
[setApplyTags]

@ -7,6 +7,8 @@ import React, {
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState';
import { REFRESH_SERIES, RSS_SYNC } from 'Commands/commandNames';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@ -51,7 +53,7 @@ import SeriesIndexTable from './Table/SeriesIndexTable';
import SeriesIndexTableOptions from './Table/SeriesIndexTableOptions';
import styles from './SeriesIndex.css';
function getViewComponent(view) {
function getViewComponent(view: string) {
if (view === 'posters') {
return SeriesIndexPosters;
}
@ -81,7 +83,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
sortKey,
sortDirection,
view,
} = useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex'));
}: SeriesAppState & SeriesIndexAppState & ClientSideCollectionAppState =
useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex'));
const isRefreshingSeries = useSelector(
createCommandExecutingSelector(REFRESH_SERIES)
@ -91,9 +94,11 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
);
const { isSmallScreen } = useSelector(createDimensionsSelector());
const dispatch = useDispatch();
const scrollerRef = useRef<HTMLDivElement>();
const scrollerRef = useRef<HTMLDivElement>(null);
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
undefined
);
const [isSelectMode, setIsSelectMode] = useState(false);
useEffect(() => {
@ -122,14 +127,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
}, [isSelectMode, setIsSelectMode]);
const onTableOptionChange = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setSeriesTableOption(payload));
},
[dispatch]
);
const onViewSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setSeriesView({ view: value }));
if (scrollerRef.current) {
@ -140,14 +145,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
);
const onSortSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setSeriesSort({ sortKey: value }));
},
[dispatch]
);
const onFilterSelect = useCallback(
(value) => {
(value: string) => {
dispatch(setSeriesFilter({ selectedFilterKey: value }));
},
[dispatch]
@ -162,15 +167,15 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
}, [setIsOptionsModalOpen]);
const onJumpBarItemPress = useCallback(
(character) => {
(character: string) => {
setJumpToCharacter(character);
},
[setJumpToCharacter]
);
const onScroll = useCallback(
({ scrollTop }) => {
setJumpToCharacter(null);
({ scrollTop }: { scrollTop: number }) => {
setJumpToCharacter(undefined);
scrollPositions.seriesIndex = scrollTop;
},
[setJumpToCharacter]
@ -184,10 +189,10 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
};
}
const characters = items.reduce((acc, item) => {
const characters = items.reduce((acc: Record<string, number>, item) => {
let char = item.sortTitle.charAt(0);
if (!isNaN(char)) {
if (!isNaN(Number(char))) {
char = '#';
}
@ -305,6 +310,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
<PageContentBody
ref={scrollerRef}
className={styles.contentBody}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
innerClassName={styles[`${view}InnerContentBody`]}
initialScrollTop={props.initialScrollTop}
onScroll={onScroll}

@ -1,12 +1,13 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setSeriesFilter } from 'Store/Actions/seriesIndexActions';
function createSeriesSelector() {
return createSelector(
(state) => state.series.items,
(state: AppState) => state.series.items,
(series) => {
return series;
}
@ -15,14 +16,20 @@ function createSeriesSelector() {
function createFilterBuilderPropsSelector() {
return createSelector(
(state) => state.seriesIndex.filterBuilderProps,
(state: AppState) => state.seriesIndex.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
export default function SeriesIndexFilterModal(props) {
interface SeriesIndexFilterModalProps {
isOpen: boolean;
}
export default function SeriesIndexFilterModal(
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createSeriesSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'series';
@ -30,7 +37,7 @@ export default function SeriesIndexFilterModal(props) {
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setSeriesFilter(payload));
},
[dispatch]
@ -38,6 +45,7 @@ export default function SeriesIndexFilterModal(props) {
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

@ -3,6 +3,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
import SeriesAppState from 'App/State/SeriesAppState';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
@ -13,7 +14,7 @@ import styles from './SeriesIndexFooter.css';
function createUnoptimizedSelector() {
return createSelector(
createClientSideCollectionSelector('series', 'seriesIndex'),
(series) => {
(series: SeriesAppState) => {
return series.items.map((s) => {
const { monitored, status, statistics } = s;
@ -45,7 +46,9 @@ export default function SeriesIndexFooter() {
let totalFileSize = 0;
series.forEach((s) => {
const { statistics = {} } = s;
const {
statistics = { episodeCount: 0, episodeFileCount: 0, sizeOnDisk: 0 },
} = s;
const {
episodeCount = 0,

@ -17,9 +17,11 @@ import { icons } from 'Helpers/Props';
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
import createSeriesIndexItemSelector from 'Series/Index/createSeriesIndexItemSelector';
import { Statistics } from 'Series/Series';
import SeriesBanner from 'Series/SeriesBanner';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import { executeCommand } from 'Store/Actions/commandActions';
import { SelectStateInputProps } from 'typings/props';
import formatBytes from 'Utilities/Number/formatBytes';
import titleCase from 'Utilities/String/titleCase';
import SeriesIndexProgressBar from '../ProgressBar/SeriesIndexProgressBar';
@ -58,7 +60,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
nextAiring,
previousAiring,
added,
statistics = {},
statistics = {} as Statistics,
seasonFolder,
images,
seriesType,
@ -137,7 +139,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
}, []);
const onSelectedChange = useCallback(
({ id, value, shiftKey }) => {
({ id, value, shiftKey }: SelectStateInputProps) => {
selectDispatch({
type: 'toggleSelected',
id,
@ -247,6 +249,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
if (name === 'nextAiring') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@ -258,6 +262,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
if (name === 'previousAiring') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}
@ -269,6 +275,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
if (name === 'added') {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
key={name}
className={styles[name]}

@ -1,8 +1,9 @@
import { throttle } from 'lodash';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Scroller from 'Components/Scroller/Scroller';
import Column from 'Components/Table/Column';
import useMeasure from 'Helpers/Hooks/useMeasure';
@ -30,17 +31,17 @@ interface RowItemData {
interface SeriesIndexTableProps {
items: Series[];
sortKey?: string;
sortKey: string;
sortDirection?: SortDirection;
jumpToCharacter?: string;
scrollTop?: number;
scrollerRef: React.MutableRefObject<HTMLElement>;
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
}
const columnsSelector = createSelector(
(state) => state.seriesIndex.columns,
(state: AppState) => state.seriesIndex.columns,
(columns) => columns
);
@ -92,7 +93,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
const columns = useSelector(columnsSelector);
const { showBanners } = useSelector(selectTableOptions);
const listRef: React.MutableRefObject<List> = useRef();
const listRef = useRef<List<RowItemData>>(null);
const [measureRef, bounds] = useMeasure();
const [size, setSize] = useState({ width: 0, height: 0 });
const windowWidth = window.innerWidth;
@ -103,7 +104,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
}, [showBanners]);
useEffect(() => {
const current = scrollerRef.current as HTMLElement;
const current = scrollerRef?.current as HTMLElement;
if (isSmallScreen) {
setSize({
@ -127,8 +128,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
useEffect(() => {
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
const currentScrollerRef = scrollerRef.current;
const currentScrollerRef = scrollerRef.current as HTMLElement;
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
const handleScroll = throttle(() => {
const { offsetTop = 0 } = currentScrollerRef;
@ -137,7 +138,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
? getWindowScrollTopPosition()
: currentScrollerRef.scrollTop) - offsetTop;
listRef.current.scrollTo(scrollTop);
listRef.current?.scrollTo(scrollTop);
}, 10);
currentScrollListener.addEventListener('scroll', handleScroll);
@ -166,8 +167,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
scrollTop += offset;
}
listRef.current.scrollTo(scrollTop);
scrollerRef.current.scrollTo(0, scrollTop);
listRef.current?.scrollTo(scrollTop);
scrollerRef?.current?.scrollTo(0, scrollTop);
}
}
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);

@ -14,6 +14,7 @@ import {
setSeriesSort,
setSeriesTableOption,
} from 'Store/Actions/seriesIndexActions';
import { CheckInputChanged } from 'typings/inputs';
import hasGrowableColumns from './hasGrowableColumns';
import SeriesIndexTableOptions from './SeriesIndexTableOptions';
import styles from './SeriesIndexTableHeader.css';
@ -32,21 +33,21 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) {
const [selectState, selectDispatch] = useSelect();
const onSortPress = useCallback(
(value) => {
(value: string) => {
dispatch(setSeriesSort({ sortKey: value }));
},
[dispatch]
);
const onTableOptionChange = useCallback(
(payload) => {
(payload: unknown) => {
dispatch(setSeriesTableOption(payload));
},
[dispatch]
);
const onSelectAllChange = useCallback(
({ value }) => {
({ value }: CheckInputChanged) => {
selectDispatch({
type: value ? 'selectAll' : 'unselectAll',
});
@ -94,6 +95,8 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) {
<VirtualTableHeaderCell
key={name}
className={classNames(
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
styles[name],
name === 'sortTitle' && showBanners && styles.banner,
name === 'sortTitle' &&

@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { CheckInputChanged } from 'typings/inputs';
import selectTableOptions from './selectTableOptions';
interface SeriesIndexTableOptionsProps {
@ -18,7 +19,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
const { showBanners, showSearchAction } = tableOptions;
const onTableOptionChangeWrapper = useCallback(
({ name, value }) => {
({ name, value }: CheckInputChanged) => {
onTableOptionChange({
tableOptions: {
...tableOptions,

@ -1,6 +1,8 @@
import Column from 'Components/Table/Column';
const growableColumns = ['network', 'qualityProfileId', 'path', 'tags'];
export default function hasGrowableColumns(columns) {
export default function hasGrowableColumns(columns: Column[]) {
return columns.some((column) => {
const { name, isVisible } = column;

@ -1,7 +1,8 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
const selectTableOptions = createSelector(
(state) => state.seriesIndex.tableOptions,
(state: AppState) => state.seriesIndex.tableOptions,
(tableOptions) => tableOptions
);

@ -1,6 +1,8 @@
import { maxBy } from 'lodash';
import { createSelector } from 'reselect';
import Command from 'Commands/Command';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Series from 'Series/Series';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector';
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
@ -10,25 +12,16 @@ function createSeriesIndexItemSelector(seriesId: number) {
createSeriesSelectorForHook(seriesId),
createSeriesQualityProfileSelector(seriesId),
createExecutingCommandsSelector(),
(series, qualityProfile, executingCommands) => {
// If a series is deleted this selector may fire before the parent
// selectors, which will result in an undefined series, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a series that has no information available.
if (!series) {
return {};
}
(series: Series, qualityProfile, executingCommands: Command[]) => {
const isRefreshingSeries = executingCommands.some((command) => {
return (
command.name === REFRESH_SERIES && command.body.seriesId === series.id
command.name === REFRESH_SERIES && command.body.seriesId === seriesId
);
});
const isSearchingSeries = executingCommands.some((command) => {
return (
command.name === SERIES_SEARCH && command.body.seriesId === series.id
command.name === SERIES_SEARCH && command.body.seriesId === seriesId
);
});

@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
export interface SeriesQueueDetails {
count: number;
@ -10,7 +11,7 @@ function createSeriesQueueDetailsSelector(
seasonNumber?: number
) {
return createSelector(
(state) => state.queue.details.items,
(state: AppState) => state.queue.details.items,
(queueItems) => {
return queueItems.reduce(
(acc: SeriesQueueDetails, item) => {

@ -14,6 +14,7 @@ export interface Language {
}
export interface Statistics {
seasonCount: number;
episodeCount: number;
episodeFileCount: number;
percentOfEpisodes: number;
@ -41,11 +42,12 @@ export interface AlternateTitle {
}
interface Series extends ModelBase {
added: Date;
added: string;
alternateTitles: AlternateTitle[];
certification: string;
cleanTitle: string;
ended: boolean;
firstAired: Date;
firstAired: string;
genres: string[];
images: Image[];
imdbId: string;
@ -54,7 +56,8 @@ interface Series extends ModelBase {
originalLanguage: Language;
overview: string;
path: string;
previousAiring: Date;
previousAiring?: string;
nextAiring?: string;
qualityProfileId: number;
ratings: Ratings;
rootFolderPath: string;
@ -73,6 +76,7 @@ interface Series extends ModelBase {
tvRageId: number;
useSceneNumbering: boolean;
year: number;
isSaving?: boolean;
}
export default Series;

@ -1,4 +1,5 @@
import { createSelector } from 'reselect';
import { DownloadClientAppState } from 'App/State/SettingsAppState';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
@ -8,7 +9,7 @@ export default function createEnabledDownloadClientsSelector(
) {
return createSelector(
createSortedSectionSelector('settings.downloadClients', sortByName),
(downloadClients) => {
(downloadClients: DownloadClientAppState) => {
const { isFetching, isPopulated, error, items } = downloadClients;
const clients = items.filter(

@ -5,6 +5,7 @@ export function createSeriesSelectorForHook(seriesId) {
(state) => state.series.itemMap,
(state) => state.series.items,
(itemMap, allSeries) => {
return seriesId ? allSeries[itemMap[seriesId]]: undefined;
}
);

@ -1,5 +0,0 @@
const scrollPositions = {
seriesIndex: 0
};
export default scrollPositions;

@ -0,0 +1,5 @@
const scrollPositions: Record<string, number> = {
seriesIndex: 0,
};
export default scrollPositions;

@ -1,15 +0,0 @@
import _ from 'lodash';
function getSelectedIds(selectedState, { parseIds = true } = {}) {
return _.reduce(selectedState, (result, value, id) => {
if (value) {
const parsedId = parseIds ? parseInt(id) : id;
result.push(parsedId);
}
return result;
}, []);
}
export default getSelectedIds;

@ -0,0 +1,24 @@
import { reduce } from 'lodash';
import { SelectedState } from 'Helpers/Hooks/useSelectState';
// TODO: This needs to handle string IDs as well
function getSelectedIds(
selectedState: SelectedState,
{ parseIds = true } = {}
): number[] {
return reduce(
selectedState,
(result: any[], value, id) => {
if (value) {
const parsedId = parseIds ? parseInt(id) : id;
result.push(parsedId);
}
return result;
},
[]
);
}
export default getSelectedIds;

@ -0,0 +1,12 @@
export interface QualityProfileFormatItem {
format: number;
name: string;
score: number;
}
interface CustomFormat {
id: number;
name: string;
}
export default CustomFormat;

@ -0,0 +1,19 @@
interface MediaInfo {
audioBitrate: number;
audioChannels: number;
audioCodec: string;
audioLanguages: string;
audioStreamCount: number;
videoBitDepth: number;
videoBitrate: number;
videoCodec: string;
videoFps: number;
videoDynamicRange: string;
videoDynamicRangeType: string;
resolution: string;
runTime: string;
scanType: string;
subtitles: string;
}
export default MediaInfo;

@ -0,0 +1,23 @@
import Quality from 'Quality/Quality';
import { QualityProfileFormatItem } from './CustomFormat';
export interface QualityProfileQualityItem {
id?: number;
quality?: Quality;
items: QualityProfileQualityItem[];
allowed: boolean;
name?: string;
}
interface QualityProfile {
name: string;
upgradeAllowed: boolean;
cutoff: number;
items: QualityProfileQualityItem[];
minFormatScore: number;
cutoffFormatScore: number;
formatItems: QualityProfileFormatItem[];
id: number;
}
export default QualityProfile;

@ -0,0 +1,6 @@
export interface UiSettings {
showRelativeDates: boolean;
shortDateFormat: string;
longDateFormat: string;
timeFormat: string;
}

@ -0,0 +1,6 @@
import SortDirection from 'Helpers/Props/SortDirection';
export type SortCallback = (
sortKey: string,
sortDirection: SortDirection
) => void;

@ -0,0 +1,4 @@
export type CheckInputChanged = {
name: string;
value: boolean;
};

@ -0,0 +1,5 @@
export interface SelectStateInputProps {
id: number;
value: boolean;
shiftKey: boolean;
}

@ -7,7 +7,15 @@
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"strict": true,
"esModuleInterop": true,
"typeRoots": ["node_modules/@types", "typings"],
"paths": {

@ -104,6 +104,9 @@
"@babel/preset-env": "7.18.0",
"@babel/preset-react": "7.17.12",
"@babel/preset-typescript": "7.18.6",
"@types/lodash": "4.14.192",
"@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1",
"@types/react-window": "1.8.5",
"@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0",

@ -1422,6 +1422,11 @@
"@types/minimatch" "*"
"@types/node" "*"
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
"@types/hoist-non-react-statics@^3.3.0":
version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
@ -1472,6 +1477,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
"@types/lodash@4.14.192":
version "4.14.192"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
"@types/minimatch@*":
version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
@ -1524,6 +1534,30 @@
hoist-non-react-statics "^3.3.0"
redux "^4.0.0"
"@types/react-router-dom@5.3.3":
version "5.3.3"
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-router" "*"
"@types/react-router@*":
version "5.1.20"
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
dependencies:
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react-text-truncate@0.14.1":
version "0.14.1"
resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"
integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw==
dependencies:
"@types/react" "*"
"@types/react-window@1.8.5":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"

Loading…
Cancel
Save