Typings cleanup and improvements
This commit is contained in:
parent
5326a102e2
commit
b2c43fb2a6
|
@ -0,0 +1,48 @@
|
|||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppSectionDeleteState {
|
||||
isDeleting: boolean;
|
||||
deleteError: Error;
|
||||
}
|
||||
|
||||
export interface AppSectionSaveState {
|
||||
isSaving: boolean;
|
||||
saveError: Error;
|
||||
}
|
||||
|
||||
export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
schemaError: Error;
|
||||
schema: {
|
||||
items: T[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface AppSectionItemState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
item: T;
|
||||
}
|
||||
|
||||
interface AppSectionState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
items: T[];
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
}
|
||||
|
||||
export default AppSectionState;
|
|
@ -0,0 +1,52 @@
|
|||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from './SeriesAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import TagsAppState from './TagsAppState';
|
||||
|
||||
interface FilterBuilderPropOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface FilterBuilderProp<T> {
|
||||
name: string;
|
||||
label: string;
|
||||
type: string;
|
||||
valueType?: string;
|
||||
optionsSelector?: (items: T[]) => FilterBuilderPropOption[];
|
||||
}
|
||||
|
||||
export interface PropertyFilter {
|
||||
key: string;
|
||||
value: boolean | string | number | string[] | number[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Filter {
|
||||
key: string;
|
||||
label: string;
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface CustomFilter {
|
||||
id: number;
|
||||
type: string;
|
||||
label: string;
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
episodesSelection: EpisodesAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
seriesIndex: SeriesIndexAppState;
|
||||
settings: SettingsAppState;
|
||||
series: SeriesAppState;
|
||||
tags: TagsAppState;
|
||||
queue: QueueAppState;
|
||||
}
|
||||
|
||||
export default AppState;
|
|
@ -0,0 +1,8 @@
|
|||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface ClientSideCollectionAppState {
|
||||
totalItems: number;
|
||||
customFilters: CustomFilter[];
|
||||
}
|
||||
|
||||
export default ClientSideCollectionAppState;
|
|
@ -0,0 +1,10 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { CustomFilter } from './AppState';
|
||||
|
||||
interface CustomFiltersAppState
|
||||
extends AppSectionState<CustomFilter>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export default CustomFiltersAppState;
|
|
@ -0,0 +1,10 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
|
||||
interface EpisodeFilesAppState
|
||||
extends AppSectionState<EpisodeFile>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export default EpisodeFilesAppState;
|
|
@ -0,0 +1,6 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Episode from 'Episode/Episode';
|
||||
|
||||
type EpisodesAppState = AppSectionState<Episode>;
|
||||
|
||||
export default EpisodesAppState;
|
|
@ -0,0 +1,12 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
||||
import ImportMode from '../../InteractiveImport/ImportMode';
|
||||
import InteractiveImport from '../../InteractiveImport/InteractiveImport';
|
||||
|
||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||
originalItems: InteractiveImport[];
|
||||
importMode: ImportMode;
|
||||
recentFolders: RecentFolder[];
|
||||
}
|
||||
|
||||
export default InteractiveImportAppState;
|
|
@ -0,0 +1,53 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
||||
|
||||
export interface StatusMessage {
|
||||
title: string;
|
||||
messages: string[];
|
||||
}
|
||||
|
||||
export interface Queue extends ModelBase {
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
size: number;
|
||||
title: string;
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: string;
|
||||
trackedDownloadState: string;
|
||||
statusMessages: StatusMessage[];
|
||||
errorMessage: string;
|
||||
downloadId: string;
|
||||
protocol: string;
|
||||
downloadClient: string;
|
||||
outputPath: string;
|
||||
episodeHasFile: boolean;
|
||||
seriesId?: number;
|
||||
episodeId?: number;
|
||||
seasonNumber?: number;
|
||||
}
|
||||
|
||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||
params: unknown;
|
||||
}
|
||||
|
||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
||||
isGrabbing: boolean;
|
||||
grabError: Error;
|
||||
isRemoving: boolean;
|
||||
removeError: Error;
|
||||
}
|
||||
|
||||
interface QueueAppState {
|
||||
status: AppSectionItemState<Queue>;
|
||||
details: QueueDetailsAppState;
|
||||
paged: QueuePagedAppState;
|
||||
}
|
||||
|
||||
export default QueueAppState;
|
|
@ -0,0 +1,62 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import Series from 'Series/Series';
|
||||
import { Filter, FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface SeriesIndexAppState {
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
secondarySortKey: string;
|
||||
secondarySortDirection: SortDirection;
|
||||
view: string;
|
||||
|
||||
posterOptions: {
|
||||
detailedProgressBar: boolean;
|
||||
size: string;
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
overviewOptions: {
|
||||
detailedProgressBar: boolean;
|
||||
size: string;
|
||||
showMonitored: boolean;
|
||||
showNetwork: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showPreviousAiring: boolean;
|
||||
showAdded: boolean;
|
||||
showSeasonCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
tableOptions: {
|
||||
showBanners: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
selectedFilterKey: string;
|
||||
filterBuilderProps: FilterBuilderProp<Series>[];
|
||||
filters: Filter[];
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface SeriesAppState
|
||||
extends AppSectionState<Series>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
|
||||
deleteOptions: {
|
||||
addImportListExclusion: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default SeriesAppState;
|
|
@ -0,0 +1,28 @@
|
|||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSchemaState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Language from 'Language/Language';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
AppSectionDeleteState {}
|
||||
|
||||
export interface QualityProfilesAppState
|
||||
extends AppSectionState<QualityProfile>,
|
||||
AppSectionSchemaState<QualityProfile> {}
|
||||
|
||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||
export type UiSettingsAppState = AppSectionState<UiSettings>;
|
||||
|
||||
interface SettingsAppState {
|
||||
downloadClients: DownloadClientAppState;
|
||||
language: LanguageSettingsAppState;
|
||||
uiSettings: UiSettingsAppState;
|
||||
qualityProfiles: QualityProfilesAppState;
|
||||
}
|
||||
|
||||
export default SettingsAppState;
|
|
@ -0,0 +1,12 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
} from 'App/State/AppSectionState';
|
||||
|
||||
export interface Tag extends ModelBase {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
|
||||
|
||||
export default TagsAppState;
|
|
@ -0,0 +1,37 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface CommandBody {
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
completionMessage: string;
|
||||
requiresDiskAccess: boolean;
|
||||
isExclusive: boolean;
|
||||
isLongRunning: boolean;
|
||||
name: string;
|
||||
lastExecutionTime: string;
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
name: string;
|
||||
commandName: string;
|
||||
message: string;
|
||||
body: CommandBody;
|
||||
priority: string;
|
||||
status: string;
|
||||
result: string;
|
||||
queued: string;
|
||||
started: string;
|
||||
ended: string;
|
||||
duration: string;
|
||||
trigger: string;
|
||||
stateChangeTime: string;
|
||||
sendUpdatesToClient: boolean;
|
||||
updateScheduledTask: boolean;
|
||||
lastExecutionTime: string;
|
||||
}
|
||||
|
||||
export default Command;
|
|
@ -23,7 +23,9 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
|||
info,
|
||||
} = props;
|
||||
|
||||
const [detailedError, setDetailedError] = useState(null);
|
||||
const [detailedError, setDetailedError] = useState<
|
||||
StackTrace.StackFrame[] | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import classNames from 'classnames';
|
||||
import React, { ComponentClass, FunctionComponent, useCallback } from 'react';
|
||||
import React, {
|
||||
ComponentClass,
|
||||
FunctionComponent,
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import styles from './Link.css';
|
||||
|
||||
|
@ -17,7 +22,7 @@ export interface LinkProps extends React.HTMLProps<HTMLAnchorElement> {
|
|||
target?: string;
|
||||
isDisabled?: boolean;
|
||||
noRouter?: boolean;
|
||||
onPress?(event: Event): void;
|
||||
onPress?(event: SyntheticEvent): void;
|
||||
}
|
||||
function Link(props: LinkProps) {
|
||||
const {
|
||||
|
@ -33,7 +38,7 @@ function Link(props: LinkProps) {
|
|||
} = props;
|
||||
|
||||
const onClick = useCallback(
|
||||
(event) => {
|
||||
(event: SyntheticEvent) => {
|
||||
if (!isDisabled && onPress) {
|
||||
onPress(event);
|
||||
}
|
||||
|
@ -57,6 +62,8 @@ function Link(props: LinkProps) {
|
|||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
linkProps.target = target;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import React, { ForwardedRef, forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller, { OnScroll } from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
@ -9,14 +9,11 @@ interface PageContentBodyProps {
|
|||
innerClassName: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const PageContentBody = forwardRef(
|
||||
(
|
||||
props: PageContentBodyProps,
|
||||
ref: React.MutableRefObject<HTMLDivElement>
|
||||
) => {
|
||||
(props: PageContentBodyProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className = styles.contentBody,
|
||||
innerClassName = styles.innerContentBody,
|
||||
|
@ -26,7 +23,7 @@ const PageContentBody = forwardRef(
|
|||
} = props;
|
||||
|
||||
const onScrollWrapper = useCallback(
|
||||
(payload) => {
|
||||
(payload: OnScroll) => {
|
||||
if (onScroll && !isLocked()) {
|
||||
onScroll(payload);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||
import React, {
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
MutableRefObject,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
export interface OnScroll {
|
||||
scrollLeft: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollerProps {
|
||||
className?: string;
|
||||
scrollDirection?: ScrollDirection;
|
||||
|
@ -12,11 +24,11 @@ interface ScrollerProps {
|
|||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
onScroll?: (payload) => void;
|
||||
onScroll?: (payload: OnScroll) => void;
|
||||
}
|
||||
|
||||
const Scroller = forwardRef(
|
||||
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||
(props: ScrollerProps, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const {
|
||||
className,
|
||||
autoFocus = false,
|
||||
|
@ -30,7 +42,7 @@ const Scroller = forwardRef(
|
|||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = ref ?? internalRef;
|
||||
const currentRef = (ref as MutableRefObject<HTMLDivElement>) ?? internalRef;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import React from 'react';
|
||||
|
||||
interface Column {
|
||||
name: string;
|
||||
label: string;
|
||||
columnLabel: string;
|
||||
isSortable: boolean;
|
||||
label: string | React.ReactNode;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router-dom';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
interface WrappedComponentProps {
|
||||
initialScrollTop: number;
|
||||
}
|
||||
|
||||
interface ScrollPositionProps {
|
||||
history: RouteComponentProps['history'];
|
||||
location: RouteComponentProps['location'];
|
||||
match: RouteComponentProps['match'];
|
||||
}
|
||||
|
||||
function withScrollPosition(
|
||||
WrappedComponent: React.FC<WrappedComponentProps>,
|
||||
scrollPositionKey: string
|
||||
) {
|
||||
function ScrollPosition(props: ScrollPositionProps) {
|
||||
const { history } = props;
|
||||
|
||||
const initialScrollTop =
|
||||
history.action === 'POP' ||
|
||||
(history.location.state && history.location.state.restoreScrollPosition)
|
||||
? scrollPositions[scrollPositionKey]
|
||||
: 0;
|
||||
history.action === 'POP' ? scrollPositions[scrollPositionKey] : 0;
|
||||
|
||||
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import ModelBase from 'App/ModelBase';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import MediaInfo from 'typings/MediaInfo';
|
||||
|
||||
export interface EpisodeFile extends ModelBase {
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
relativePath: string;
|
||||
path: string;
|
||||
size: number;
|
||||
dateAdded: string;
|
||||
sceneName: string;
|
||||
releaseGroup: string;
|
||||
languages: CustomFormat[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
mediaInfo: MediaInfo;
|
||||
qualityCutoffNotMet: boolean;
|
||||
}
|
|
@ -5,7 +5,7 @@ import areAllSelected from 'Utilities/Table/areAllSelected';
|
|||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
|
||||
type SelectedState = Record<number, boolean>;
|
||||
export type SelectedState = Record<number, boolean>;
|
||||
|
||||
export interface SelectState {
|
||||
selectedState: SelectedState;
|
||||
|
|
|
@ -7,8 +7,8 @@ import SelectEpisodeModalContent, {
|
|||
interface SelectEpisodeModalProps {
|
||||
isOpen: boolean;
|
||||
selectedIds: number[] | string[];
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
seriesId?: number;
|
||||
seasonNumber?: number;
|
||||
selectedDetails?: string;
|
||||
isAnime: boolean;
|
||||
modalTitle: string;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EpisodesAppState from 'App/State/EpisodesAppState';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
@ -14,12 +15,15 @@ import TableBody from 'Components/Table/TableBody';
|
|||
import Episode from 'Episode/Episode';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
clearEpisodes,
|
||||
fetchEpisodes,
|
||||
setEpisodesSort,
|
||||
} from 'Store/Actions/episodeSelectionActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import SelectEpisodeRow from './SelectEpisodeRow';
|
||||
|
@ -47,7 +51,7 @@ const columns = [
|
|||
function episodesSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('episodeSelection'),
|
||||
(episodes) => {
|
||||
(episodes: EpisodesAppState) => {
|
||||
return episodes;
|
||||
}
|
||||
);
|
||||
|
@ -60,8 +64,8 @@ export interface SelectedEpisode {
|
|||
|
||||
interface SelectEpisodeModalContentProps {
|
||||
selectedIds: number[] | string[];
|
||||
seriesId: number;
|
||||
seasonNumber: number;
|
||||
seriesId?: number;
|
||||
seasonNumber?: number;
|
||||
selectedDetails?: string;
|
||||
isAnime: boolean;
|
||||
sortKey?: string;
|
||||
|
@ -100,26 +104,26 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
|||
const filterEpisodeNumber = parseInt(filter);
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load episodes');
|
||||
const selectedCount = selectedIds.length;
|
||||
const selectedEpisodesCount = getSelectedIds(selectState).length;
|
||||
const selectedEpisodesCount = getSelectedIds(selectedState).length;
|
||||
const selectionIsValid =
|
||||
selectedEpisodesCount > 0 && selectedEpisodesCount % selectedCount === 0;
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setFilter(value.toLowerCase());
|
||||
},
|
||||
[setFilter]
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey = false }) => {
|
||||
({ id, value, shiftKey = false }: SelectStateInputProps) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
items,
|
||||
|
@ -132,7 +136,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
|||
);
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(newSortKey, newSortDirection) => {
|
||||
(newSortKey: string, newSortDirection: SortDirection) => {
|
||||
dispatch(
|
||||
setEpisodesSort({
|
||||
sortKey: newSortKey,
|
||||
|
@ -144,9 +148,9 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
|||
);
|
||||
|
||||
const onEpisodesSelectWrapper = useCallback(() => {
|
||||
const episodeIds = getSelectedIds(selectedState);
|
||||
const episodeIds: number[] = getSelectedIds(selectedState);
|
||||
|
||||
const selectedEpisodes = items.reduce((acc, item) => {
|
||||
const selectedEpisodes = items.reduce((acc: Episode[], item) => {
|
||||
if (episodeIds.indexOf(item.id) > -1) {
|
||||
acc.push(item);
|
||||
}
|
||||
|
@ -167,7 +171,7 @@ function SelectEpisodeModalContent(props: SelectEpisodeModalContentProps) {
|
|||
);
|
||||
|
||||
return {
|
||||
fileId,
|
||||
fileId: fileId as number,
|
||||
episodes,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import PathInputConnector from 'Components/Form/PathInputConnector';
|
||||
import Icon from 'Components/Icon';
|
||||
|
@ -18,7 +19,6 @@ import {
|
|||
removeRecentFolder,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import RecentFolder from './RecentFolder';
|
||||
import RecentFolderRow from './RecentFolderRow';
|
||||
import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||
|
||||
|
@ -49,9 +49,9 @@ function InteractiveImportSelectFolderModalContent(
|
|||
const { modalTitle, onFolderSelect, onModalClose } = props;
|
||||
const [folder, setFolder] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const recentFolders: RecentFolder[] = useSelector(
|
||||
const recentFolders = useSelector(
|
||||
createSelector(
|
||||
(state) => state.interactiveImport.recentFolders,
|
||||
(state: AppState) => state.interactiveImport.recentFolders,
|
||||
(recentFolders) => {
|
||||
return recentFolders;
|
||||
}
|
||||
|
@ -59,14 +59,14 @@ function InteractiveImportSelectFolderModalContent(
|
|||
);
|
||||
|
||||
const onPathChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setFolder(value);
|
||||
},
|
||||
[setFolder]
|
||||
);
|
||||
|
||||
const onRecentPathPress = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
setFolder(value);
|
||||
},
|
||||
[setFolder]
|
||||
|
@ -91,8 +91,8 @@ function InteractiveImportSelectFolderModalContent(
|
|||
}, [folder, onFolderSelect, dispatch]);
|
||||
|
||||
const onRemoveRecentFolderPress = useCallback(
|
||||
(f) => {
|
||||
dispatch(removeRecentFolder({ folder: f }));
|
||||
(folderToRemove: string) => {
|
||||
dispatch(removeRecentFolder({ folder: folderToRemove }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
enum ImportMode {
|
||||
Auto = 'auto',
|
||||
Move = 'move',
|
||||
Copy = 'copy',
|
||||
}
|
||||
type ImportMode = 'auto' | 'move' | 'copy' | 'chooseImportMode';
|
||||
|
||||
export default ImportMode;
|
||||
|
|
|
@ -2,6 +2,8 @@ import { cloneDeep, without } from 'lodash';
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Icon from 'Components/Icon';
|
||||
|
@ -20,16 +22,24 @@ import ModalHeader from 'Components/Modal/ModalHeader';
|
|||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { EpisodeFile } from 'EpisodeFile/EpisodeFile';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { align, icons, kinds, scrollDirections } from 'Helpers/Props';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||
import ImportMode from 'InteractiveImport/ImportMode';
|
||||
import InteractiveImport, {
|
||||
InteractiveImportCommandOptions,
|
||||
} from 'InteractiveImport/InteractiveImport';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectReleaseGroupModal from 'InteractiveImport/ReleaseGroup/SelectReleaseGroupModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
deleteEpisodeFiles,
|
||||
|
@ -44,6 +54,8 @@ import {
|
|||
updateInteractiveImportItems,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SortCallback } from 'typings/callbacks';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
|
@ -59,6 +71,13 @@ type SelectType =
|
|||
| 'quality'
|
||||
| 'language';
|
||||
|
||||
type FilterExistingFiles = 'all' | 'new';
|
||||
|
||||
// TODO: This feels janky to do, but not sure of a better way currently
|
||||
type OnSelectedChangeCallback = React.ComponentProps<
|
||||
typeof InteractiveImportRow
|
||||
>['onSelectedChange'];
|
||||
|
||||
const COLUMNS = [
|
||||
{
|
||||
name: 'relativePath',
|
||||
|
@ -125,25 +144,23 @@ const COLUMNS = [
|
|||
},
|
||||
];
|
||||
|
||||
const filterExistingFilesOptions = {
|
||||
ALL: 'all',
|
||||
NEW: 'new',
|
||||
};
|
||||
|
||||
const importModeOptions = [
|
||||
{ key: 'chooseImportMode', value: 'Choose Import Mode', disabled: true },
|
||||
{ key: 'move', value: 'Move Files' },
|
||||
{ key: 'copy', value: 'Hardlink/Copy Files' },
|
||||
];
|
||||
|
||||
function isSameEpisodeFile(file, originalFile) {
|
||||
function isSameEpisodeFile(
|
||||
file: InteractiveImport,
|
||||
originalFile?: InteractiveImport
|
||||
) {
|
||||
const { series, seasonNumber, episodes } = file;
|
||||
|
||||
if (!originalFile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!originalFile.series || series.id !== originalFile.series.id) {
|
||||
if (!originalFile.series || series?.id !== originalFile.series.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -155,8 +172,8 @@ function isSameEpisodeFile(file, originalFile) {
|
|||
}
|
||||
|
||||
const episodeFilesInfoSelector = createSelector(
|
||||
(state) => state.episodeFiles.isDeleting,
|
||||
(state) => state.episodeFiles.deleteError,
|
||||
(state: AppState) => state.episodeFiles.isDeleting,
|
||||
(state: AppState) => state.episodeFiles.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
|
@ -166,7 +183,7 @@ const episodeFilesInfoSelector = createSelector(
|
|||
);
|
||||
|
||||
const importModeSelector = createSelector(
|
||||
(state) => state.interactiveImport.importMode,
|
||||
(state: AppState) => state.interactiveImport.importMode,
|
||||
(importMode) => {
|
||||
return importMode;
|
||||
}
|
||||
|
@ -178,7 +195,6 @@ interface InteractiveImportModalContentProps {
|
|||
seasonNumber?: number;
|
||||
showSeries?: boolean;
|
||||
allowSeriesChange?: boolean;
|
||||
autoSelectRow?: boolean;
|
||||
showDelete?: boolean;
|
||||
showImportMode?: boolean;
|
||||
showFilterExistingFiles?: boolean;
|
||||
|
@ -200,7 +216,6 @@ function InteractiveImportModalContent(
|
|||
seriesId,
|
||||
seasonNumber,
|
||||
allowSeriesChange = true,
|
||||
autoSelectRow = true,
|
||||
showSeries = true,
|
||||
showFilterExistingFiles = false,
|
||||
showDelete = false,
|
||||
|
@ -221,16 +236,18 @@ function InteractiveImportModalContent(
|
|||
originalItems,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
} = useSelector(createClientSideCollectionSelector('interactiveImport'));
|
||||
}: InteractiveImportAppState = useSelector(
|
||||
createClientSideCollectionSelector('interactiveImport')
|
||||
);
|
||||
|
||||
const { isDeleting, deleteError } = useSelector(episodeFilesInfoSelector);
|
||||
const importMode = useSelector(importModeSelector);
|
||||
|
||||
const [invalidRowsSelected, setInvalidRowsSelected] = useState([]);
|
||||
const [invalidRowsSelected, setInvalidRowsSelected] = useState<number[]>([]);
|
||||
const [
|
||||
withoutEpisodeFileIdRowsSelected,
|
||||
setWithoutEpisodeFileIdRowsSelected,
|
||||
] = useState([]);
|
||||
] = useState<number[]>([]);
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
);
|
||||
|
@ -253,16 +270,20 @@ function InteractiveImportModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const columns: Column[] = useMemo(() => {
|
||||
const result = cloneDeep(COLUMNS);
|
||||
const result: Column[] = cloneDeep(COLUMNS);
|
||||
|
||||
if (!showSeries) {
|
||||
result.find((c) => c.name === 'series').isVisible = false;
|
||||
const seriesColumn = result.find((c) => c.name === 'series');
|
||||
|
||||
if (seriesColumn) {
|
||||
seriesColumn.isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [showSeries]);
|
||||
|
||||
const selectedIds = useMemo(() => {
|
||||
const selectedIds: number[] = useMemo(() => {
|
||||
return getSelectedIds(selectedState);
|
||||
}, [selectedState]);
|
||||
|
||||
|
@ -317,13 +338,13 @@ function InteractiveImportModalContent(
|
|||
}, [previousIsDeleting, isDeleting, deleteError, onModalClose]);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: SelectStateInputProps) => {
|
||||
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
|
||||
},
|
||||
[items, setSelectState]
|
||||
);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
const onSelectedChange = useCallback<OnSelectedChangeCallback>(
|
||||
({ id, value, hasEpisodeFileId, shiftKey = false }) => {
|
||||
setSelectState({
|
||||
type: 'toggleSelected',
|
||||
|
@ -365,7 +386,7 @@ function InteractiveImportModalContent(
|
|||
const onConfirmDelete = useCallback(() => {
|
||||
setIsConfirmDeleteModalOpen(false);
|
||||
|
||||
const episodeFileIds = items.reduce((acc, item) => {
|
||||
const episodeFileIds = items.reduce((acc: number[], item) => {
|
||||
if (selectedIds.indexOf(item.id) > -1 && item.episodeFileId) {
|
||||
acc.push(item.episodeFileId);
|
||||
}
|
||||
|
@ -381,11 +402,10 @@ function InteractiveImportModalContent(
|
|||
}, [setIsConfirmDeleteModalOpen]);
|
||||
|
||||
const onImportSelectedPress = useCallback(() => {
|
||||
const finalImportMode =
|
||||
downloadId || !showImportMode ? ImportMode.Auto : importMode;
|
||||
const finalImportMode = downloadId || !showImportMode ? 'auto' : importMode;
|
||||
|
||||
const existingFiles = [];
|
||||
const files = [];
|
||||
const existingFiles: Partial<EpisodeFile>[] = [];
|
||||
const files: InteractiveImportCommandOptions[] = [];
|
||||
|
||||
if (finalImportMode === 'chooseImportMode') {
|
||||
setInteractiveImportErrorMessage('An import mode must be selected');
|
||||
|
@ -511,16 +531,18 @@ function InteractiveImportModalContent(
|
|||
dispatch,
|
||||
]);
|
||||
|
||||
const onSortPress = useCallback(
|
||||
const onSortPress = useCallback<SortCallback>(
|
||||
(sortKey, sortDirection) => {
|
||||
dispatch(setInteractiveImportSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterExistingFilesChange = useCallback(
|
||||
const onFilterExistingFilesChange = useCallback<
|
||||
(value: FilterExistingFiles) => void
|
||||
>(
|
||||
(value) => {
|
||||
const filter = value !== filterExistingFilesOptions.ALL;
|
||||
const filter = value !== 'all';
|
||||
|
||||
setFilterExistingFiles(filter);
|
||||
|
||||
|
@ -536,14 +558,18 @@ function InteractiveImportModalContent(
|
|||
[downloadId, seriesId, folder, setFilterExistingFiles, dispatch]
|
||||
);
|
||||
|
||||
const onImportModeChange = useCallback(
|
||||
const onImportModeChange = useCallback<
|
||||
({ value }: { value: ImportMode }) => void
|
||||
>(
|
||||
({ value }) => {
|
||||
dispatch(setInteractiveImportMode({ importMode: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectModalSelect = useCallback(
|
||||
const onSelectModalSelect = useCallback<
|
||||
({ value }: { value: SelectType }) => void
|
||||
>(
|
||||
({ value }) => {
|
||||
setSelectModalOpen(value);
|
||||
},
|
||||
|
@ -555,7 +581,7 @@ function InteractiveImportModalContent(
|
|||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSeriesSelect = useCallback(
|
||||
(series) => {
|
||||
(series: Series) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -573,7 +599,7 @@ function InteractiveImportModalContent(
|
|||
);
|
||||
|
||||
const onSeasonSelect = useCallback(
|
||||
(seasonNumber) => {
|
||||
(seasonNumber: number) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -590,7 +616,7 @@ function InteractiveImportModalContent(
|
|||
);
|
||||
|
||||
const onEpisodesSelect = useCallback(
|
||||
(episodes) => {
|
||||
(episodes: SelectedEpisode[]) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -606,7 +632,7 @@ function InteractiveImportModalContent(
|
|||
);
|
||||
|
||||
const onReleaseGroupSelect = useCallback(
|
||||
(releaseGroup) => {
|
||||
(releaseGroup: string) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -622,7 +648,7 @@ function InteractiveImportModalContent(
|
|||
);
|
||||
|
||||
const onLanguagesSelect = useCallback(
|
||||
(newLanguages) => {
|
||||
(newLanguages: Language[]) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -638,7 +664,7 @@ function InteractiveImportModalContent(
|
|||
);
|
||||
|
||||
const onQualitySelect = useCallback(
|
||||
(quality) => {
|
||||
(quality: QualityModel) => {
|
||||
dispatch(
|
||||
updateInteractiveImportItems({
|
||||
ids: selectedIds,
|
||||
|
@ -653,7 +679,7 @@ function InteractiveImportModalContent(
|
|||
[selectedIds, dispatch]
|
||||
);
|
||||
|
||||
const orderedSelectedIds = items.reduce((acc, file) => {
|
||||
const orderedSelectedIds = items.reduce((acc: number[], file) => {
|
||||
if (selectedIds.includes(file.id)) {
|
||||
acc.push(file.id);
|
||||
}
|
||||
|
@ -690,7 +716,7 @@ function InteractiveImportModalContent(
|
|||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
name={'all'}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
|
@ -698,7 +724,7 @@ function InteractiveImportModalContent(
|
|||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
name={'new'}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={onFilterExistingFilesChange}
|
||||
>
|
||||
|
@ -733,7 +759,6 @@ function InteractiveImportModalContent(
|
|||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
allowSeriesChange={allowSeriesChange}
|
||||
autoSelectRow={autoSelectRow}
|
||||
columns={columns}
|
||||
modalTitle={modalTitle}
|
||||
onSelectedChange={onSelectedChange}
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
reprocessInteractiveImportItems,
|
||||
updateInteractiveImportItem,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import Rejection from 'typings/Rejection';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import InteractiveImportRowCellPlaceholder from './InteractiveImportRowCellPlaceholder';
|
||||
|
@ -40,6 +41,10 @@ type SelectType =
|
|||
| 'quality'
|
||||
| 'language';
|
||||
|
||||
type SelectedChangeProps = SelectStateInputProps & {
|
||||
hasEpisodeFileId: boolean;
|
||||
};
|
||||
|
||||
interface InteractiveImportRowProps {
|
||||
id: number;
|
||||
allowSeriesChange: boolean;
|
||||
|
@ -58,7 +63,7 @@ interface InteractiveImportRowProps {
|
|||
isReprocessing?: boolean;
|
||||
isSelected?: boolean;
|
||||
modalTitle: string;
|
||||
onSelectedChange(...args: unknown[]): void;
|
||||
onSelectedChange(result: SelectedChangeProps): void;
|
||||
onValidRowChange(id: number, isValid: boolean): void;
|
||||
}
|
||||
|
||||
|
@ -88,7 +93,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const isSeriesColumnVisible = useMemo(
|
||||
() => columns.find((c) => c.name === 'series').isVisible,
|
||||
() => columns.find((c) => c.name === 'series')?.isVisible ?? false,
|
||||
[columns]
|
||||
);
|
||||
|
||||
|
@ -110,6 +115,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true,
|
||||
shiftKey: false,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
@ -143,7 +149,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
]);
|
||||
|
||||
const onSelectedChangeWrapper = useCallback(
|
||||
(result) => {
|
||||
(result: SelectedChangeProps) => {
|
||||
onSelectedChange({
|
||||
...result,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
|
@ -158,6 +164,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
id,
|
||||
hasEpisodeFileId: !!episodeFileId,
|
||||
value: true,
|
||||
shiftKey: false,
|
||||
});
|
||||
}
|
||||
}, [id, episodeFileId, isSelected, onSelectedChange]);
|
||||
|
@ -312,9 +319,10 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
);
|
||||
});
|
||||
|
||||
const requiresSeasonNumber = isNaN(Number(seasonNumber));
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder =
|
||||
isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
|
||||
isSelected && !!series && requiresSeasonNumber && !isReprocessing;
|
||||
const showEpisodeNumbersPlaceholder =
|
||||
isSelected && Number.isInteger(seasonNumber) && !episodes.length;
|
||||
const showReleaseGroupPlaceholder = isSelected && !releaseGroup;
|
||||
|
@ -364,9 +372,11 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
</TableRowCellButton>
|
||||
|
||||
<TableRowCellButton
|
||||
isDisabled={!series || isNaN(seasonNumber)}
|
||||
isDisabled={!series || requiresSeasonNumber}
|
||||
title={
|
||||
series && !isNaN(seasonNumber) ? 'Click to change episode' : undefined
|
||||
series && !requiresSeasonNumber
|
||||
? 'Click to change episode'
|
||||
: undefined
|
||||
}
|
||||
onPress={onSelectEpisodePress}
|
||||
>
|
||||
|
@ -456,7 +466,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
|
||||
<SelectSeasonModal
|
||||
isOpen={selectModalOpen === 'season'}
|
||||
seriesId={series && series.id}
|
||||
seriesId={series?.id}
|
||||
modalTitle={modalTitle}
|
||||
onSeasonSelect={onSeasonSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
|
@ -465,7 +475,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
|||
<SelectEpisodeModal
|
||||
isOpen={selectModalOpen === 'episode'}
|
||||
selectedIds={[id]}
|
||||
seriesId={series && series.id}
|
||||
seriesId={series?.id}
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
selectedDetails={relativePath}
|
||||
|
|
|
@ -3,6 +3,19 @@ import Episode from 'Episode/Episode';
|
|||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Series from 'Series/Series';
|
||||
import Rejection from 'typings/Rejection';
|
||||
|
||||
export interface InteractiveImportCommandOptions {
|
||||
path: string;
|
||||
folderName: string;
|
||||
seriesId: number;
|
||||
episodeIds: number[];
|
||||
releaseGroup?: string;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
downloadId?: string;
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
interface InteractiveImport extends ModelBase {
|
||||
path: string;
|
||||
|
@ -18,7 +31,7 @@ interface InteractiveImport extends ModelBase {
|
|||
episodes: Episode[];
|
||||
qualityWeight: number;
|
||||
customFormats: object[];
|
||||
rejections: string[];
|
||||
rejections: Rejection[];
|
||||
episodeFileId?: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,8 @@ function InteractiveImportModal(props: InteractiveImportModalProps) {
|
|||
const previousIsOpen = usePrevious(isOpen);
|
||||
|
||||
const onFolderSelect = useCallback(
|
||||
(f) => {
|
||||
setFolderPath(f);
|
||||
(path: string) => {
|
||||
setFolderPath(path);
|
||||
},
|
||||
[setFolderPath]
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { LanguageSettingsAppState } from 'App/State/SettingsAppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
@ -25,11 +26,12 @@ interface SelectLanguageModalContentProps {
|
|||
|
||||
function createFilteredLanguagesSelector() {
|
||||
return createSelector(createLanguagesSelector(), (languages) => {
|
||||
const { isFetching, isPopulated, error, items } = languages;
|
||||
const { isFetching, isPopulated, error, items } =
|
||||
languages as LanguageSettingsAppState;
|
||||
|
||||
const filterItems = ['Any', 'Original'];
|
||||
const filteredLanguages = items.filter(
|
||||
(lang) => !filterItems.includes(lang.name)
|
||||
(lang: Language) => !filterItems.includes(lang.name)
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -51,7 +53,7 @@ function SelectLanguageModalContent(props: SelectLanguageModalContentProps) {
|
|||
const [languageIds, setLanguageIds] = useState(props.languageIds);
|
||||
|
||||
const onLanguageChange = useCallback(
|
||||
({ value, name }) => {
|
||||
({ name, value }: { name: string; value: boolean }) => {
|
||||
const changedId = parseInt(name);
|
||||
|
||||
let newLanguages = [...languageIds];
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Error } from 'App/State/AppSectionState';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
@ -12,22 +14,32 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import Quality, { QualityModel } from 'Quality/Quality';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
|
||||
function createQualitySchemeSelctor() {
|
||||
interface QualitySchemaState {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
items: Quality[];
|
||||
}
|
||||
|
||||
function createQualitySchemaSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
(state: AppState) => state.settings.qualityProfiles,
|
||||
(qualityProfiles): QualitySchemaState => {
|
||||
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
|
||||
qualityProfiles;
|
||||
|
||||
const items = getQualities(schema.items) as Quality[];
|
||||
|
||||
return {
|
||||
isFetching: isSchemaFetching,
|
||||
isPopulated: isSchemaPopulated,
|
||||
error: schemaError,
|
||||
items: getQualities(schema.items),
|
||||
items,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -50,7 +62,7 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
|||
const [real, setReal] = useState(props.real);
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createQualitySchemeSelctor()
|
||||
createQualitySchemaSelector()
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
@ -72,28 +84,28 @@ function SelectQualityModalContent(props: SelectQualityModalContentProps) {
|
|||
}, [items]);
|
||||
|
||||
const onQualityChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setQualityId(parseInt(value));
|
||||
},
|
||||
[setQualityId]
|
||||
);
|
||||
|
||||
const onProperChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setProper(value);
|
||||
},
|
||||
[setProper]
|
||||
);
|
||||
|
||||
const onRealChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setReal(value);
|
||||
},
|
||||
[setReal]
|
||||
);
|
||||
|
||||
const onQualitySelectWrapper = useCallback(() => {
|
||||
const quality = items.find((item) => item.id === qualityId);
|
||||
const quality = items.find((item) => item.id === qualityId) as Quality;
|
||||
|
||||
const revision = {
|
||||
version: proper ? 2 : 1,
|
||||
|
|
|
@ -25,7 +25,7 @@ function SelectReleaseGroupModalContent(
|
|||
const [releaseGroup, setReleaseGroup] = useState(props.releaseGroup);
|
||||
|
||||
const onReleaseGroupChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setReleaseGroup(value);
|
||||
},
|
||||
[setReleaseGroup]
|
||||
|
|
|
@ -5,8 +5,8 @@ import SelectSeasonModalContent from './SelectSeasonModalContent';
|
|||
interface SelectSeasonModalProps {
|
||||
isOpen: boolean;
|
||||
modalTitle: string;
|
||||
seriesId: number;
|
||||
onSeasonSelect(seasonNumber): void;
|
||||
seriesId?: number;
|
||||
onSeasonSelect(seasonNumber: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,20 +5,21 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { Season } from 'Series/Series';
|
||||
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
|
||||
import SelectSeasonRow from './SelectSeasonRow';
|
||||
|
||||
interface SelectSeasonModalContentProps {
|
||||
seriesId: number;
|
||||
seriesId?: number;
|
||||
modalTitle: string;
|
||||
onSeasonSelect(seasonNumber): void;
|
||||
onSeasonSelect(seasonNumber: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectSeasonModalContent(props: SelectSeasonModalContentProps) {
|
||||
const { seriesId, modalTitle, onSeasonSelect, onModalClose } = props;
|
||||
const series = useSelector(createSeriesSelectorForHook(seriesId));
|
||||
const seasons = useMemo(() => {
|
||||
const seasons = useMemo<Season[]>(() => {
|
||||
return series.seasons.slice(0).reverse();
|
||||
}, [series]);
|
||||
|
||||
|
|
|
@ -22,11 +22,11 @@ interface SelectSeriesModalContentProps {
|
|||
function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
||||
const { modalTitle, onSeriesSelect, onModalClose } = props;
|
||||
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setFilter(value);
|
||||
},
|
||||
[setFilter]
|
||||
|
@ -34,7 +34,7 @@ function SelectSeriesModalContent(props: SelectSeriesModalContentProps) {
|
|||
|
||||
const onSeriesSelectWrapper = useCallback(
|
||||
(seriesId: number) => {
|
||||
const series = allSeries.find((s) => s.id === seriesId);
|
||||
const series = allSeries.find((s) => s.id === seriesId) as Series;
|
||||
|
||||
onSeriesSelect(series);
|
||||
},
|
||||
|
|
|
@ -15,6 +15,7 @@ import EpisodeQuality from 'Episode/EpisodeQuality';
|
|||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
@ -25,7 +26,11 @@ import ReleaseEpisode from './ReleaseEpisode';
|
|||
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
function getDownloadIcon(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
|
@ -37,7 +42,11 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
|||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
function getDownloadTooltip(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
|
@ -65,7 +74,7 @@ interface InteractiveSearchRowProps {
|
|||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats?: object[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
sceneMapping?: object;
|
||||
seasonNumber?: number;
|
||||
|
|
|
@ -4,7 +4,7 @@ import styles from './SelectDownloadClientRow.css';
|
|||
|
||||
interface SelectSeasonRowProps {
|
||||
id: number;
|
||||
name: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
onDownloadClientSelect(downloadClientId: number): unknown;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ interface OverrideMatchModalProps {
|
|||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError: string;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
|||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||
import { SelectedEpisode } from 'InteractiveImport/Episode/SelectEpisodeModalContent';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||
|
@ -49,7 +50,7 @@ interface OverrideMatchModalContentProps {
|
|||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError: string;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
|
@ -70,7 +71,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
const [episodes, setEpisodes] = useState(props.episodes);
|
||||
const [languages, setLanguages] = useState(props.languages);
|
||||
const [quality, setQuality] = useState(props.quality);
|
||||
const [downloadClientId, setDownloadClientId] = useState(null);
|
||||
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
|
@ -137,7 +138,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
}, [setSelectModalOpen]);
|
||||
|
||||
const onEpisodesSelect = useCallback(
|
||||
(episodeMap) => {
|
||||
(episodeMap: SelectedEpisode[]) => {
|
||||
setEpisodes(episodeMap[0].episodes);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
|
@ -149,7 +150,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
}, [setSelectModalOpen]);
|
||||
|
||||
const onQualitySelect = useCallback(
|
||||
(quality) => {
|
||||
(quality: QualityModel) => {
|
||||
setQuality(quality);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
|
@ -161,7 +162,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
}, [setSelectModalOpen]);
|
||||
|
||||
const onLanguagesSelect = useCallback(
|
||||
(languages) => {
|
||||
(languages: Language[]) => {
|
||||
setLanguages(languages);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
|
@ -173,7 +174,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
}, [setSelectModalOpen]);
|
||||
|
||||
const onDownloadClientSelect = useCallback(
|
||||
(downloadClientId) => {
|
||||
(downloadClientId: number) => {
|
||||
setDownloadClientId(downloadClientId);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
|
@ -264,7 +265,7 @@ function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
|||
data={
|
||||
<OverrideMatchData
|
||||
value={episodeInfo}
|
||||
isDisabled={!series || isNaN(seasonNumber)}
|
||||
isDisabled={!series || isNaN(Number(seasonNumber))}
|
||||
onPress={onSelectEpisodePress}
|
||||
/>
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import { icons, tooltipPositions } from 'Helpers/Props';
|
|||
import styles from './ReleaseSceneIndicator.css';
|
||||
|
||||
function formatReleaseNumber(
|
||||
seasonNumber,
|
||||
episodeNumbers,
|
||||
absoluteEpisodeNumbers
|
||||
seasonNumber: number | undefined,
|
||||
episodeNumbers: number[] | undefined,
|
||||
absoluteEpisodeNumbers: number[] | undefined
|
||||
) {
|
||||
if (episodeNumbers && episodeNumbers.length) {
|
||||
if (episodeNumbers.length > 1) {
|
||||
|
|
|
@ -652,7 +652,6 @@ class SeriesDetails extends Component {
|
|||
initialSortDirection={sortDirections.DESCENDING}
|
||||
showSeries={false}
|
||||
allowSeriesChange={false}
|
||||
autoSelectRow={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={'Manage Episodes'}
|
||||
|
|
|
@ -498,7 +498,6 @@ class SeriesDetailsSeason extends Component {
|
|||
initialSortDirection={sortDirections.DESCENDING}
|
||||
showSeries={false}
|
||||
allowSeriesChange={false}
|
||||
autoSelectRow={false}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={'Manage Episodes'}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { CustomFilter } from 'App/State/AppState';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SeriesIndexFilterModal from 'Series/Index/SeriesIndexFilterModal';
|
||||
|
||||
function SeriesIndexFilterMenu(props) {
|
||||
interface SeriesIndexFilterMenuProps {
|
||||
selectedFilterKey: string | number;
|
||||
filters: object[];
|
||||
customFilters: CustomFilter[];
|
||||
isDisabled: boolean;
|
||||
onFilterSelect(filterName: string): unknown;
|
||||
}
|
||||
|
||||
function SeriesIndexFilterMenu(props: SeriesIndexFilterMenuProps) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
|
@ -26,15 +34,6 @@ function SeriesIndexFilterMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
SeriesIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number])
|
||||
.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
SeriesIndexFilterMenu.defaultProps = {
|
||||
showCustomFilters: false,
|
||||
};
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import { align } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
|
@ -150,11 +157,4 @@ function SeriesIndexSortMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
SeriesIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SeriesIndexSortMenu;
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import ViewMenu from 'Components/Menu/ViewMenu';
|
||||
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function SeriesIndexViewMenu(props) {
|
||||
interface SeriesIndexViewMenuProps {
|
||||
view: string;
|
||||
isDisabled: boolean;
|
||||
onViewSelect(value: string): unknown;
|
||||
}
|
||||
|
||||
function SeriesIndexViewMenu(props: SeriesIndexViewMenuProps) {
|
||||
const { view, isDisabled, onViewSelect } = props;
|
||||
|
||||
return (
|
||||
|
@ -31,10 +36,4 @@ function SeriesIndexViewMenu(props) {
|
|||
);
|
||||
}
|
||||
|
||||
SeriesIndexViewMenu.propTypes = {
|
||||
view: PropTypes.string.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default SeriesIndexViewMenu;
|
||||
|
|
|
@ -45,7 +45,7 @@ function SeriesIndexOverviewOptionsModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const onOverviewOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
dispatch(setSeriesOverviewOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
|||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
|
||||
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
|
||||
import { Statistics } from 'Series/Series';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
|
@ -66,7 +67,7 @@ function SeriesIndexOverview(props: SeriesIndexOverviewProps) {
|
|||
previousAiring,
|
||||
added,
|
||||
overview,
|
||||
statistics = {},
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
network,
|
||||
} = series;
|
||||
|
|
|
@ -1,14 +1,50 @@
|
|||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { UiSettings } from 'typings/UiSettings';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import SeriesIndexOverviewInfoRow from './SeriesIndexOverviewInfoRow';
|
||||
import styles from './SeriesIndexOverviewInfo.css';
|
||||
|
||||
interface RowProps {
|
||||
name: string;
|
||||
showProp: string;
|
||||
valueProp: string;
|
||||
}
|
||||
|
||||
interface RowInfoProps {
|
||||
title: string;
|
||||
iconName: IconDefinition;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface SeriesIndexOverviewInfoProps {
|
||||
height: number;
|
||||
showNetwork: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showPreviousAiring: boolean;
|
||||
showAdded: boolean;
|
||||
showSeasonCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
monitored: boolean;
|
||||
nextAiring?: string;
|
||||
network?: string;
|
||||
qualityProfile: object;
|
||||
previousAiring?: string;
|
||||
added?: string;
|
||||
seasonCount: number;
|
||||
path: string;
|
||||
sizeOnDisk?: number;
|
||||
sortKey: string;
|
||||
}
|
||||
|
||||
const infoRowHeight = parseInt(dimensions.seriesIndexOverviewInfoRowHeight);
|
||||
|
||||
const rows = [
|
||||
|
@ -54,7 +90,11 @@ const rows = [
|
|||
},
|
||||
];
|
||||
|
||||
function getInfoRowProps(row, props, uiSettings) {
|
||||
function getInfoRowProps(
|
||||
row: RowProps,
|
||||
props: SeriesIndexOverviewInfoProps,
|
||||
uiSettings: UiSettings
|
||||
): RowInfoProps | null {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
|
@ -71,7 +111,7 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
return {
|
||||
title: 'Network',
|
||||
iconName: icons.NETWORK,
|
||||
label: props.network,
|
||||
label: props.network ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,6 +119,9 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
return {
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
// TODO: Type QualityProfile
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2339)
|
||||
label: props.qualityProfile.name,
|
||||
};
|
||||
}
|
||||
|
@ -95,15 +138,11 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
timeFormat
|
||||
)}`,
|
||||
iconName: icons.CALENDAR,
|
||||
label: getRelativeDate(
|
||||
previousAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
label:
|
||||
getRelativeDate(previousAiring, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
),
|
||||
}) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -115,10 +154,11 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
return {
|
||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.ADD,
|
||||
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}),
|
||||
label:
|
||||
getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}) ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -154,28 +194,8 @@ function getInfoRowProps(row, props, uiSettings) {
|
|||
label: formatBytes(props.sizeOnDisk),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface SeriesIndexOverviewInfoProps {
|
||||
height: number;
|
||||
showNetwork: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showPreviousAiring: boolean;
|
||||
showAdded: boolean;
|
||||
showSeasonCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
monitored: boolean;
|
||||
nextAiring?: string;
|
||||
network?: string;
|
||||
qualityProfile: object;
|
||||
previousAiring?: string;
|
||||
added?: string;
|
||||
seasonCount: number;
|
||||
path: string;
|
||||
sizeOnDisk?: number;
|
||||
sortKey: string;
|
||||
return null;
|
||||
}
|
||||
|
||||
function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
|
||||
|
@ -194,6 +214,8 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
|
|||
const { name, showProp, valueProp } = row;
|
||||
|
||||
const isVisible =
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(7053)
|
||||
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
||||
|
||||
return {
|
||||
|
@ -234,6 +256,10 @@ function SeriesIndexOverviewInfo(props: SeriesIndexOverviewInfoProps) {
|
|||
|
||||
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||
|
||||
if (infoRowProps == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SeriesIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||
})}
|
||||
</div>
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { IconDefinition } from '@fortawesome/free-regular-svg-icons';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './SeriesIndexOverviewInfoRow.css';
|
||||
|
||||
interface SeriesIndexOverviewInfoRowProps {
|
||||
title?: string;
|
||||
iconName: object;
|
||||
label: string;
|
||||
iconName?: IconDefinition;
|
||||
label: string | null;
|
||||
}
|
||||
|
||||
function SeriesIndexOverviewInfoRow(props: SeriesIndexOverviewInfoRowProps) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
|
@ -33,11 +33,11 @@ interface RowItemData {
|
|||
|
||||
interface SeriesIndexOverviewsProps {
|
||||
items: Series[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: string;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
|
|||
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||
selectOverviewOptions
|
||||
);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const listRef = useRef<List>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -136,8 +136,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
|
|||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -146,7 +146,7 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -175,8 +175,8 @@ function SeriesIndexOverviews(props: SeriesIndexOverviewsProps) {
|
|||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectOverviewOptions = createSelector(
|
||||
(state) => state.seriesIndex.overviewOptions,
|
||||
(state: AppState) => state.seriesIndex.overviewOptions,
|
||||
(overviewOptions) => overviewOptions
|
||||
);
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ function SeriesIndexPosterOptionsModalContent(
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const onPosterOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: unknown }) => {
|
||||
dispatch(setSeriesPosterOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
|
|
@ -10,6 +10,7 @@ import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
|||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import SeriesIndexProgressBar from 'Series/Index/ProgressBar/SeriesIndexProgressBar';
|
||||
import SeriesIndexPosterSelect from 'Series/Index/Select/SeriesIndexPosterSelect';
|
||||
import { Statistics } from 'Series/Series';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
|
@ -52,7 +53,7 @@ function SeriesIndexPoster(props: SeriesIndexPosterProps) {
|
|||
path,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
statistics = {},
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
} = series;
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import SeriesIndexPoster from 'Series/Index/Posters/SeriesIndexPoster';
|
||||
|
@ -21,7 +22,7 @@ const columnPaddingSmallScreen = parseInt(
|
|||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
const ADDITIONAL_COLUMN_COUNT: Record<string, number> = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
|
@ -41,17 +42,17 @@ interface CellItemData {
|
|||
|
||||
interface SeriesIndexPostersProps {
|
||||
items: Series[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const seriesIndexSelector = createSelector(
|
||||
(state) => state.seriesIndex.posterOptions,
|
||||
(state: AppState) => state.seriesIndex.posterOptions,
|
||||
(posterOptions) => {
|
||||
return {
|
||||
posterOptions,
|
||||
|
@ -108,7 +109,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
|||
} = props;
|
||||
|
||||
const { posterOptions } = useSelector(seriesIndexSelector);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const ref = useRef<Grid>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
|
@ -210,8 +211,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
|||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -220,7 +221,7 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -243,8 +244,8 @@ export default function SeriesIndexPosters(props: SeriesIndexPostersProps) {
|
|||
|
||||
const scrollTop = rowIndex * rowHeight + padding;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
ref.current?.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectPosterOptions = createSelector(
|
||||
(state) => state.seriesIndex.posterOptions,
|
||||
(state: AppState) => state.seriesIndex.posterOptions,
|
||||
(posterOptions) => posterOptions
|
||||
);
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { orderBy } from 'lodash';
|
|||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
|
@ -11,8 +12,10 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import { bulkDeleteSeries, setDeleteOption } from 'Store/Actions/seriesActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import styles from './DeleteSeriesModalContent.css';
|
||||
|
||||
interface DeleteSeriesModalContentProps {
|
||||
|
@ -21,7 +24,7 @@ interface DeleteSeriesModalContentProps {
|
|||
}
|
||||
|
||||
const selectDeleteOptions = createSelector(
|
||||
(state) => state.series.deleteOptions,
|
||||
(state: AppState) => state.series.deleteOptions,
|
||||
(deleteOptions) => deleteOptions
|
||||
);
|
||||
|
||||
|
@ -29,28 +32,28 @@ function DeleteSeriesModalContent(props: DeleteSeriesModalContentProps) {
|
|||
const { seriesIds, onModalClose } = props;
|
||||
|
||||
const { addImportListExclusion } = useSelector(selectDeleteOptions);
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const series = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
const series = useMemo((): Series[] => {
|
||||
const seriesList = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
}) as Series[];
|
||||
|
||||
return orderBy(series, ['sortTitle']);
|
||||
return orderBy(seriesList, ['sortTitle']);
|
||||
}, [seriesIds, allSeries]);
|
||||
|
||||
const onDeleteFilesChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[setDeleteFiles]
|
||||
);
|
||||
|
||||
const onDeleteOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: boolean }) => {
|
||||
dispatch(
|
||||
setDeleteOption({
|
||||
[name]: value,
|
||||
|
|
|
@ -54,7 +54,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
|||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const save = useCallback(
|
||||
(moveFiles) => {
|
||||
(moveFiles: boolean) => {
|
||||
let hasChanges = false;
|
||||
const payload: SavePayload = {};
|
||||
|
||||
|
@ -102,7 +102,7 @@ function EditSeriesModalContent(props: EditSeriesModalContentProps) {
|
|||
);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: { name: string; value: string }) => {
|
||||
switch (name) {
|
||||
case 'monitored':
|
||||
setMonitored(value);
|
||||
|
|
|
@ -10,6 +10,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import styles from './OrganizeSeriesModalContent.css';
|
||||
|
@ -22,13 +23,19 @@ interface OrganizeSeriesModalContentProps {
|
|||
function OrganizeSeriesModalContent(props: OrganizeSeriesModalContentProps) {
|
||||
const { seriesIds, onModalClose } = props;
|
||||
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const seriesTitles = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
const series = seriesIds.reduce((acc: Series[], id) => {
|
||||
const s = allSeries.find((s) => s.id === id);
|
||||
|
||||
if (s) {
|
||||
acc.push(s);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const sorted = orderBy(series, ['sortTitle']);
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ function ChangeMonitoringModalContent(
|
|||
const [monitor, setMonitor] = useState(NO_CHANGE);
|
||||
|
||||
const onInputChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setMonitor(value);
|
||||
},
|
||||
[setMonitor]
|
||||
|
|
|
@ -18,7 +18,12 @@ function SeasonDetails(props: SeasonDetailsProps) {
|
|||
return (
|
||||
<div className={styles.seasons}>
|
||||
{latestSeasons.map((season) => {
|
||||
const { seasonNumber, monitored, statistics, isSaving } = season;
|
||||
const {
|
||||
seasonNumber,
|
||||
monitored,
|
||||
statistics,
|
||||
isSaving = false,
|
||||
} = season;
|
||||
|
||||
return (
|
||||
<SeasonPassSeason
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
@ -15,8 +15,9 @@ function SeriesIndexPosterSelect(props: SeriesIndexPosterSelectProps) {
|
|||
const isSelected = selectState.selectedState[seriesId];
|
||||
|
||||
const onSelectPress = useCallback(
|
||||
(event) => {
|
||||
const shiftKey = event.nativeEvent.shiftKey;
|
||||
(event: SyntheticEvent) => {
|
||||
const nativeEvent = event.nativeEvent as PointerEvent;
|
||||
const shiftKey = nativeEvent.shiftKey;
|
||||
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
|
|
|
@ -6,7 +6,7 @@ import { icons } from 'Helpers/Props';
|
|||
interface SeriesIndexSelectAllButtonProps {
|
||||
label: string;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
}
|
||||
|
||||
function SeriesIndexSelectAllButton(props: SeriesIndexSelectAllButtonProps) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { RENAME_SERIES } from 'Commands/commandNames';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
|
@ -22,7 +23,7 @@ import TagsModal from './Tags/TagsModal';
|
|||
import styles from './SeriesIndexSelectFooter.css';
|
||||
|
||||
const seriesEditorSelector = createSelector(
|
||||
(state) => state.series,
|
||||
(state: AppState) => state.series,
|
||||
(series) => {
|
||||
const { isSaving, isDeleting, deleteError } = series;
|
||||
|
||||
|
@ -71,7 +72,7 @@ function SeriesIndexSelectFooter() {
|
|||
}, [setIsEditModalOpen]);
|
||||
|
||||
const onSavePress = useCallback(
|
||||
(payload) => {
|
||||
(payload: any) => {
|
||||
setIsSavingSeries(true);
|
||||
setIsEditModalOpen(false);
|
||||
|
||||
|
@ -102,7 +103,7 @@ function SeriesIndexSelectFooter() {
|
|||
}, [setIsTagsModalOpen]);
|
||||
|
||||
const onApplyTagsPress = useCallback(
|
||||
(tags, applyTags) => {
|
||||
(tags: number[], applyTags: string) => {
|
||||
setIsSavingTags(true);
|
||||
setIsTagsModalOpen(false);
|
||||
|
||||
|
@ -126,7 +127,7 @@ function SeriesIndexSelectFooter() {
|
|||
}, [setIsMonitoringModalOpen]);
|
||||
|
||||
const onMonitoringSavePress = useCallback(
|
||||
(monitor) => {
|
||||
(monitor: string) => {
|
||||
setIsSavingMonitoring(true);
|
||||
setIsMonitoringModalOpen(false);
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ interface SeriesIndexSelectModeButtonProps {
|
|||
label: string;
|
||||
iconName: IconDefinition;
|
||||
isSelectMode: boolean;
|
||||
overflowComponent: React.FunctionComponent;
|
||||
overflowComponent: React.FunctionComponent<never>;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { concat, uniq } from 'lodash';
|
||||
import { uniq } from 'lodash';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Tag } from 'App/State/TagsAppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
@ -12,6 +13,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import Series from 'Series/Series';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
||||
import styles from './TagsModalContent.css';
|
||||
|
@ -25,29 +27,35 @@ interface TagsModalContentProps {
|
|||
function TagsModalContent(props: TagsModalContentProps) {
|
||||
const { seriesIds, onModalClose, onApplyTagsPress } = props;
|
||||
|
||||
const allSeries = useSelector(createAllSeriesSelector());
|
||||
const tagList = useSelector(createTagsSelector());
|
||||
const allSeries: Series[] = useSelector(createAllSeriesSelector());
|
||||
const tagList: Tag[] = useSelector(createTagsSelector());
|
||||
|
||||
const [tags, setTags] = useState<number[]>([]);
|
||||
const [applyTags, setApplyTags] = useState('add');
|
||||
|
||||
const seriesTags = useMemo(() => {
|
||||
const series = seriesIds.map((id) => {
|
||||
return allSeries.find((s) => s.id === id);
|
||||
});
|
||||
const tags = seriesIds.reduce((acc: number[], id) => {
|
||||
const s = allSeries.find((s) => s.id === id);
|
||||
|
||||
return uniq(concat(...series.map((s) => s.tags)));
|
||||
if (s) {
|
||||
acc.push(...s.tags);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return uniq(tags);
|
||||
}, [seriesIds, allSeries]);
|
||||
|
||||
const onTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: number[] }) => {
|
||||
setTags(value);
|
||||
},
|
||||
[setTags]
|
||||
);
|
||||
|
||||
const onApplyTagsChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: { value: string }) => {
|
||||
setApplyTags(value);
|
||||
},
|
||||
[setApplyTags]
|
||||
|
|
|
@ -7,6 +7,8 @@ import React, {
|
|||
} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { SelectProvider } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import SeriesAppState, { SeriesIndexAppState } from 'App/State/SeriesAppState';
|
||||
import { REFRESH_SERIES, RSS_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
@ -51,7 +53,7 @@ import SeriesIndexTable from './Table/SeriesIndexTable';
|
|||
import SeriesIndexTableOptions from './Table/SeriesIndexTableOptions';
|
||||
import styles from './SeriesIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
function getViewComponent(view: string) {
|
||||
if (view === 'posters') {
|
||||
return SeriesIndexPosters;
|
||||
}
|
||||
|
@ -81,7 +83,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
} = useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex'));
|
||||
}: SeriesAppState & SeriesIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createSeriesClientSideCollectionItemsSelector('seriesIndex'));
|
||||
|
||||
const isRefreshingSeries = useSelector(
|
||||
createCommandExecutingSelector(REFRESH_SERIES)
|
||||
|
@ -91,9 +94,11 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const scrollerRef = useRef<HTMLDivElement>(null);
|
||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [isSelectMode, setIsSelectMode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -122,14 +127,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
}, [isSelectMode, setIsSelectMode]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setSeriesTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onViewSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setSeriesView({ view: value }));
|
||||
|
||||
if (scrollerRef.current) {
|
||||
|
@ -140,14 +145,14 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setSeriesSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setSeriesFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -162,15 +167,15 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
}, [setIsOptionsModalOpen]);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character) => {
|
||||
(character: string) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
({ scrollTop }: { scrollTop: number }) => {
|
||||
setJumpToCharacter(undefined);
|
||||
scrollPositions.seriesIndex = scrollTop;
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
|
@ -184,10 +189,10 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
const characters = items.reduce((acc: Record<string, number>, item) => {
|
||||
let char = item.sortTitle.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
if (!isNaN(Number(char))) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
|
@ -305,6 +310,8 @@ const SeriesIndex = withScrollPosition((props: SeriesIndexProps) => {
|
|||
<PageContentBody
|
||||
ref={scrollerRef}
|
||||
className={styles.contentBody}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
initialScrollTop={props.initialScrollTop}
|
||||
onScroll={onScroll}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setSeriesFilter } from 'Store/Actions/seriesIndexActions';
|
||||
|
||||
function createSeriesSelector() {
|
||||
return createSelector(
|
||||
(state) => state.series.items,
|
||||
(state: AppState) => state.series.items,
|
||||
(series) => {
|
||||
return series;
|
||||
}
|
||||
|
@ -15,14 +16,20 @@ function createSeriesSelector() {
|
|||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.seriesIndex.filterBuilderProps,
|
||||
(state: AppState) => state.seriesIndex.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function SeriesIndexFilterModal(props) {
|
||||
interface SeriesIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function SeriesIndexFilterModal(
|
||||
props: SeriesIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createSeriesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'series';
|
||||
|
@ -30,7 +37,7 @@ export default function SeriesIndexFilterModal(props) {
|
|||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setSeriesFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
|
@ -38,6 +45,7 @@ export default function SeriesIndexFilterModal(props) {
|
|||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import SeriesAppState from 'App/State/SeriesAppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
|
@ -13,7 +14,7 @@ import styles from './SeriesIndexFooter.css';
|
|||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('series', 'seriesIndex'),
|
||||
(series) => {
|
||||
(series: SeriesAppState) => {
|
||||
return series.items.map((s) => {
|
||||
const { monitored, status, statistics } = s;
|
||||
|
||||
|
@ -45,7 +46,9 @@ export default function SeriesIndexFooter() {
|
|||
let totalFileSize = 0;
|
||||
|
||||
series.forEach((s) => {
|
||||
const { statistics = {} } = s;
|
||||
const {
|
||||
statistics = { episodeCount: 0, episodeFileCount: 0, sizeOnDisk: 0 },
|
||||
} = s;
|
||||
|
||||
const {
|
||||
episodeCount = 0,
|
||||
|
|
|
@ -17,9 +17,11 @@ import { icons } from 'Helpers/Props';
|
|||
import DeleteSeriesModal from 'Series/Delete/DeleteSeriesModal';
|
||||
import EditSeriesModalConnector from 'Series/Edit/EditSeriesModalConnector';
|
||||
import createSeriesIndexItemSelector from 'Series/Index/createSeriesIndexItemSelector';
|
||||
import { Statistics } from 'Series/Series';
|
||||
import SeriesBanner from 'Series/SeriesBanner';
|
||||
import SeriesTitleLink from 'Series/SeriesTitleLink';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
import SeriesIndexProgressBar from '../ProgressBar/SeriesIndexProgressBar';
|
||||
|
@ -58,7 +60,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
nextAiring,
|
||||
previousAiring,
|
||||
added,
|
||||
statistics = {},
|
||||
statistics = {} as Statistics,
|
||||
seasonFolder,
|
||||
images,
|
||||
seriesType,
|
||||
|
@ -137,7 +139,7 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
}, []);
|
||||
|
||||
const onSelectedChange = useCallback(
|
||||
({ id, value, shiftKey }) => {
|
||||
({ id, value, shiftKey }: SelectStateInputProps) => {
|
||||
selectDispatch({
|
||||
type: 'toggleSelected',
|
||||
id,
|
||||
|
@ -247,6 +249,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
|
||||
if (name === 'nextAiring') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
|
@ -258,6 +262,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
|
||||
if (name === 'previousAiring') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
|
@ -269,6 +275,8 @@ function SeriesIndexRow(props: SeriesIndexRowProps) {
|
|||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore ts(2739)
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { RefObject, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Column from 'Components/Table/Column';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
|
@ -30,17 +31,17 @@ interface RowItemData {
|
|||
|
||||
interface SeriesIndexTableProps {
|
||||
items: Series[];
|
||||
sortKey?: string;
|
||||
sortKey: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
scrollerRef: RefObject<HTMLElement>;
|
||||
isSelectMode: boolean;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const columnsSelector = createSelector(
|
||||
(state) => state.seriesIndex.columns,
|
||||
(state: AppState) => state.seriesIndex.columns,
|
||||
(columns) => columns
|
||||
);
|
||||
|
||||
|
@ -92,7 +93,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
|
|||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const listRef = useRef<List<RowItemData>>(null);
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
const windowWidth = window.innerWidth;
|
||||
|
@ -103,7 +104,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
|
|||
}, [showBanners]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
const current = scrollerRef?.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
|
@ -127,8 +128,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
|
|||
}, [isSmallScreen, windowWidth, windowHeight, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
const currentScrollListener = isSmallScreen ? window : currentScrollerRef;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
|
@ -137,7 +138,7 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
|
|||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
@ -166,8 +167,8 @@ function SeriesIndexTable(props: SeriesIndexTableProps) {
|
|||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
listRef.current?.scrollTo(scrollTop);
|
||||
scrollerRef?.current?.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
setSeriesSort,
|
||||
setSeriesTableOption,
|
||||
} from 'Store/Actions/seriesIndexActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import SeriesIndexTableOptions from './SeriesIndexTableOptions';
|
||||
import styles from './SeriesIndexTableHeader.css';
|
||||
|
@ -32,21 +33,21 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) {
|
|||
const [selectState, selectDispatch] = useSelect();
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value) => {
|
||||
(value: string) => {
|
||||
dispatch(setSeriesSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
(payload: unknown) => {
|
||||
dispatch(setSeriesTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onSelectAllChange = useCallback(
|
||||
({ value }) => {
|
||||
({ value }: CheckInputChanged) => {
|
||||
selectDispatch({
|
||||
type: value ? 'selectAll' : 'unselectAll',
|
||||
});
|
||||
|
@ -94,6 +95,8 @@ function SeriesIndexTableHeader(props: SeriesIndexTableHeaderProps) {
|
|||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
styles[name],
|
||||
name === 'sortTitle' && showBanners && styles.banner,
|
||||
name === 'sortTitle' &&
|
||||
|
|
|
@ -4,6 +4,7 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
interface SeriesIndexTableOptionsProps {
|
||||
|
@ -18,7 +19,7 @@ function SeriesIndexTableOptions(props: SeriesIndexTableOptionsProps) {
|
|||
const { showBanners, showSearchAction } = tableOptions;
|
||||
|
||||
const onTableOptionChangeWrapper = useCallback(
|
||||
({ name, value }) => {
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
onTableOptionChange({
|
||||
tableOptions: {
|
||||
...tableOptions,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import Column from 'Components/Table/Column';
|
||||
|
||||
const growableColumns = ['network', 'qualityProfileId', 'path', 'tags'];
|
||||
|
||||
export default function hasGrowableColumns(columns) {
|
||||
export default function hasGrowableColumns(columns: Column[]) {
|
||||
return columns.some((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectTableOptions = createSelector(
|
||||
(state) => state.seriesIndex.tableOptions,
|
||||
(state: AppState) => state.seriesIndex.tableOptions,
|
||||
(tableOptions) => tableOptions
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { maxBy } from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
import Command from 'Commands/Command';
|
||||
import { REFRESH_SERIES, SERIES_SEARCH } from 'Commands/commandNames';
|
||||
import Series from 'Series/Series';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
import createSeriesQualityProfileSelector from 'Store/Selectors/createSeriesQualityProfileSelector';
|
||||
import { createSeriesSelectorForHook } from 'Store/Selectors/createSeriesSelector';
|
||||
|
@ -10,25 +12,16 @@ function createSeriesIndexItemSelector(seriesId: number) {
|
|||
createSeriesSelectorForHook(seriesId),
|
||||
createSeriesQualityProfileSelector(seriesId),
|
||||
createExecutingCommandsSelector(),
|
||||
(series, qualityProfile, executingCommands) => {
|
||||
// If a series is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined series, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show a series that has no information available.
|
||||
|
||||
if (!series) {
|
||||
return {};
|
||||
}
|
||||
|
||||
(series: Series, qualityProfile, executingCommands: Command[]) => {
|
||||
const isRefreshingSeries = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === REFRESH_SERIES && command.body.seriesId === series.id
|
||||
command.name === REFRESH_SERIES && command.body.seriesId === seriesId
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingSeries = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === SERIES_SEARCH && command.body.seriesId === series.id
|
||||
command.name === SERIES_SEARCH && command.body.seriesId === seriesId
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export interface SeriesQueueDetails {
|
||||
count: number;
|
||||
|
@ -10,7 +11,7 @@ function createSeriesQueueDetailsSelector(
|
|||
seasonNumber?: number
|
||||
) {
|
||||
return createSelector(
|
||||
(state) => state.queue.details.items,
|
||||
(state: AppState) => state.queue.details.items,
|
||||
(queueItems) => {
|
||||
return queueItems.reduce(
|
||||
(acc: SeriesQueueDetails, item) => {
|
||||
|
|
|
@ -14,6 +14,7 @@ export interface Language {
|
|||
}
|
||||
|
||||
export interface Statistics {
|
||||
seasonCount: number;
|
||||
episodeCount: number;
|
||||
episodeFileCount: number;
|
||||
percentOfEpisodes: number;
|
||||
|
@ -41,11 +42,12 @@ export interface AlternateTitle {
|
|||
}
|
||||
|
||||
interface Series extends ModelBase {
|
||||
added: Date;
|
||||
added: string;
|
||||
alternateTitles: AlternateTitle[];
|
||||
certification: string;
|
||||
cleanTitle: string;
|
||||
ended: boolean;
|
||||
firstAired: Date;
|
||||
firstAired: string;
|
||||
genres: string[];
|
||||
images: Image[];
|
||||
imdbId: string;
|
||||
|
@ -54,7 +56,8 @@ interface Series extends ModelBase {
|
|||
originalLanguage: Language;
|
||||
overview: string;
|
||||
path: string;
|
||||
previousAiring: Date;
|
||||
previousAiring?: string;
|
||||
nextAiring?: string;
|
||||
qualityProfileId: number;
|
||||
ratings: Ratings;
|
||||
rootFolderPath: string;
|
||||
|
@ -73,6 +76,7 @@ interface Series extends ModelBase {
|
|||
tvRageId: number;
|
||||
useSceneNumbering: boolean;
|
||||
year: number;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Series;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
|
@ -8,7 +9,7 @@ export default function createEnabledDownloadClientsSelector(
|
|||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
(downloadClients) => {
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
const clients = items.filter(
|
||||
|
|
|
@ -5,6 +5,7 @@ export function createSeriesSelectorForHook(seriesId) {
|
|||
(state) => state.series.itemMap,
|
||||
(state) => state.series.items,
|
||||
(itemMap, allSeries) => {
|
||||
|
||||
return seriesId ? allSeries[itemMap[seriesId]]: undefined;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
const scrollPositions = {
|
||||
seriesIndex: 0
|
||||
};
|
||||
|
||||
export default scrollPositions;
|
|
@ -0,0 +1,5 @@
|
|||
const scrollPositions: Record<string, number> = {
|
||||
seriesIndex: 0,
|
||||
};
|
||||
|
||||
export default scrollPositions;
|
|
@ -1,15 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
|
||||
function getSelectedIds(selectedState, { parseIds = true } = {}) {
|
||||
return _.reduce(selectedState, (result, value, id) => {
|
||||
if (value) {
|
||||
const parsedId = parseIds ? parseInt(id) : id;
|
||||
|
||||
result.push(parsedId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
}
|
||||
|
||||
export default getSelectedIds;
|
|
@ -0,0 +1,24 @@
|
|||
import { reduce } from 'lodash';
|
||||
import { SelectedState } from 'Helpers/Hooks/useSelectState';
|
||||
|
||||
// TODO: This needs to handle string IDs as well
|
||||
function getSelectedIds(
|
||||
selectedState: SelectedState,
|
||||
{ parseIds = true } = {}
|
||||
): number[] {
|
||||
return reduce(
|
||||
selectedState,
|
||||
(result: any[], value, id) => {
|
||||
if (value) {
|
||||
const parsedId = parseIds ? parseInt(id) : id;
|
||||
|
||||
result.push(parsedId);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
export default getSelectedIds;
|
|
@ -0,0 +1,12 @@
|
|||
export interface QualityProfileFormatItem {
|
||||
format: number;
|
||||
name: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface CustomFormat {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export default CustomFormat;
|
|
@ -0,0 +1,19 @@
|
|||
interface MediaInfo {
|
||||
audioBitrate: number;
|
||||
audioChannels: number;
|
||||
audioCodec: string;
|
||||
audioLanguages: string;
|
||||
audioStreamCount: number;
|
||||
videoBitDepth: number;
|
||||
videoBitrate: number;
|
||||
videoCodec: string;
|
||||
videoFps: number;
|
||||
videoDynamicRange: string;
|
||||
videoDynamicRangeType: string;
|
||||
resolution: string;
|
||||
runTime: string;
|
||||
scanType: string;
|
||||
subtitles: string;
|
||||
}
|
||||
|
||||
export default MediaInfo;
|
|
@ -0,0 +1,23 @@
|
|||
import Quality from 'Quality/Quality';
|
||||
import { QualityProfileFormatItem } from './CustomFormat';
|
||||
|
||||
export interface QualityProfileQualityItem {
|
||||
id?: number;
|
||||
quality?: Quality;
|
||||
items: QualityProfileQualityItem[];
|
||||
allowed: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
interface QualityProfile {
|
||||
name: string;
|
||||
upgradeAllowed: boolean;
|
||||
cutoff: number;
|
||||
items: QualityProfileQualityItem[];
|
||||
minFormatScore: number;
|
||||
cutoffFormatScore: number;
|
||||
formatItems: QualityProfileFormatItem[];
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default QualityProfile;
|
|
@ -0,0 +1,6 @@
|
|||
export interface UiSettings {
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
|
||||
export type SortCallback = (
|
||||
sortKey: string,
|
||||
sortDirection: SortDirection
|
||||
) => void;
|
|
@ -0,0 +1,4 @@
|
|||
export type CheckInputChanged = {
|
||||
name: string;
|
||||
value: boolean;
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
export interface SelectStateInputProps {
|
||||
id: number;
|
||||
value: boolean;
|
||||
shiftKey: boolean;
|
||||
}
|
|
@ -7,7 +7,15 @@
|
|||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"typeRoots": ["node_modules/@types", "typings"],
|
||||
"paths": {
|
||||
|
|
|
@ -104,6 +104,9 @@
|
|||
"@babel/preset-env": "7.18.0",
|
||||
"@babel/preset-react": "7.17.12",
|
||||
"@babel/preset-typescript": "7.18.6",
|
||||
"@types/lodash": "4.14.192",
|
||||
"@types/react-router-dom": "5.3.3",
|
||||
"@types/react-text-truncate": "0.14.1",
|
||||
"@types/react-window": "1.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.0",
|
||||
|
|
34
yarn.lock
34
yarn.lock
|
@ -1422,6 +1422,11 @@
|
|||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/history@^4.7.11":
|
||||
version "4.7.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
|
||||
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
|
||||
|
||||
"@types/hoist-non-react-statics@^3.3.0":
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f"
|
||||
|
@ -1472,6 +1477,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/lodash@4.14.192":
|
||||
version "4.14.192"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.192.tgz#5790406361a2852d332d41635d927f1600811285"
|
||||
integrity sha512-km+Vyn3BYm5ytMO13k9KTp27O75rbQ0NFw+U//g+PX7VZyjCioXaRFisqSIJRECljcTv73G3i6BpglNGHgUQ5A==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
|
@ -1524,6 +1534,30 @@
|
|||
hoist-non-react-statics "^3.3.0"
|
||||
redux "^4.0.0"
|
||||
|
||||
"@types/react-router-dom@5.3.3":
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
|
||||
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
|
||||
dependencies:
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router@*":
|
||||
version "5.1.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
|
||||
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
|
||||
dependencies:
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-text-truncate@0.14.1":
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-text-truncate/-/react-text-truncate-0.14.1.tgz#3d24eca927e5fd1bfd789b047ae8ec53ba878b28"
|
||||
integrity sha512-yCtOOOJzrsfWF6TbnTDZz0gM5JYOxJmewExaTJTv01E7yrmpkNcmVny2fAtsNgSFCp8k2VgCePBoIvFBpKyEOw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-window@1.8.5":
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
||||
|
|
Loading…
Reference in New Issue