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