You've already forked Sonarr
							
							
				mirror of
				https://github.com/Sonarr/Sonarr.git
				synced 2025-10-31 00:07:55 +02:00 
			
		
		
		
	Convert Utilities to TypeScript
This commit is contained in:
		
				
					committed by
					
						 Mark McDowall
						Mark McDowall
					
				
			
			
				
	
			
			
			
						parent
						
							76650af9fd
						
					
				
				
					commit
					d46f4b2154
				
			
							
								
								
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| { | ||||
|   "typescript.tsdk": "node_modules\\typescript\\lib" | ||||
| } | ||||
| @@ -43,6 +43,13 @@ export interface AppSectionSchemaState<T> { | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export interface AppSectionItemSchemaState<T> { | ||||
|   isSchemaFetching: boolean; | ||||
|   isSchemaPopulated: boolean; | ||||
|   schemaError: Error; | ||||
|   schema: T; | ||||
| } | ||||
|  | ||||
| export interface AppSectionItemState<T> { | ||||
|   isFetching: boolean; | ||||
|   isPopulated: boolean; | ||||
|   | ||||
| @@ -35,14 +35,14 @@ export interface PropertyFilter { | ||||
| export interface Filter { | ||||
|   key: string; | ||||
|   label: string; | ||||
|   filers: PropertyFilter[]; | ||||
|   filters: PropertyFilter[]; | ||||
| } | ||||
|  | ||||
| export interface CustomFilter { | ||||
|   id: number; | ||||
|   type: string; | ||||
|   label: string; | ||||
|   filers: PropertyFilter[]; | ||||
|   filters: PropertyFilter[]; | ||||
| } | ||||
|  | ||||
| export interface AppSectionState { | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| import AppSectionState, { | ||||
|   AppSectionDeleteState, | ||||
|   AppSectionItemSchemaState, | ||||
|   AppSectionItemState, | ||||
|   AppSectionSaveState, | ||||
|   AppSectionSchemaState, | ||||
|   PagedAppSectionState, | ||||
| } from 'App/State/AppSectionState'; | ||||
| import Language from 'Language/Language'; | ||||
| @@ -40,7 +40,7 @@ export interface NotificationAppState | ||||
|  | ||||
| export interface QualityProfilesAppState | ||||
|   extends AppSectionState<QualityProfile>, | ||||
|     AppSectionSchemaState<QualityProfile> {} | ||||
|     AppSectionItemSchemaState<QualityProfile> {} | ||||
|  | ||||
| export interface ImportListOptionsSettingsAppState | ||||
|   extends AppSectionItemState<ImportListOptionsSettings>, | ||||
|   | ||||
| @@ -1,5 +1,16 @@ | ||||
| import ModelBase from 'App/ModelBase'; | ||||
|  | ||||
| export type CommandStatus = | ||||
|   | 'queued' | ||||
|   | 'started' | ||||
|   | 'completed' | ||||
|   | 'failed' | ||||
|   | 'aborted' | ||||
|   | 'cancelled' | ||||
|   | 'orphaned'; | ||||
|  | ||||
| export type CommandResult = 'unknown' | 'successful' | 'unsuccessful'; | ||||
|  | ||||
| export interface CommandBody { | ||||
|   sendUpdatesToClient: boolean; | ||||
|   updateScheduledTask: boolean; | ||||
| @@ -15,6 +26,7 @@ export interface CommandBody { | ||||
|   seriesId?: number; | ||||
|   seriesIds?: number[]; | ||||
|   seasonNumber?: number; | ||||
|   [key: string]: string | number | boolean | undefined | number[] | undefined; | ||||
| } | ||||
|  | ||||
| interface Command extends ModelBase { | ||||
| @@ -23,8 +35,8 @@ interface Command extends ModelBase { | ||||
|   message: string; | ||||
|   body: CommandBody; | ||||
|   priority: string; | ||||
|   status: string; | ||||
|   result: string; | ||||
|   status: CommandStatus; | ||||
|   result: CommandResult; | ||||
|   queued: string; | ||||
|   started: string; | ||||
|   ended: string; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| 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 Alert from 'Components/Alert'; | ||||
| import Form from 'Components/Form/Form'; | ||||
| @@ -21,21 +20,14 @@ import { CheckInputChanged } from 'typings/inputs'; | ||||
| import getQualities from 'Utilities/Quality/getQualities'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
|  | ||||
| interface QualitySchemaState { | ||||
|   isFetching: boolean; | ||||
|   isPopulated: boolean; | ||||
|   error: Error; | ||||
|   items: Quality[]; | ||||
| } | ||||
|  | ||||
| function createQualitySchemaSelector() { | ||||
|   return createSelector( | ||||
|     (state: AppState) => state.settings.qualityProfiles, | ||||
|     (qualityProfiles): QualitySchemaState => { | ||||
|     (qualityProfiles) => { | ||||
|       const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = | ||||
|         qualityProfiles; | ||||
|  | ||||
|       const items = getQualities(schema.items) as Quality[]; | ||||
|       const items = getQualities(schema.items); | ||||
|  | ||||
|       return { | ||||
|         isFetching: isSchemaFetching, | ||||
|   | ||||
| @@ -2,6 +2,20 @@ import ModelBase from 'App/ModelBase'; | ||||
| import Language from 'Language/Language'; | ||||
|  | ||||
| export type SeriesType = 'anime' | 'daily' | 'standard'; | ||||
| export type SeriesMonitor = | ||||
|   | 'all' | ||||
|   | 'future' | ||||
|   | 'missing' | ||||
|   | 'existing' | ||||
|   | 'recent' | ||||
|   | 'pilot' | ||||
|   | 'firstSeason' | ||||
|   | 'lastSeason' | ||||
|   | 'monitorSpecials' | ||||
|   | 'unmonitorSpecials' | ||||
|   | 'none'; | ||||
|  | ||||
| export type MonitorNewItems = 'all' | 'none'; | ||||
|  | ||||
| export interface Image { | ||||
|   coverType: string; | ||||
| @@ -34,7 +48,15 @@ export interface Ratings { | ||||
|  | ||||
| export interface AlternateTitle { | ||||
|   seasonNumber: number; | ||||
|   sceneSeasonNumber?: number; | ||||
|   title: string; | ||||
|   sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb'; | ||||
| } | ||||
|  | ||||
| export interface SeriesAddOptions { | ||||
|   monitor: SeriesMonitor; | ||||
|   searchForMissingEpisodes: boolean; | ||||
|   searchForCutoffUnmetEpisodes: boolean; | ||||
| } | ||||
|  | ||||
| interface Series extends ModelBase { | ||||
| @@ -48,6 +70,7 @@ interface Series extends ModelBase { | ||||
|   images: Image[]; | ||||
|   imdbId: string; | ||||
|   monitored: boolean; | ||||
|   monitorNewItems: MonitorNewItems; | ||||
|   network: string; | ||||
|   originalLanguage: Language; | ||||
|   overview: string; | ||||
| @@ -74,6 +97,7 @@ interface Series extends ModelBase { | ||||
|   useSceneNumbering: boolean; | ||||
|   year: number; | ||||
|   isSaving?: boolean; | ||||
|   addOptions: SeriesAddOptions; | ||||
| } | ||||
|  | ||||
| export default Series; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import pages from 'Utilities/pages'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import pages from 'Utilities/State/pages'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler'; | ||||
| import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; | ||||
| import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler'; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import pages from 'Utilities/pages'; | ||||
| import getSectionState from 'Utilities/State/getSectionState'; | ||||
| import pages from 'Utilities/State/pages'; | ||||
|  | ||||
| function createSetServerSideCollectionPageHandler(section, page, fetchHandler) { | ||||
|   return function(getState, payload, dispatch) { | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import createServerSideCollectionHandlers from 'Store/Actions/Creators/createSer | ||||
| import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; | ||||
| import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
|  | ||||
| // | ||||
| // Variables | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { batchActions } from 'redux-batched-actions'; | ||||
| import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import createAjaxRequest from 'Utilities/createAjaxRequest'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
| import { set, updateItem } from './baseActions'; | ||||
| import createHandleActions from './Creators/createHandleActions'; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import Icon from 'Components/Icon'; | ||||
| import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import createAjaxRequest from 'Utilities/createAjaxRequest'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
| import { updateItem } from './baseActions'; | ||||
| import createHandleActions from './Creators/createHandleActions'; | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import Icon from 'Components/Icon'; | ||||
| import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import createAjaxRequest from 'Utilities/createAjaxRequest'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
| import { set, updateItem } from './baseActions'; | ||||
| import createFetchHandler from './Creators/createFetchHandler'; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { filterTypes, sortDirections } from 'Helpers/Props'; | ||||
| import { setAppValue } from 'Store/Actions/appActions'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import createAjaxRequest from 'Utilities/createAjaxRequest'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
| import { pingServer } from './appActions'; | ||||
| import { set } from './baseActions'; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { createAction } from 'redux-actions'; | ||||
| import { filterTypes, sortDirections } from 'Helpers/Props'; | ||||
| import { createThunk, handleThunks } from 'Store/thunks'; | ||||
| import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; | ||||
| import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers'; | ||||
| import translate from 'Utilities/String/translate'; | ||||
| import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler'; | ||||
| import createHandleActions from './Creators/createHandleActions'; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import createCommandSelector from './createCommandSelector'; | ||||
|  | ||||
| function createCommandExecutingSelector(name: string, contraints = {}) { | ||||
|   return createSelector(createCommandSelector(name, contraints), (command) => { | ||||
|     return isCommandExecuting(command); | ||||
|     return command ? isCommandExecuting(command) : false; | ||||
|   }); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| export default function getIndexOfFirstCharacter(items, character) { | ||||
|   return items.findIndex((item) => { | ||||
|     const firstCharacter = item.sortTitle.charAt(0); | ||||
|  | ||||
|     if (character === '#') { | ||||
|       return !isNaN(firstCharacter); | ||||
|     } | ||||
|  | ||||
|     return firstCharacter === character; | ||||
|   }); | ||||
| } | ||||
							
								
								
									
										18
									
								
								frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import Series from 'Series/Series'; | ||||
|  | ||||
| const STARTS_WITH_NUMBER_REGEX = /^\d/; | ||||
|  | ||||
| export default function getIndexOfFirstCharacter( | ||||
|   items: Series[], | ||||
|   character: string | ||||
| ) { | ||||
|   return items.findIndex((item) => { | ||||
|     const firstCharacter = item.sortTitle.charAt(0); | ||||
|  | ||||
|     if (character === '#') { | ||||
|       return STARTS_WITH_NUMBER_REGEX.test(firstCharacter); | ||||
|     } | ||||
|  | ||||
|     return firstCharacter === character; | ||||
|   }); | ||||
| } | ||||
| @@ -1,7 +1,8 @@ | ||||
| import _ from 'lodash'; | ||||
| import Command, { CommandBody } from 'Commands/Command'; | ||||
| import isSameCommand from './isSameCommand'; | ||||
| 
 | ||||
| function findCommand(commands, options) { | ||||
| function findCommand(commands: Command[], options: Partial<CommandBody>) { | ||||
|   return _.findLast(commands, (command) => { | ||||
|     return isSameCommand(command.body, options); | ||||
|   }); | ||||
| @@ -1,9 +0,0 @@ | ||||
| function isCommandComplete(command) { | ||||
|   if (!command) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return command.status === 'complete'; | ||||
| } | ||||
|  | ||||
| export default isCommandComplete; | ||||
							
								
								
									
										11
									
								
								frontend/src/Utilities/Command/isCommandComplete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								frontend/src/Utilities/Command/isCommandComplete.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| import Command from 'Commands/Command'; | ||||
|  | ||||
| function isCommandComplete(command: Command) { | ||||
|   if (!command) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return command.status === 'completed'; | ||||
| } | ||||
|  | ||||
| export default isCommandComplete; | ||||
| @@ -1,4 +1,6 @@ | ||||
| function isCommandExecuting(command) { | ||||
| import Command from 'Commands/Command'; | ||||
| 
 | ||||
| function isCommandExecuting(command?: Command) { | ||||
|   if (!command) { | ||||
|     return false; | ||||
|   } | ||||
| @@ -1,12 +0,0 @@ | ||||
| function isCommandFailed(command) { | ||||
|   if (!command) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return command.status === 'failed' || | ||||
|          command.status === 'aborted' || | ||||
|          command.status === 'cancelled' || | ||||
|          command.status === 'orphaned'; | ||||
| } | ||||
|  | ||||
| export default isCommandFailed; | ||||
							
								
								
									
										16
									
								
								frontend/src/Utilities/Command/isCommandFailed.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								frontend/src/Utilities/Command/isCommandFailed.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
| import Command from 'Commands/Command'; | ||||
|  | ||||
| function isCommandFailed(command: Command) { | ||||
|   if (!command) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     command.status === 'failed' || | ||||
|     command.status === 'aborted' || | ||||
|     command.status === 'cancelled' || | ||||
|     command.status === 'orphaned' | ||||
|   ); | ||||
| } | ||||
|  | ||||
| export default isCommandFailed; | ||||
| @@ -1,24 +0,0 @@ | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| function isSameCommand(commandA, commandB) { | ||||
|   if (commandA.name.toLocaleLowerCase() !== commandB.name.toLocaleLowerCase()) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   for (const key in commandB) { | ||||
|     if (key !== 'name') { | ||||
|       const value = commandB[key]; | ||||
|       if (Array.isArray(value)) { | ||||
|         if (_.difference(value, commandA[key]).length > 0) { | ||||
|           return false; | ||||
|         } | ||||
|       } else if (value !== commandA[key]) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| export default isSameCommand; | ||||
							
								
								
									
										50
									
								
								frontend/src/Utilities/Command/isSameCommand.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								frontend/src/Utilities/Command/isSameCommand.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { CommandBody } from 'Commands/Command'; | ||||
|  | ||||
| function isSameCommand( | ||||
|   commandA: Partial<CommandBody>, | ||||
|   commandB: Partial<CommandBody> | ||||
| ) { | ||||
|   if ( | ||||
|     commandA.name?.toLocaleLowerCase() !== commandB.name?.toLocaleLowerCase() | ||||
|   ) { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   for (const key in commandB) { | ||||
|     if (key !== 'name') { | ||||
|       const value = commandB[key]; | ||||
|  | ||||
|       if (Array.isArray(value)) { | ||||
|         const sortedB = [...value].sort((a, b) => a - b); | ||||
|         const commandAProp = commandA[key]; | ||||
|         const sortedA = Array.isArray(commandAProp) | ||||
|           ? [...commandAProp].sort((a, b) => a - b) | ||||
|           : []; | ||||
|  | ||||
|         if (sortedA === sortedB) { | ||||
|           return true; | ||||
|         } | ||||
|  | ||||
|         if (sortedA == null || sortedB == null) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         if (sortedA.length !== sortedB.length) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         for (let i = 0; i < sortedB.length; ++i) { | ||||
|           if (sortedB[i] !== sortedA[i]) { | ||||
|             return false; | ||||
|           } | ||||
|         } | ||||
|       } else if (value !== commandA[key]) { | ||||
|         return false; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| export default isSameCommand; | ||||
| @@ -1,12 +1,17 @@ | ||||
| import _ from 'lodash'; | ||||
| import Episode from 'Episode/Episode'; | ||||
| import { update } from 'Store/Actions/baseActions'; | ||||
| 
 | ||||
| function updateEpisodes(section, episodes, episodeIds, options) { | ||||
|   const data = _.reduce(episodes, (result, item) => { | ||||
| function updateEpisodes( | ||||
|   section: string, | ||||
|   episodes: Episode[], | ||||
|   episodeIds: number[], | ||||
|   options: Partial<Episode> | ||||
| ) { | ||||
|   const data = episodes.reduce<Episode[]>((result, item) => { | ||||
|     if (episodeIds.indexOf(item.id) > -1) { | ||||
|       result.push({ | ||||
|         ...item, | ||||
|         ...options | ||||
|         ...options, | ||||
|       }); | ||||
|     } else { | ||||
|       result.push(item); | ||||
| @@ -1,19 +0,0 @@ | ||||
| export default function findSelectedFilters(selectedFilterKey, filters = [], customFilters = []) { | ||||
|   if (!selectedFilterKey) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   let selectedFilter = filters.find((f) => f.key === selectedFilterKey); | ||||
|  | ||||
|   if (!selectedFilter) { | ||||
|     selectedFilter = customFilters.find((f) => f.id === selectedFilterKey); | ||||
|   } | ||||
|  | ||||
|   if (!selectedFilter) { | ||||
|     // TODO: throw in dev | ||||
|     console.error('Matching filter not found'); | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return selectedFilter.filters; | ||||
| } | ||||
							
								
								
									
										27
									
								
								frontend/src/Utilities/Filter/findSelectedFilters.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/Utilities/Filter/findSelectedFilters.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import { CustomFilter, Filter } from 'App/State/AppState'; | ||||
|  | ||||
| export default function findSelectedFilters( | ||||
|   selectedFilterKey: string | number, | ||||
|   filters: Filter[] = [], | ||||
|   customFilters: CustomFilter[] = [] | ||||
| ) { | ||||
|   if (!selectedFilterKey) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   let selectedFilter: Filter | CustomFilter | undefined = filters.find( | ||||
|     (f) => f.key === selectedFilterKey | ||||
|   ); | ||||
|  | ||||
|   if (!selectedFilter) { | ||||
|     selectedFilter = customFilters.find((f) => f.id === selectedFilterKey); | ||||
|   } | ||||
|  | ||||
|   if (!selectedFilter) { | ||||
|     // TODO: throw in dev | ||||
|     console.error('Matching filter not found'); | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return selectedFilter.filters; | ||||
| } | ||||
| @@ -1,4 +1,11 @@ | ||||
| export default function getFilterValue(filters, filterKey, filterValueKey, defaultValue) { | ||||
| import { Filter } from 'App/State/AppState'; | ||||
| 
 | ||||
| export default function getFilterValue( | ||||
|   filters: Filter[], | ||||
|   filterKey: string | number, | ||||
|   filterValueKey: string, | ||||
|   defaultValue: string | number | boolean | ||||
| ) { | ||||
|   const filter = filters.find((f) => f.key === filterKey); | ||||
| 
 | ||||
|   if (!filter) { | ||||
| @@ -1,5 +1,4 @@ | ||||
| 
 | ||||
| function convertToBytes(input, power, binaryPrefix) { | ||||
| function convertToBytes(input: number, power: number, binaryPrefix: boolean) { | ||||
|   const size = Number(input); | ||||
| 
 | ||||
|   if (isNaN(size)) { | ||||
| @@ -1,19 +0,0 @@ | ||||
| import translate from 'Utilities/String/translate'; | ||||
|  | ||||
| function formatAge(age, ageHours, ageMinutes) { | ||||
|   age = Math.round(age); | ||||
|   ageHours = parseFloat(ageHours); | ||||
|   ageMinutes = ageMinutes && parseFloat(ageMinutes); | ||||
|  | ||||
|   if (age < 2 && ageHours) { | ||||
|     if (ageHours < 2 && !!ageMinutes) { | ||||
|       return `${ageMinutes.toFixed(0)} ${ageHours === 1 ? translate('FormatAgeMinute') : translate('FormatAgeMinutes')}`; | ||||
|     } | ||||
|  | ||||
|     return `${ageHours.toFixed(1)} ${ageHours === 1 ? translate('FormatAgeHour') : translate('FormatAgeHours')}`; | ||||
|   } | ||||
|  | ||||
|   return `${age} ${age === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays')}`; | ||||
| } | ||||
|  | ||||
| export default formatAge; | ||||
							
								
								
									
										33
									
								
								frontend/src/Utilities/Number/formatAge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/Utilities/Number/formatAge.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import translate from 'Utilities/String/translate'; | ||||
|  | ||||
| function formatAge( | ||||
|   age: string | number, | ||||
|   ageHours: string | number, | ||||
|   ageMinutes: string | number | ||||
| ) { | ||||
|   const ageRounded = Math.round(Number(age)); | ||||
|   const ageHoursFloat = parseFloat(String(ageHours)); | ||||
|   const ageMinutesFloat = ageMinutes && parseFloat(String(ageMinutes)); | ||||
|  | ||||
|   if (ageRounded < 2 && ageHoursFloat) { | ||||
|     if (ageHoursFloat < 2 && !!ageMinutesFloat) { | ||||
|       return `${ageMinutesFloat.toFixed(0)} ${ | ||||
|         ageHoursFloat === 1 | ||||
|           ? translate('FormatAgeMinute') | ||||
|           : translate('FormatAgeMinutes') | ||||
|       }`; | ||||
|     } | ||||
|  | ||||
|     return `${ageHoursFloat.toFixed(1)} ${ | ||||
|       ageHoursFloat === 1 | ||||
|         ? translate('FormatAgeHour') | ||||
|         : translate('FormatAgeHours') | ||||
|     }`; | ||||
|   } | ||||
|  | ||||
|   return `${ageRounded} ${ | ||||
|     ageRounded === 1 ? translate('FormatAgeDay') : translate('FormatAgeDays') | ||||
|   }`; | ||||
| } | ||||
|  | ||||
| export default formatAge; | ||||
| @@ -1,6 +1,10 @@ | ||||
| import { filesize } from 'filesize'; | ||||
| 
 | ||||
| function formatBytes(input) { | ||||
| function formatBytes(input?: string | number) { | ||||
|   if (!input) { | ||||
|     return ''; | ||||
|   } | ||||
| 
 | ||||
|   const size = Number(input); | ||||
| 
 | ||||
|   if (isNaN(size)) { | ||||
| @@ -9,7 +13,7 @@ function formatBytes(input) { | ||||
| 
 | ||||
|   return `${filesize(size, { | ||||
|     base: 2, | ||||
|     round: 1 | ||||
|     round: 1, | ||||
|   })}`;
 | ||||
| } | ||||
| 
 | ||||
| @@ -1,10 +0,0 @@ | ||||
| function padNumber(input, width, paddingCharacter = 0) { | ||||
|   if (input == null) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   input = `${input}`; | ||||
|   return input.length >= width ? input : new Array(width - input.length + 1).join(paddingCharacter) + input; | ||||
| } | ||||
|  | ||||
| export default padNumber; | ||||
							
								
								
									
										13
									
								
								frontend/src/Utilities/Number/padNumber.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								frontend/src/Utilities/Number/padNumber.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| function padNumber(input: number, width: number, paddingCharacter = '0') { | ||||
|   if (input == null) { | ||||
|     return ''; | ||||
|   } | ||||
|  | ||||
|   const result = `${input}`; | ||||
|  | ||||
|   return result.length >= width | ||||
|     ? result | ||||
|     : new Array(width - result.length + 1).join(paddingCharacter) + result; | ||||
| } | ||||
|  | ||||
| export default padNumber; | ||||
| @@ -1,4 +1,4 @@ | ||||
| export default function roundNumber(input, decimalPlaces = 1) { | ||||
| export default function roundNumber(input: number, decimalPlaces = 1) { | ||||
|   const multiplier = Math.pow(10, decimalPlaces); | ||||
| 
 | ||||
|   return Math.round(input * multiplier) / multiplier; | ||||
| @@ -1,4 +1,12 @@ | ||||
| function getErrorMessage(xhr, fallbackErrorMessage) { | ||||
| interface AjaxResponse { | ||||
|   responseJSON: | ||||
|     | { | ||||
|         message: string | undefined; | ||||
|       } | ||||
|     | undefined; | ||||
| } | ||||
| 
 | ||||
| function getErrorMessage(xhr: AjaxResponse, fallbackErrorMessage?: string) { | ||||
|   if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) { | ||||
|     return fallbackErrorMessage; | ||||
|   } | ||||
| @@ -1,15 +0,0 @@ | ||||
| function getRemovedItems(prevItems, currentItems, idProp = 'id') { | ||||
|   if (prevItems === currentItems) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   const currentItemIds = new Set(); | ||||
|  | ||||
|   currentItems.forEach((currentItem) => { | ||||
|     currentItemIds.add(currentItem[idProp]); | ||||
|   }); | ||||
|  | ||||
|   return prevItems.filter((prevItem) => !currentItemIds.has(prevItem[idProp])); | ||||
| } | ||||
|  | ||||
| export default getRemovedItems; | ||||
| @@ -1,4 +1,10 @@ | ||||
| function hasDifferentItems(prevItems, currentItems, idProp = 'id') { | ||||
| import ModelBase from 'App/ModelBase'; | ||||
| 
 | ||||
| function hasDifferentItems<T extends ModelBase>( | ||||
|   prevItems: T[], | ||||
|   currentItems: T[], | ||||
|   idProp: keyof T = 'id' | ||||
| ) { | ||||
|   if (prevItems === currentItems) { | ||||
|     return false; | ||||
|   } | ||||
| @@ -1,4 +1,10 @@ | ||||
| function hasDifferentItemsOrOrder(prevItems, currentItems, idProp = 'id') { | ||||
| import ModelBase from 'App/ModelBase'; | ||||
| 
 | ||||
| function hasDifferentItemsOrOrder<T extends ModelBase>( | ||||
|   prevItems: T[], | ||||
|   currentItems: T[], | ||||
|   idProp: keyof T = 'id' | ||||
| ) { | ||||
|   if (prevItems === currentItems) { | ||||
|     return false; | ||||
|   } | ||||
| @@ -1,16 +0,0 @@ | ||||
| export default function getQualities(qualities) { | ||||
|   if (!qualities) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return qualities.reduce((acc, item) => { | ||||
|     if (item.quality) { | ||||
|       acc.push(item.quality); | ||||
|     } else { | ||||
|       const groupQualities = item.items.map((i) => i.quality); | ||||
|       acc.push(...groupQualities); | ||||
|     } | ||||
|  | ||||
|     return acc; | ||||
|   }, []); | ||||
| } | ||||
							
								
								
									
										26
									
								
								frontend/src/Utilities/Quality/getQualities.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/Utilities/Quality/getQualities.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| import Quality from 'Quality/Quality'; | ||||
| import { QualityProfileQualityItem } from 'typings/QualityProfile'; | ||||
|  | ||||
| export default function getQualities(qualities?: QualityProfileQualityItem[]) { | ||||
|   if (!qualities) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return qualities.reduce<Quality[]>((acc, item) => { | ||||
|     if (item.quality) { | ||||
|       acc.push(item.quality); | ||||
|     } else { | ||||
|       const groupQualities = item.items.reduce<Quality[]>((acc, i) => { | ||||
|         if (i.quality) { | ||||
|           acc.push(i.quality); | ||||
|         } | ||||
|  | ||||
|         return acc; | ||||
|       }, []); | ||||
|  | ||||
|       acc.push(...groupQualities); | ||||
|     } | ||||
|  | ||||
|     return acc; | ||||
|   }, []); | ||||
| } | ||||
| @@ -1,26 +0,0 @@ | ||||
| import $ from 'jquery'; | ||||
|  | ||||
| module.exports = { | ||||
|   resolutions: { | ||||
|     desktopLarge: 1200, | ||||
|     desktop: 992, | ||||
|     tablet: 768, | ||||
|     mobile: 480 | ||||
|   }, | ||||
|  | ||||
|   isDesktopLarge() { | ||||
|     return $(window).width() < this.resolutions.desktopLarge; | ||||
|   }, | ||||
|  | ||||
|   isDesktop() { | ||||
|     return $(window).width() < this.resolutions.desktop; | ||||
|   }, | ||||
|  | ||||
|   isTablet() { | ||||
|     return $(window).width() < this.resolutions.tablet; | ||||
|   }, | ||||
|  | ||||
|   isMobile() { | ||||
|     return $(window).width() < this.resolutions.mobile; | ||||
|   } | ||||
| }; | ||||
							
								
								
									
										24
									
								
								frontend/src/Utilities/ResolutionUtility.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								frontend/src/Utilities/ResolutionUtility.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| module.exports = { | ||||
|   resolutions: { | ||||
|     desktopLarge: 1200, | ||||
|     desktop: 992, | ||||
|     tablet: 768, | ||||
|     mobile: 480, | ||||
|   }, | ||||
|  | ||||
|   isDesktopLarge() { | ||||
|     return window.innerWidth < this.resolutions.desktopLarge; | ||||
|   }, | ||||
|  | ||||
|   isDesktop() { | ||||
|     return window.innerWidth < this.resolutions.desktop; | ||||
|   }, | ||||
|  | ||||
|   isTablet() { | ||||
|     return window.innerWidth < this.resolutions.tablet; | ||||
|   }, | ||||
|  | ||||
|   isMobile() { | ||||
|     return window.innerWidth < this.resolutions.mobile; | ||||
|   }, | ||||
| }; | ||||
| @@ -1,53 +0,0 @@ | ||||
|  | ||||
| function filterAlternateTitles(alternateTitles, seriesTitle, useSceneNumbering, seasonNumber, sceneSeasonNumber) { | ||||
|   const globalTitles = []; | ||||
|   const seasonTitles = []; | ||||
|  | ||||
|   if (alternateTitles) { | ||||
|     alternateTitles.forEach((alternateTitle) => { | ||||
|       if (alternateTitle.sceneOrigin === 'unknown' || alternateTitle.sceneOrigin === 'unknown:tvdb') { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (alternateTitle.sceneOrigin === 'mixed') { | ||||
|         // For now filter out 'mixed' from the UI, the user will get an rejection during manual search. | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const hasAltSeasonNumber = (alternateTitle.seasonNumber !== -1 && alternateTitle.seasonNumber !== undefined); | ||||
|       const hasAltSceneSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined); | ||||
|  | ||||
|       // Global alias that should be displayed global | ||||
|       if (!hasAltSeasonNumber && !hasAltSceneSeasonNumber && | ||||
|           (alternateTitle.title !== seriesTitle) && | ||||
|           (!alternateTitle.sceneOrigin || !useSceneNumbering)) { | ||||
|         globalTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Global alias that should be displayed per episode | ||||
|       if (!hasAltSeasonNumber && !hasAltSceneSeasonNumber && alternateTitle.sceneOrigin && useSceneNumbering) { | ||||
|         seasonTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Apply the alternative mapping (release to scene) | ||||
|       const mappedAltSeasonNumber = hasAltSeasonNumber ? alternateTitle.seasonNumber : alternateTitle.sceneSeasonNumber; | ||||
|       // Select scene or tvdb on the episode | ||||
|       const mappedSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : sceneSeasonNumber; | ||||
|  | ||||
|       if (mappedSeasonNumber !== undefined && mappedSeasonNumber === mappedAltSeasonNumber) { | ||||
|         seasonTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (seasonNumber === undefined) { | ||||
|     return globalTitles; | ||||
|   } | ||||
|  | ||||
|   return seasonTitles; | ||||
| } | ||||
|  | ||||
| export default filterAlternateTitles; | ||||
							
								
								
									
										83
									
								
								frontend/src/Utilities/Series/filterAlternateTitles.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								frontend/src/Utilities/Series/filterAlternateTitles.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| import { AlternateTitle } from 'Series/Series'; | ||||
|  | ||||
| function filterAlternateTitles( | ||||
|   alternateTitles: AlternateTitle[], | ||||
|   seriesTitle: string | null, | ||||
|   useSceneNumbering: boolean, | ||||
|   seasonNumber?: number, | ||||
|   sceneSeasonNumber?: number | ||||
| ) { | ||||
|   const globalTitles: AlternateTitle[] = []; | ||||
|   const seasonTitles: AlternateTitle[] = []; | ||||
|  | ||||
|   if (alternateTitles) { | ||||
|     alternateTitles.forEach((alternateTitle) => { | ||||
|       if ( | ||||
|         alternateTitle.sceneOrigin === 'unknown' || | ||||
|         alternateTitle.sceneOrigin === 'unknown:tvdb' | ||||
|       ) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       if (alternateTitle.sceneOrigin === 'mixed') { | ||||
|         // For now filter out 'mixed' from the UI, the user will get an rejection during manual search. | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const hasAltSeasonNumber = | ||||
|         alternateTitle.seasonNumber !== -1 && | ||||
|         alternateTitle.seasonNumber !== undefined; | ||||
|       const hasAltSceneSeasonNumber = | ||||
|         alternateTitle.sceneSeasonNumber !== -1 && | ||||
|         alternateTitle.sceneSeasonNumber !== undefined; | ||||
|  | ||||
|       // Global alias that should be displayed global | ||||
|       if ( | ||||
|         !hasAltSeasonNumber && | ||||
|         !hasAltSceneSeasonNumber && | ||||
|         alternateTitle.title !== seriesTitle && | ||||
|         (!alternateTitle.sceneOrigin || !useSceneNumbering) | ||||
|       ) { | ||||
|         globalTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Global alias that should be displayed per episode | ||||
|       if ( | ||||
|         !hasAltSeasonNumber && | ||||
|         !hasAltSceneSeasonNumber && | ||||
|         alternateTitle.sceneOrigin && | ||||
|         useSceneNumbering | ||||
|       ) { | ||||
|         seasonTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       // Apply the alternative mapping (release to scene) | ||||
|       const mappedAltSeasonNumber = hasAltSeasonNumber | ||||
|         ? alternateTitle.seasonNumber | ||||
|         : alternateTitle.sceneSeasonNumber; | ||||
|       // Select scene or tvdb on the episode | ||||
|       const mappedSeasonNumber = | ||||
|         alternateTitle.sceneOrigin === 'tvdb' | ||||
|           ? seasonNumber | ||||
|           : sceneSeasonNumber; | ||||
|  | ||||
|       if ( | ||||
|         mappedSeasonNumber !== undefined && | ||||
|         mappedSeasonNumber === mappedAltSeasonNumber | ||||
|       ) { | ||||
|         seasonTitles.push(alternateTitle); | ||||
|         return; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   if (seasonNumber === undefined) { | ||||
|     return globalTitles; | ||||
|   } | ||||
|  | ||||
|   return seasonTitles; | ||||
| } | ||||
|  | ||||
| export default filterAlternateTitles; | ||||
| @@ -1,5 +1,22 @@ | ||||
| import Series, { | ||||
|   MonitorNewItems, | ||||
|   SeriesMonitor, | ||||
|   SeriesType, | ||||
| } from 'Series/Series'; | ||||
| 
 | ||||
| function getNewSeries(series, payload) { | ||||
| interface NewSeriesPayload { | ||||
|   rootFolderPath: string; | ||||
|   monitor: SeriesMonitor; | ||||
|   monitorNewItems: MonitorNewItems; | ||||
|   qualityProfileId: number; | ||||
|   seriesType: SeriesType; | ||||
|   seasonFolder: boolean; | ||||
|   tags: number[]; | ||||
|   searchForMissingEpisodes?: boolean; | ||||
|   searchForCutoffUnmetEpisodes?: boolean; | ||||
| } | ||||
| 
 | ||||
| function getNewSeries(series: Series, payload: NewSeriesPayload) { | ||||
|   const { | ||||
|     rootFolderPath, | ||||
|     monitor, | ||||
| @@ -9,13 +26,13 @@ function getNewSeries(series, payload) { | ||||
|     seasonFolder, | ||||
|     tags, | ||||
|     searchForMissingEpisodes = false, | ||||
|     searchForCutoffUnmetEpisodes = false | ||||
|     searchForCutoffUnmetEpisodes = false, | ||||
|   } = payload; | ||||
| 
 | ||||
|   const addOptions = { | ||||
|     monitor, | ||||
|     searchForMissingEpisodes, | ||||
|     searchForCutoffUnmetEpisodes | ||||
|     searchForCutoffUnmetEpisodes, | ||||
|   }; | ||||
| 
 | ||||
|   series.addOptions = addOptions; | ||||
| @@ -5,14 +5,14 @@ const monitorNewItemsOptions = [ | ||||
|     key: 'all', | ||||
|     get value() { | ||||
|       return translate('MonitorAllSeasons'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'none', | ||||
|     get value() { | ||||
|       return translate('MonitorNoNewSeasons'); | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export default monitorNewItemsOptions; | ||||
| @@ -5,68 +5,68 @@ const monitorOptions = [ | ||||
|     key: 'all', | ||||
|     get value() { | ||||
|       return translate('MonitorAllEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'future', | ||||
|     get value() { | ||||
|       return translate('MonitorFutureEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'missing', | ||||
|     get value() { | ||||
|       return translate('MonitorMissingEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'existing', | ||||
|     get value() { | ||||
|       return translate('MonitorExistingEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'recent', | ||||
|     get value() { | ||||
|       return translate('MonitorRecentEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'pilot', | ||||
|     get value() { | ||||
|       return translate('MonitorPilotEpisode'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'firstSeason', | ||||
|     get value() { | ||||
|       return translate('MonitorFirstSeason'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'lastSeason', | ||||
|     get value() { | ||||
|       return translate('MonitorLastSeason'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'monitorSpecials', | ||||
|     get value() { | ||||
|       return translate('MonitorSpecialEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'unmonitorSpecials', | ||||
|     get value() { | ||||
|       return translate('UnmonitorSpecialEpisodes'); | ||||
|     } | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     key: 'none', | ||||
|     get value() { | ||||
|       return translate('MonitorNoEpisodes'); | ||||
|     } | ||||
|   } | ||||
|     }, | ||||
|   }, | ||||
| ]; | ||||
| 
 | ||||
| export default monitorOptions; | ||||
| @@ -1,5 +0,0 @@ | ||||
| function getNextId(items) { | ||||
|   return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; | ||||
| } | ||||
|  | ||||
| export default getNextId; | ||||
							
								
								
									
										7
									
								
								frontend/src/Utilities/State/getNextId.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/Utilities/State/getNextId.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| import ModelBase from 'App/ModelBase'; | ||||
|  | ||||
| function getNextId<T extends ModelBase>(items: T[]) { | ||||
|   return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; | ||||
| } | ||||
|  | ||||
| export default getNextId; | ||||
| @@ -1,5 +0,0 @@ | ||||
| export default function combinePath(isWindows, basePath, paths = []) { | ||||
|   const slash = isWindows ? '\\' : '/'; | ||||
|  | ||||
|   return `${basePath}${slash}${paths.join(slash)}`; | ||||
| } | ||||
							
								
								
									
										9
									
								
								frontend/src/Utilities/String/combinePath.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								frontend/src/Utilities/String/combinePath.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| export default function combinePath( | ||||
|   isWindows: boolean, | ||||
|   basePath: string, | ||||
|   paths: string[] = [] | ||||
| ) { | ||||
|   const slash = isWindows ? '\\' : '/'; | ||||
|  | ||||
|   return `${basePath}${slash}${paths.join(slash)}`; | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| export default function generateUUIDv4() { | ||||
|   return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => | ||||
|     // eslint-disable-next-line no-bitwise | ||||
|     (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) | ||||
|   ); | ||||
| } | ||||
| @@ -1,3 +1,3 @@ | ||||
| export default function isString(possibleString) { | ||||
| export default function isString(possibleString: unknown) { | ||||
|   return typeof possibleString === 'string' || possibleString instanceof String; | ||||
| } | ||||
| @@ -1,6 +1,6 @@ | ||||
| const regex = /\d+/g; | ||||
| 
 | ||||
| function naturalExpansion(input) { | ||||
| function naturalExpansion(input: string) { | ||||
|   if (!input) { | ||||
|     return ''; | ||||
|   } | ||||
| @@ -4,13 +4,13 @@ import qs from 'qs'; | ||||
| // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
 | ||||
| const anchor = document.createElement('a'); | ||||
| 
 | ||||
| export default function parseUrl(url) { | ||||
| export default function parseUrl(url: string) { | ||||
|   anchor.href = url; | ||||
| 
 | ||||
|   // The `origin`, `password`, and `username` properties are unavailable in
 | ||||
|   // Opera Presto. We synthesize `origin` if it's not present. While `password`
 | ||||
|   // and `username` are ignored intentionally.
 | ||||
|   const properties = _.pick( | ||||
|   const properties: Record<string, string | number | boolean | object> = _.pick( | ||||
|     anchor, | ||||
|     'hash', | ||||
|     'host', | ||||
| @@ -23,11 +23,11 @@ export default function parseUrl(url) { | ||||
|     'search' | ||||
|   ); | ||||
| 
 | ||||
|   properties.isAbsolute = (/^[\w:]*\/\//).test(url); | ||||
|   properties.isAbsolute = /^[\w:]*\/\//.test(url); | ||||
| 
 | ||||
|   if (properties.search) { | ||||
|     // Remove leading ? from querystring before parsing.
 | ||||
|     properties.params = qs.parse(properties.search.substring(1)); | ||||
|     properties.params = qs.parse((properties.search as string).substring(1)); | ||||
|   } else { | ||||
|     properties.params = {}; | ||||
|   } | ||||
| @@ -1,17 +0,0 @@ | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| function split(input, separator = ',') { | ||||
|   if (!input) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return _.reduce(input.split(separator), (result, s) => { | ||||
|     if (s) { | ||||
|       result.push(s); | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|   }, []); | ||||
| } | ||||
|  | ||||
| export default split; | ||||
							
								
								
									
										15
									
								
								frontend/src/Utilities/String/split.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/Utilities/String/split.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | ||||
| function split(input: string, separator = ',') { | ||||
|   if (!input) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   return input.split(separator).reduce<string[]>((acc, s) => { | ||||
|     if (s) { | ||||
|       acc.push(s); | ||||
|     } | ||||
|  | ||||
|     return acc; | ||||
|   }, []); | ||||
| } | ||||
|  | ||||
| export default split; | ||||
| @@ -1,6 +1,6 @@ | ||||
| const regex = /\b\w+/g; | ||||
| 
 | ||||
| function titleCase(input) { | ||||
| function titleCase(input: string | undefined) { | ||||
|   if (!input) { | ||||
|     return ''; | ||||
|   } | ||||
| @@ -1,17 +0,0 @@ | ||||
| export default function areAllSelected(selectedState) { | ||||
|   let allSelected = true; | ||||
|   let allUnselected = true; | ||||
|  | ||||
|   Object.keys(selectedState).forEach((key) => { | ||||
|     if (selectedState[key]) { | ||||
|       allUnselected = false; | ||||
|     } else { | ||||
|       allSelected = false; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     allSelected, | ||||
|     allUnselected | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										19
									
								
								frontend/src/Utilities/Table/areAllSelected.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/Utilities/Table/areAllSelected.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { SelectedState } from 'Helpers/Hooks/useSelectState'; | ||||
|  | ||||
| export default function areAllSelected(selectedState: SelectedState) { | ||||
|   let allSelected = true; | ||||
|   let allUnselected = true; | ||||
|  | ||||
|   Object.values(selectedState).forEach((value) => { | ||||
|     if (value) { | ||||
|       allUnselected = false; | ||||
|     } else { | ||||
|       allSelected = false; | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     allSelected, | ||||
|     allUnselected, | ||||
|   }; | ||||
| } | ||||
| @@ -1,23 +0,0 @@ | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| function getToggledRange(items, id, lastToggled) { | ||||
|   const lastToggledIndex = _.findIndex(items, { id: lastToggled }); | ||||
|   const changedIndex = _.findIndex(items, { id }); | ||||
|   let lower = 0; | ||||
|   let upper = 0; | ||||
|  | ||||
|   if (lastToggledIndex > changedIndex) { | ||||
|     lower = changedIndex; | ||||
|     upper = lastToggledIndex + 1; | ||||
|   } else { | ||||
|     lower = lastToggledIndex; | ||||
|     upper = changedIndex; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     lower, | ||||
|     upper | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default getToggledRange; | ||||
							
								
								
									
										27
									
								
								frontend/src/Utilities/Table/getToggledRange.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/src/Utilities/Table/getToggledRange.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import ModelBase from 'App/ModelBase'; | ||||
|  | ||||
| function getToggledRange<T extends ModelBase>( | ||||
|   items: T[], | ||||
|   id: number, | ||||
|   lastToggled: number | ||||
| ) { | ||||
|   const lastToggledIndex = items.findIndex((item) => item.id === lastToggled); | ||||
|   const changedIndex = items.findIndex((item) => item.id === id); | ||||
|   let lower = 0; | ||||
|   let upper = 0; | ||||
|  | ||||
|   if (lastToggledIndex > changedIndex) { | ||||
|     lower = changedIndex; | ||||
|     upper = lastToggledIndex + 1; | ||||
|   } else { | ||||
|     lower = lastToggledIndex; | ||||
|     upper = changedIndex; | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     lower, | ||||
|     upper, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default getToggledRange; | ||||
| @@ -1,16 +0,0 @@ | ||||
| import areAllSelected from './areAllSelected'; | ||||
|  | ||||
| export default function removeOldSelectedState(state, prevItems) { | ||||
|   const selectedState = { | ||||
|     ...state.selectedState | ||||
|   }; | ||||
|  | ||||
|   prevItems.forEach((item) => { | ||||
|     delete selectedState[item.id]; | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     ...areAllSelected(selectedState), | ||||
|     selectedState | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										21
									
								
								frontend/src/Utilities/Table/removeOldSelectedState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								frontend/src/Utilities/Table/removeOldSelectedState.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import ModelBase from 'App/ModelBase'; | ||||
| import { SelectState } from 'Helpers/Hooks/useSelectState'; | ||||
| import areAllSelected from './areAllSelected'; | ||||
|  | ||||
| export default function removeOldSelectedState<T extends ModelBase>( | ||||
|   state: SelectState, | ||||
|   prevItems: T[] | ||||
| ) { | ||||
|   const selectedState = { | ||||
|     ...state.selectedState, | ||||
|   }; | ||||
|  | ||||
|   prevItems.forEach((item) => { | ||||
|     delete selectedState[item.id]; | ||||
|   }); | ||||
|  | ||||
|   return { | ||||
|     ...areAllSelected(selectedState), | ||||
|     selectedState, | ||||
|   }; | ||||
| } | ||||
| @@ -1,17 +0,0 @@ | ||||
| import _ from 'lodash'; | ||||
|  | ||||
| function selectAll(selectedState, selected) { | ||||
|   const newSelectedState = _.reduce(Object.keys(selectedState), (result, item) => { | ||||
|     result[item] = selected; | ||||
|     return result; | ||||
|   }, {}); | ||||
|  | ||||
|   return { | ||||
|     allSelected: selected, | ||||
|     allUnselected: !selected, | ||||
|     lastToggled: null, | ||||
|     selectedState: newSelectedState | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default selectAll; | ||||
							
								
								
									
										19
									
								
								frontend/src/Utilities/Table/selectAll.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								frontend/src/Utilities/Table/selectAll.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| import { SelectedState } from 'Helpers/Hooks/useSelectState'; | ||||
|  | ||||
| function selectAll(selectedState: SelectedState, selected: boolean) { | ||||
|   const newSelectedState = Object.keys(selectedState).reduce< | ||||
|     Record<number, boolean> | ||||
|   >((acc, item) => { | ||||
|     acc[Number(item)] = selected; | ||||
|     return acc; | ||||
|   }, {}); | ||||
|  | ||||
|   return { | ||||
|     allSelected: selected, | ||||
|     allUnselected: !selected, | ||||
|     lastToggled: null, | ||||
|     selectedState: newSelectedState, | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export default selectAll; | ||||
| @@ -1,11 +1,19 @@ | ||||
| import ModelBase from 'App/ModelBase'; | ||||
| import { SelectState } from 'Helpers/Hooks/useSelectState'; | ||||
| import areAllSelected from './areAllSelected'; | ||||
| import getToggledRange from './getToggledRange'; | ||||
| 
 | ||||
| function toggleSelected(selectedState, items, id, selected, shiftKey) { | ||||
|   const lastToggled = selectedState.lastToggled; | ||||
| function toggleSelected<T extends ModelBase>( | ||||
|   selectState: SelectState, | ||||
|   items: T[], | ||||
|   id: number, | ||||
|   selected: boolean, | ||||
|   shiftKey: boolean | ||||
| ) { | ||||
|   const lastToggled = selectState.lastToggled; | ||||
|   const nextSelectedState = { | ||||
|     ...selectedState.selectedState, | ||||
|     [id]: selected | ||||
|     ...selectState.selectedState, | ||||
|     [id]: selected, | ||||
|   }; | ||||
| 
 | ||||
|   if (selected == null) { | ||||
| @@ -23,7 +31,7 @@ function toggleSelected(selectedState, items, id, selected, shiftKey) { | ||||
|   return { | ||||
|     ...areAllSelected(nextSelectedState), | ||||
|     lastToggled: id, | ||||
|     selectedState: nextSelectedState | ||||
|     selectedState: nextSelectedState, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| @@ -3,7 +3,6 @@ import MobileDetect from 'mobile-detect'; | ||||
| const mobileDetect = new MobileDetect(window.navigator.userAgent); | ||||
| 
 | ||||
| export function isMobile() { | ||||
| 
 | ||||
|   return mobileDetect.mobile() != null; | ||||
| } | ||||
| 
 | ||||
| @@ -1,3 +0,0 @@ | ||||
| export default function getPathWithUrlBase(path) { | ||||
|   return `${window.Sonarr.urlBase}${path}`; | ||||
| } | ||||
							
								
								
									
										3
									
								
								frontend/src/Utilities/getPathWithUrlBase.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/Utilities/getPathWithUrlBase.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export default function getPathWithUrlBase(path: string) { | ||||
|   return `${window.Sonarr.urlBase}${path}`; | ||||
| } | ||||
| @@ -1,19 +1,24 @@ | ||||
| let currentPopulator = null; | ||||
| let currentReasons = []; | ||||
| type Populator = () => void; | ||||
| 
 | ||||
| export function registerPagePopulator(populator, reasons = []) { | ||||
| let currentPopulator: Populator | null = null; | ||||
| let currentReasons: string[] = []; | ||||
| 
 | ||||
| export function registerPagePopulator( | ||||
|   populator: Populator, | ||||
|   reasons: string[] = [] | ||||
| ) { | ||||
|   currentPopulator = populator; | ||||
|   currentReasons = reasons; | ||||
| } | ||||
| 
 | ||||
| export function unregisterPagePopulator(populator) { | ||||
| export function unregisterPagePopulator(populator: Populator) { | ||||
|   if (currentPopulator === populator) { | ||||
|     currentPopulator = null; | ||||
|     currentReasons = []; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| export function repopulatePage(reason) { | ||||
| export function repopulatePage(reason: string) { | ||||
|   if (!currentPopulator) { | ||||
|     return; | ||||
|   } | ||||
| @@ -1,18 +1,17 @@ | ||||
| import $ from 'jquery'; | ||||
| import _ from 'lodash'; | ||||
| import createAjaxRequest from './createAjaxRequest'; | ||||
|  | ||||
| function flattenProviderData(providerData) { | ||||
|   return _.reduce(Object.keys(providerData), (result, key) => { | ||||
|   return Object.keys(providerData).reduce((acc, key) => { | ||||
|     const property = providerData[key]; | ||||
|  | ||||
|     if (key === 'fields') { | ||||
|       result[key] = property; | ||||
|       acc[key] = property; | ||||
|     } else { | ||||
|       result[key] = property.value; | ||||
|       acc[key] = property.value; | ||||
|     } | ||||
|  | ||||
|     return result; | ||||
|     return acc; | ||||
|   }, {}); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -8,6 +8,6 @@ export function isLocked() { | ||||
|   return scrollLock; | ||||
| } | ||||
| 
 | ||||
| export function setScrollLock(locked) { | ||||
| export function setScrollLock(locked: boolean) { | ||||
|   scrollLock = locked; | ||||
| } | ||||
| @@ -1,6 +0,0 @@ | ||||
| const sectionTypes = { | ||||
|   COLLECTION: 'collection', | ||||
|   MODEL: 'model' | ||||
| }; | ||||
|  | ||||
| export default sectionTypes; | ||||
| @@ -92,6 +92,7 @@ | ||||
|     "@babel/preset-react": "7.24.1", | ||||
|     "@babel/preset-typescript": "7.24.1", | ||||
|     "@types/lodash": "4.14.194", | ||||
|     "@types/qs": "6.9.15", | ||||
|     "@types/react-lazyload": "3.2.0", | ||||
|     "@types/react-router-dom": "5.3.3", | ||||
|     "@types/react-text-truncate": "0.14.1", | ||||
|   | ||||
| @@ -1458,6 +1458,11 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" | ||||
|   integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== | ||||
|  | ||||
| "@types/qs@6.9.15": | ||||
|   version "6.9.15" | ||||
|   resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" | ||||
|   integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== | ||||
|  | ||||
| "@types/react-dom@18.2.25": | ||||
|   version "18.2.25" | ||||
|   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user