Typings cleanup and improvements

This commit is contained in:
Mark McDowall 2023-04-04 09:21:34 -07:00
parent 5326a102e2
commit b2c43fb2a6
92 changed files with 1019 additions and 346 deletions

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,6 +3,19 @@ import Episode from 'Episode/Episode';
import Language from 'Language/Language'; import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality'; import { QualityModel } from 'Quality/Quality';
import Series from 'Series/Series'; 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 { interface InteractiveImport extends ModelBase {
path: string; path: string;
@ -18,7 +31,7 @@ interface InteractiveImport extends ModelBase {
episodes: Episode[]; episodes: Episode[];
qualityWeight: number; qualityWeight: number;
customFormats: object[]; customFormats: object[];
rejections: string[]; rejections: Rejection[];
episodeFileId?: number; episodeFileId?: number;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { CustomFilter } from 'App/State/AppState';
import FilterMenu from 'Components/Menu/FilterMenu'; import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal'; 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 { const {
selectedFilterKey, selectedFilterKey,
filters, 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 = { SeriesIndexFilterMenu.defaultProps = {
showCustomFilters: false, showCustomFilters: false,
}; };

View File

@ -1,11 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MenuContent from 'Components/Menu/MenuContent'; import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu'; import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem'; import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import SortDirection from 'Helpers/Props/SortDirection';
function SeriesIndexSortMenu(props) { interface SeriesIndexSortMenuProps {
sortKey?: string;
sortDirection?: SortDirection;
isDisabled: boolean;
onSortSelect(sortKey: string): unknown;
}
function SeriesIndexSortMenu(props: SeriesIndexSortMenuProps) {
const { sortKey, sortDirection, isDisabled, onSortSelect } = props; const { sortKey, sortDirection, isDisabled, onSortSelect } = props;
return ( 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; export default SeriesIndexSortMenu;

View File

@ -1,11 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MenuContent from 'Components/Menu/MenuContent'; import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu'; import ViewMenu from 'Components/Menu/ViewMenu';
import ViewMenuItem from 'Components/Menu/ViewMenuItem'; import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props'; 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; const { view, isDisabled, onViewSelect } = props;
return ( 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; export default SeriesIndexViewMenu;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,8 @@
import { maxBy } from 'lodash'; import { maxBy } from 'lodash';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import Command from 'Commands/Command';
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames'; import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
import Series from 'Series/Series';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector'; import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector';
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector'; import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
@ -10,25 +12,16 @@ function createSeriesIndexItemSelector(seriesId: number) {
createSeriesSelectorForHook(seriesId), createSeriesSelectorForHook(seriesId),
createSeriesQualityProfileSelector(seriesId), createSeriesQualityProfileSelector(seriesId),
createExecutingCommandsSelector(), createExecutingCommandsSelector(),
(series, qualityProfile, executingCommands) => { (series: Series, qualityProfile, executingCommands: Command[]) => {
// 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 {};
}
const isRefreshingSeries = executingCommands.some((command) => { const isRefreshingSeries = executingCommands.some((command) => {
return ( return (
command.name === REFRESH_SERIES && command.body.seriesId === series.id command.name === REFRESH_SERIES && command.body.seriesId === seriesId
); );
}); });
const isSearchingSeries = executingCommands.some((command) => { const isSearchingSeries = executingCommands.some((command) => {
return ( return (
command.name === SERIES_SEARCH && command.body.seriesId === series.id command.name === SERIES_SEARCH && command.body.seriesId === seriesId
); );
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -104,6 +104,9 @@
"@babel/preset-env": "7.18.0", "@babel/preset-env": "7.18.0",
"@babel/preset-react": "7.17.12", "@babel/preset-react": "7.17.12",
"@babel/preset-typescript": "7.18.6", "@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", "@types/react-window": "1.8.5",
"@typescript-eslint/eslint-plugin": "5.48.1", "@typescript-eslint/eslint-plugin": "5.48.1",
"@typescript-eslint/parser": "5.48.0", "@typescript-eslint/parser": "5.48.0",

View File

@ -1422,6 +1422,11 @@
"@types/minimatch" "*" "@types/minimatch" "*"
"@types/node" "*" "@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": "@types/hoist-non-react-statics@^3.3.0":
version "3.3.1" version "3.3.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" 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" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== 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@*": "@types/minimatch@*":
version "5.1.2" version "5.1.2"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
@ -1524,6 +1534,30 @@
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
redux "^4.0.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": "@types/react-window@1.8.5":
version "1.8.5" version "1.8.5"
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1" resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"