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