diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..44aeb4060 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules\\typescript\\lib" +} diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index f89eb25f7..18f0adf50 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -43,6 +43,13 @@ export interface AppSectionSchemaState { }; } +export interface AppSectionItemSchemaState { + isSchemaFetching: boolean; + isSchemaPopulated: boolean; + schemaError: Error; + schema: T; +} + export interface AppSectionItemState { isFetching: boolean; isPopulated: boolean; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 6e0893926..744f11f31 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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 { diff --git a/frontend/src/App/State/SettingsAppState.ts b/frontend/src/App/State/SettingsAppState.ts index d6624ff74..0959e99c5 100644 --- a/frontend/src/App/State/SettingsAppState.ts +++ b/frontend/src/App/State/SettingsAppState.ts @@ -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, - AppSectionSchemaState {} + AppSectionItemSchemaState {} export interface ImportListOptionsSettingsAppState extends AppSectionItemState, diff --git a/frontend/src/Commands/Command.ts b/frontend/src/Commands/Command.ts index 0830fd34b..d0797a79a 100644 --- a/frontend/src/Commands/Command.ts +++ b/frontend/src/Commands/Command.ts @@ -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; diff --git a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx index edb65663c..5d4d561b0 100644 --- a/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx +++ b/frontend/src/InteractiveImport/Quality/SelectQualityModalContent.tsx @@ -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, diff --git a/frontend/src/Series/Series.ts b/frontend/src/Series/Series.ts index 321fc7378..c93ccf3ff 100644 --- a/frontend/src/Series/Series.ts +++ b/frontend/src/Series/Series.ts @@ -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; diff --git a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js index 8b4697377..9d0615509 100644 --- a/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js +++ b/frontend/src/Store/Actions/Creators/createServerSideCollectionHandlers.js @@ -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'; diff --git a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js index 12b21bb0d..069de8d5b 100644 --- a/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js +++ b/frontend/src/Store/Actions/Creators/createSetServerSideCollectionPageHandler.js @@ -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) { diff --git a/frontend/src/Store/Actions/Settings/importListExclusions.js b/frontend/src/Store/Actions/Settings/importListExclusions.js index d6371946f..3af8bf9ec 100644 --- a/frontend/src/Store/Actions/Settings/importListExclusions.js +++ b/frontend/src/Store/Actions/Settings/importListExclusions.js @@ -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 diff --git a/frontend/src/Store/Actions/blocklistActions.js b/frontend/src/Store/Actions/blocklistActions.js index 87ffe7f7c..e188a0380 100644 --- a/frontend/src/Store/Actions/blocklistActions.js +++ b/frontend/src/Store/Actions/blocklistActions.js @@ -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'; diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 3e773eca8..45d858249 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -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'; diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index dff490d12..5f91318ad 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -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'; diff --git a/frontend/src/Store/Actions/systemActions.js b/frontend/src/Store/Actions/systemActions.js index 92360b589..0f2410846 100644 --- a/frontend/src/Store/Actions/systemActions.js +++ b/frontend/src/Store/Actions/systemActions.js @@ -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'; diff --git a/frontend/src/Store/Actions/wantedActions.js b/frontend/src/Store/Actions/wantedActions.js index 21bfcd3c0..bb39416aa 100644 --- a/frontend/src/Store/Actions/wantedActions.js +++ b/frontend/src/Store/Actions/wantedActions.js @@ -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'; diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts index 6a80e172b..dd5071b9d 100644 --- a/frontend/src/Store/Selectors/createCommandExecutingSelector.ts +++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.ts @@ -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; }); } diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js deleted file mode 100644 index 5cbb30085..000000000 --- a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.js +++ /dev/null @@ -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; - }); -} diff --git a/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts new file mode 100644 index 000000000..8e4c1f308 --- /dev/null +++ b/frontend/src/Utilities/Array/getIndexOfFirstCharacter.ts @@ -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; + }); +} diff --git a/frontend/src/Utilities/Command/findCommand.js b/frontend/src/Utilities/Command/findCommand.ts similarity index 60% rename from frontend/src/Utilities/Command/findCommand.js rename to frontend/src/Utilities/Command/findCommand.ts index cf7d5444a..fad9e59fe 100644 --- a/frontend/src/Utilities/Command/findCommand.js +++ b/frontend/src/Utilities/Command/findCommand.ts @@ -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) { return _.findLast(commands, (command) => { return isSameCommand(command.body, options); }); diff --git a/frontend/src/Utilities/Command/index.js b/frontend/src/Utilities/Command/index.ts similarity index 100% rename from frontend/src/Utilities/Command/index.js rename to frontend/src/Utilities/Command/index.ts diff --git a/frontend/src/Utilities/Command/isCommandComplete.js b/frontend/src/Utilities/Command/isCommandComplete.js deleted file mode 100644 index 558ab801b..000000000 --- a/frontend/src/Utilities/Command/isCommandComplete.js +++ /dev/null @@ -1,9 +0,0 @@ -function isCommandComplete(command) { - if (!command) { - return false; - } - - return command.status === 'complete'; -} - -export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandComplete.ts b/frontend/src/Utilities/Command/isCommandComplete.ts new file mode 100644 index 000000000..678023737 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandComplete.ts @@ -0,0 +1,11 @@ +import Command from 'Commands/Command'; + +function isCommandComplete(command: Command) { + if (!command) { + return false; + } + + return command.status === 'completed'; +} + +export default isCommandComplete; diff --git a/frontend/src/Utilities/Command/isCommandExecuting.js b/frontend/src/Utilities/Command/isCommandExecuting.ts similarity index 62% rename from frontend/src/Utilities/Command/isCommandExecuting.js rename to frontend/src/Utilities/Command/isCommandExecuting.ts index 8e637704e..7da31bdee 100644 --- a/frontend/src/Utilities/Command/isCommandExecuting.js +++ b/frontend/src/Utilities/Command/isCommandExecuting.ts @@ -1,4 +1,6 @@ -function isCommandExecuting(command) { +import Command from 'Commands/Command'; + +function isCommandExecuting(command?: Command) { if (!command) { return false; } diff --git a/frontend/src/Utilities/Command/isCommandFailed.js b/frontend/src/Utilities/Command/isCommandFailed.js deleted file mode 100644 index 00e5ccdf2..000000000 --- a/frontend/src/Utilities/Command/isCommandFailed.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Command/isCommandFailed.ts b/frontend/src/Utilities/Command/isCommandFailed.ts new file mode 100644 index 000000000..4e88b95c9 --- /dev/null +++ b/frontend/src/Utilities/Command/isCommandFailed.ts @@ -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; diff --git a/frontend/src/Utilities/Command/isSameCommand.js b/frontend/src/Utilities/Command/isSameCommand.js deleted file mode 100644 index d0acb24b5..000000000 --- a/frontend/src/Utilities/Command/isSameCommand.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Command/isSameCommand.ts b/frontend/src/Utilities/Command/isSameCommand.ts new file mode 100644 index 000000000..cbe18aa8f --- /dev/null +++ b/frontend/src/Utilities/Command/isSameCommand.ts @@ -0,0 +1,50 @@ +import { CommandBody } from 'Commands/Command'; + +function isSameCommand( + commandA: Partial, + commandB: Partial +) { + 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; diff --git a/frontend/src/Utilities/Constants/keyCodes.js b/frontend/src/Utilities/Constants/keyCodes.ts similarity index 100% rename from frontend/src/Utilities/Constants/keyCodes.js rename to frontend/src/Utilities/Constants/keyCodes.ts diff --git a/frontend/src/Utilities/Episode/updateEpisodes.js b/frontend/src/Utilities/Episode/updateEpisodes.ts similarity index 54% rename from frontend/src/Utilities/Episode/updateEpisodes.js rename to frontend/src/Utilities/Episode/updateEpisodes.ts index 80890b53f..d6a9e9eb4 100644 --- a/frontend/src/Utilities/Episode/updateEpisodes.js +++ b/frontend/src/Utilities/Episode/updateEpisodes.ts @@ -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 +) { + const data = episodes.reduce((result, item) => { if (episodeIds.indexOf(item.id) > -1) { result.push({ ...item, - ...options + ...options, }); } else { result.push(item); diff --git a/frontend/src/Utilities/Filter/findSelectedFilters.js b/frontend/src/Utilities/Filter/findSelectedFilters.js deleted file mode 100644 index 1c104073c..000000000 --- a/frontend/src/Utilities/Filter/findSelectedFilters.js +++ /dev/null @@ -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; -} diff --git a/frontend/src/Utilities/Filter/findSelectedFilters.ts b/frontend/src/Utilities/Filter/findSelectedFilters.ts new file mode 100644 index 000000000..89211f628 --- /dev/null +++ b/frontend/src/Utilities/Filter/findSelectedFilters.ts @@ -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; +} diff --git a/frontend/src/Utilities/Filter/getFilterValue.js b/frontend/src/Utilities/Filter/getFilterValue.ts similarity index 53% rename from frontend/src/Utilities/Filter/getFilterValue.js rename to frontend/src/Utilities/Filter/getFilterValue.ts index 70b0b51f1..95c078e48 100644 --- a/frontend/src/Utilities/Filter/getFilterValue.js +++ b/frontend/src/Utilities/Filter/getFilterValue.ts @@ -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) { diff --git a/frontend/src/Utilities/Number/convertToBytes.js b/frontend/src/Utilities/Number/convertToBytes.ts similarity index 76% rename from frontend/src/Utilities/Number/convertToBytes.js rename to frontend/src/Utilities/Number/convertToBytes.ts index 6c63fb117..53dbc27dd 100644 --- a/frontend/src/Utilities/Number/convertToBytes.js +++ b/frontend/src/Utilities/Number/convertToBytes.ts @@ -1,5 +1,4 @@ - -function convertToBytes(input, power, binaryPrefix) { +function convertToBytes(input: number, power: number, binaryPrefix: boolean) { const size = Number(input); if (isNaN(size)) { diff --git a/frontend/src/Utilities/Number/formatAge.js b/frontend/src/Utilities/Number/formatAge.js deleted file mode 100644 index a8f0e9f65..000000000 --- a/frontend/src/Utilities/Number/formatAge.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Number/formatAge.ts b/frontend/src/Utilities/Number/formatAge.ts new file mode 100644 index 000000000..e114cd9b4 --- /dev/null +++ b/frontend/src/Utilities/Number/formatAge.ts @@ -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; diff --git a/frontend/src/Utilities/Number/formatBytes.js b/frontend/src/Utilities/Number/formatBytes.ts similarity index 66% rename from frontend/src/Utilities/Number/formatBytes.js rename to frontend/src/Utilities/Number/formatBytes.ts index d4d389357..bccf7435a 100644 --- a/frontend/src/Utilities/Number/formatBytes.js +++ b/frontend/src/Utilities/Number/formatBytes.ts @@ -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, })}`; } diff --git a/frontend/src/Utilities/Number/padNumber.js b/frontend/src/Utilities/Number/padNumber.js deleted file mode 100644 index 53ae69cac..000000000 --- a/frontend/src/Utilities/Number/padNumber.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Number/padNumber.ts b/frontend/src/Utilities/Number/padNumber.ts new file mode 100644 index 000000000..8646e28d8 --- /dev/null +++ b/frontend/src/Utilities/Number/padNumber.ts @@ -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; diff --git a/frontend/src/Utilities/Number/roundNumber.js b/frontend/src/Utilities/Number/roundNumber.ts similarity index 59% rename from frontend/src/Utilities/Number/roundNumber.js rename to frontend/src/Utilities/Number/roundNumber.ts index e1a19018f..2035e11cc 100644 --- a/frontend/src/Utilities/Number/roundNumber.js +++ b/frontend/src/Utilities/Number/roundNumber.ts @@ -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; diff --git a/frontend/src/Utilities/Object/getErrorMessage.js b/frontend/src/Utilities/Object/getErrorMessage.ts similarity index 53% rename from frontend/src/Utilities/Object/getErrorMessage.js rename to frontend/src/Utilities/Object/getErrorMessage.ts index 1ba874660..d757ceec3 100644 --- a/frontend/src/Utilities/Object/getErrorMessage.js +++ b/frontend/src/Utilities/Object/getErrorMessage.ts @@ -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; } diff --git a/frontend/src/Utilities/Object/getRemovedItems.js b/frontend/src/Utilities/Object/getRemovedItems.js deleted file mode 100644 index df7ada3a8..000000000 --- a/frontend/src/Utilities/Object/getRemovedItems.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Object/hasDifferentItems.js b/frontend/src/Utilities/Object/hasDifferentItems.ts similarity index 70% rename from frontend/src/Utilities/Object/hasDifferentItems.js rename to frontend/src/Utilities/Object/hasDifferentItems.ts index d3c0046e4..0fd832769 100644 --- a/frontend/src/Utilities/Object/hasDifferentItems.js +++ b/frontend/src/Utilities/Object/hasDifferentItems.ts @@ -1,4 +1,10 @@ -function hasDifferentItems(prevItems, currentItems, idProp = 'id') { +import ModelBase from 'App/ModelBase'; + +function hasDifferentItems( + prevItems: T[], + currentItems: T[], + idProp: keyof T = 'id' +) { if (prevItems === currentItems) { return false; } diff --git a/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts similarity index 67% rename from frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js rename to frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts index e2acbc5c0..1235d4732 100644 --- a/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.js +++ b/frontend/src/Utilities/Object/hasDifferentItemsOrOrder.ts @@ -1,4 +1,10 @@ -function hasDifferentItemsOrOrder(prevItems, currentItems, idProp = 'id') { +import ModelBase from 'App/ModelBase'; + +function hasDifferentItemsOrOrder( + prevItems: T[], + currentItems: T[], + idProp: keyof T = 'id' +) { if (prevItems === currentItems) { return false; } diff --git a/frontend/src/Utilities/Quality/getQualities.js b/frontend/src/Utilities/Quality/getQualities.js deleted file mode 100644 index da09851ea..000000000 --- a/frontend/src/Utilities/Quality/getQualities.js +++ /dev/null @@ -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; - }, []); -} diff --git a/frontend/src/Utilities/Quality/getQualities.ts b/frontend/src/Utilities/Quality/getQualities.ts new file mode 100644 index 000000000..cf35b7992 --- /dev/null +++ b/frontend/src/Utilities/Quality/getQualities.ts @@ -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((acc, item) => { + if (item.quality) { + acc.push(item.quality); + } else { + const groupQualities = item.items.reduce((acc, i) => { + if (i.quality) { + acc.push(i.quality); + } + + return acc; + }, []); + + acc.push(...groupQualities); + } + + return acc; + }, []); +} diff --git a/frontend/src/Utilities/ResolutionUtility.js b/frontend/src/Utilities/ResolutionUtility.js deleted file mode 100644 index 358448ca9..000000000 --- a/frontend/src/Utilities/ResolutionUtility.js +++ /dev/null @@ -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; - } -}; diff --git a/frontend/src/Utilities/ResolutionUtility.ts b/frontend/src/Utilities/ResolutionUtility.ts new file mode 100644 index 000000000..4b0d58419 --- /dev/null +++ b/frontend/src/Utilities/ResolutionUtility.ts @@ -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; + }, +}; diff --git a/frontend/src/Utilities/Series/filterAlternateTitles.js b/frontend/src/Utilities/Series/filterAlternateTitles.js deleted file mode 100644 index 52a6723c1..000000000 --- a/frontend/src/Utilities/Series/filterAlternateTitles.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Series/filterAlternateTitles.ts b/frontend/src/Utilities/Series/filterAlternateTitles.ts new file mode 100644 index 000000000..a8dfad702 --- /dev/null +++ b/frontend/src/Utilities/Series/filterAlternateTitles.ts @@ -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; diff --git a/frontend/src/Utilities/Series/getNewSeries.js b/frontend/src/Utilities/Series/getNewSeries.ts similarity index 52% rename from frontend/src/Utilities/Series/getNewSeries.js rename to frontend/src/Utilities/Series/getNewSeries.ts index 0acbe93d7..67c0cd20c 100644 --- a/frontend/src/Utilities/Series/getNewSeries.js +++ b/frontend/src/Utilities/Series/getNewSeries.ts @@ -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; diff --git a/frontend/src/Utilities/Series/monitorNewItemsOptions.js b/frontend/src/Utilities/Series/monitorNewItemsOptions.ts similarity index 94% rename from frontend/src/Utilities/Series/monitorNewItemsOptions.js rename to frontend/src/Utilities/Series/monitorNewItemsOptions.ts index 49c948c7f..be49fd60b 100644 --- a/frontend/src/Utilities/Series/monitorNewItemsOptions.js +++ b/frontend/src/Utilities/Series/monitorNewItemsOptions.ts @@ -5,14 +5,14 @@ const monitorNewItemsOptions = [ key: 'all', get value() { return translate('MonitorAllSeasons'); - } + }, }, { key: 'none', get value() { return translate('MonitorNoNewSeasons'); - } - } + }, + }, ]; export default monitorNewItemsOptions; diff --git a/frontend/src/Utilities/Series/monitorOptions.js b/frontend/src/Utilities/Series/monitorOptions.ts similarity index 93% rename from frontend/src/Utilities/Series/monitorOptions.js rename to frontend/src/Utilities/Series/monitorOptions.ts index 6616cf7c4..5efcc51f4 100644 --- a/frontend/src/Utilities/Series/monitorOptions.js +++ b/frontend/src/Utilities/Series/monitorOptions.ts @@ -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; diff --git a/frontend/src/Utilities/Series/seriesTypes.js b/frontend/src/Utilities/Series/seriesTypes.ts similarity index 100% rename from frontend/src/Utilities/Series/seriesTypes.js rename to frontend/src/Utilities/Series/seriesTypes.ts diff --git a/frontend/src/Utilities/State/getNextId.js b/frontend/src/Utilities/State/getNextId.js deleted file mode 100644 index 204aac95a..000000000 --- a/frontend/src/Utilities/State/getNextId.js +++ /dev/null @@ -1,5 +0,0 @@ -function getNextId(items) { - return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; -} - -export default getNextId; diff --git a/frontend/src/Utilities/State/getNextId.ts b/frontend/src/Utilities/State/getNextId.ts new file mode 100644 index 000000000..c0cbdec97 --- /dev/null +++ b/frontend/src/Utilities/State/getNextId.ts @@ -0,0 +1,7 @@ +import ModelBase from 'App/ModelBase'; + +function getNextId(items: T[]) { + return items.reduce((id, x) => Math.max(id, x.id), 1) + 1; +} + +export default getNextId; diff --git a/frontend/src/Utilities/pages.js b/frontend/src/Utilities/State/pages.js similarity index 100% rename from frontend/src/Utilities/pages.js rename to frontend/src/Utilities/State/pages.js diff --git a/frontend/src/Utilities/serverSideCollectionHandlers.js b/frontend/src/Utilities/State/serverSideCollectionHandlers.js similarity index 100% rename from frontend/src/Utilities/serverSideCollectionHandlers.js rename to frontend/src/Utilities/State/serverSideCollectionHandlers.js diff --git a/frontend/src/Utilities/String/combinePath.js b/frontend/src/Utilities/String/combinePath.js deleted file mode 100644 index 9e4e9abe8..000000000 --- a/frontend/src/Utilities/String/combinePath.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function combinePath(isWindows, basePath, paths = []) { - const slash = isWindows ? '\\' : '/'; - - return `${basePath}${slash}${paths.join(slash)}`; -} diff --git a/frontend/src/Utilities/String/combinePath.ts b/frontend/src/Utilities/String/combinePath.ts new file mode 100644 index 000000000..d62b71628 --- /dev/null +++ b/frontend/src/Utilities/String/combinePath.ts @@ -0,0 +1,9 @@ +export default function combinePath( + isWindows: boolean, + basePath: string, + paths: string[] = [] +) { + const slash = isWindows ? '\\' : '/'; + + return `${basePath}${slash}${paths.join(slash)}`; +} diff --git a/frontend/src/Utilities/String/generateUUIDv4.js b/frontend/src/Utilities/String/generateUUIDv4.js deleted file mode 100644 index 51b15ec60..000000000 --- a/frontend/src/Utilities/String/generateUUIDv4.js +++ /dev/null @@ -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) - ); -} diff --git a/frontend/src/Utilities/String/isString.js b/frontend/src/Utilities/String/isString.ts similarity index 58% rename from frontend/src/Utilities/String/isString.js rename to frontend/src/Utilities/String/isString.ts index 1e7c3dff8..f67d10683 100644 --- a/frontend/src/Utilities/String/isString.js +++ b/frontend/src/Utilities/String/isString.ts @@ -1,3 +1,3 @@ -export default function isString(possibleString) { +export default function isString(possibleString: unknown) { return typeof possibleString === 'string' || possibleString instanceof String; } diff --git a/frontend/src/Utilities/String/naturalExpansion.js b/frontend/src/Utilities/String/naturalExpansion.ts similarity index 78% rename from frontend/src/Utilities/String/naturalExpansion.js rename to frontend/src/Utilities/String/naturalExpansion.ts index 2cdd69b86..3b7422933 100644 --- a/frontend/src/Utilities/String/naturalExpansion.js +++ b/frontend/src/Utilities/String/naturalExpansion.ts @@ -1,6 +1,6 @@ const regex = /\d+/g; -function naturalExpansion(input) { +function naturalExpansion(input: string) { if (!input) { return ''; } diff --git a/frontend/src/Utilities/String/parseUrl.js b/frontend/src/Utilities/String/parseUrl.ts similarity index 72% rename from frontend/src/Utilities/String/parseUrl.js rename to frontend/src/Utilities/String/parseUrl.ts index 93341f85f..47d6c5d98 100644 --- a/frontend/src/Utilities/String/parseUrl.js +++ b/frontend/src/Utilities/String/parseUrl.ts @@ -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 = _.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 = {}; } diff --git a/frontend/src/Utilities/String/split.js b/frontend/src/Utilities/String/split.js deleted file mode 100644 index 0e57e7545..000000000 --- a/frontend/src/Utilities/String/split.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/String/split.ts b/frontend/src/Utilities/String/split.ts new file mode 100644 index 000000000..2f6af7605 --- /dev/null +++ b/frontend/src/Utilities/String/split.ts @@ -0,0 +1,15 @@ +function split(input: string, separator = ',') { + if (!input) { + return []; + } + + return input.split(separator).reduce((acc, s) => { + if (s) { + acc.push(s); + } + + return acc; + }, []); +} + +export default split; diff --git a/frontend/src/Utilities/String/titleCase.js b/frontend/src/Utilities/String/titleCase.ts similarity index 81% rename from frontend/src/Utilities/String/titleCase.js rename to frontend/src/Utilities/String/titleCase.ts index 03573b9e3..72513cd09 100644 --- a/frontend/src/Utilities/String/titleCase.js +++ b/frontend/src/Utilities/String/titleCase.ts @@ -1,6 +1,6 @@ const regex = /\b\w+/g; -function titleCase(input) { +function titleCase(input: string | undefined) { if (!input) { return ''; } diff --git a/frontend/src/Utilities/Table/areAllSelected.js b/frontend/src/Utilities/Table/areAllSelected.js deleted file mode 100644 index 26102f89b..000000000 --- a/frontend/src/Utilities/Table/areAllSelected.js +++ /dev/null @@ -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 - }; -} diff --git a/frontend/src/Utilities/Table/areAllSelected.ts b/frontend/src/Utilities/Table/areAllSelected.ts new file mode 100644 index 000000000..ffb791ed1 --- /dev/null +++ b/frontend/src/Utilities/Table/areAllSelected.ts @@ -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, + }; +} diff --git a/frontend/src/Utilities/Table/getToggledRange.js b/frontend/src/Utilities/Table/getToggledRange.js deleted file mode 100644 index c0cc44fe5..000000000 --- a/frontend/src/Utilities/Table/getToggledRange.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Table/getToggledRange.ts b/frontend/src/Utilities/Table/getToggledRange.ts new file mode 100644 index 000000000..59a098e17 --- /dev/null +++ b/frontend/src/Utilities/Table/getToggledRange.ts @@ -0,0 +1,27 @@ +import ModelBase from 'App/ModelBase'; + +function getToggledRange( + 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; diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.js b/frontend/src/Utilities/Table/removeOldSelectedState.js deleted file mode 100644 index ff3a4fe11..000000000 --- a/frontend/src/Utilities/Table/removeOldSelectedState.js +++ /dev/null @@ -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 - }; -} diff --git a/frontend/src/Utilities/Table/removeOldSelectedState.ts b/frontend/src/Utilities/Table/removeOldSelectedState.ts new file mode 100644 index 000000000..8edb9e4dc --- /dev/null +++ b/frontend/src/Utilities/Table/removeOldSelectedState.ts @@ -0,0 +1,21 @@ +import ModelBase from 'App/ModelBase'; +import { SelectState } from 'Helpers/Hooks/useSelectState'; +import areAllSelected from './areAllSelected'; + +export default function removeOldSelectedState( + state: SelectState, + prevItems: T[] +) { + const selectedState = { + ...state.selectedState, + }; + + prevItems.forEach((item) => { + delete selectedState[item.id]; + }); + + return { + ...areAllSelected(selectedState), + selectedState, + }; +} diff --git a/frontend/src/Utilities/Table/selectAll.js b/frontend/src/Utilities/Table/selectAll.js deleted file mode 100644 index ffaaeaddf..000000000 --- a/frontend/src/Utilities/Table/selectAll.js +++ /dev/null @@ -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; diff --git a/frontend/src/Utilities/Table/selectAll.ts b/frontend/src/Utilities/Table/selectAll.ts new file mode 100644 index 000000000..bc7f8de8c --- /dev/null +++ b/frontend/src/Utilities/Table/selectAll.ts @@ -0,0 +1,19 @@ +import { SelectedState } from 'Helpers/Hooks/useSelectState'; + +function selectAll(selectedState: SelectedState, selected: boolean) { + const newSelectedState = Object.keys(selectedState).reduce< + Record + >((acc, item) => { + acc[Number(item)] = selected; + return acc; + }, {}); + + return { + allSelected: selected, + allUnselected: !selected, + lastToggled: null, + selectedState: newSelectedState, + }; +} + +export default selectAll; diff --git a/frontend/src/Utilities/Table/toggleSelected.js b/frontend/src/Utilities/Table/toggleSelected.ts similarity index 57% rename from frontend/src/Utilities/Table/toggleSelected.js rename to frontend/src/Utilities/Table/toggleSelected.ts index ec8870b0b..e3510572a 100644 --- a/frontend/src/Utilities/Table/toggleSelected.js +++ b/frontend/src/Utilities/Table/toggleSelected.ts @@ -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( + 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, }; } diff --git a/frontend/src/Utilities/browser.js b/frontend/src/Utilities/browser.ts similarity index 99% rename from frontend/src/Utilities/browser.js rename to frontend/src/Utilities/browser.ts index ff896e801..2ec0481f3 100644 --- a/frontend/src/Utilities/browser.js +++ b/frontend/src/Utilities/browser.ts @@ -3,7 +3,6 @@ import MobileDetect from 'mobile-detect'; const mobileDetect = new MobileDetect(window.navigator.userAgent); export function isMobile() { - return mobileDetect.mobile() != null; } diff --git a/frontend/src/Utilities/getPathWithUrlBase.js b/frontend/src/Utilities/getPathWithUrlBase.js deleted file mode 100644 index 60533d3d3..000000000 --- a/frontend/src/Utilities/getPathWithUrlBase.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getPathWithUrlBase(path) { - return `${window.Sonarr.urlBase}${path}`; -} diff --git a/frontend/src/Utilities/getPathWithUrlBase.ts b/frontend/src/Utilities/getPathWithUrlBase.ts new file mode 100644 index 000000000..c8335b899 --- /dev/null +++ b/frontend/src/Utilities/getPathWithUrlBase.ts @@ -0,0 +1,3 @@ +export default function getPathWithUrlBase(path: string) { + return `${window.Sonarr.urlBase}${path}`; +} diff --git a/frontend/src/Utilities/getUniqueElementId.js b/frontend/src/Utilities/getUniqueElementId.ts similarity index 100% rename from frontend/src/Utilities/getUniqueElementId.js rename to frontend/src/Utilities/getUniqueElementId.ts diff --git a/frontend/src/Utilities/pagePopulator.js b/frontend/src/Utilities/pagePopulator.ts similarity index 51% rename from frontend/src/Utilities/pagePopulator.js rename to frontend/src/Utilities/pagePopulator.ts index f58dbe803..45689f63a 100644 --- a/frontend/src/Utilities/pagePopulator.js +++ b/frontend/src/Utilities/pagePopulator.ts @@ -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; } diff --git a/frontend/src/Utilities/requestAction.js b/frontend/src/Utilities/requestAction.js index ed69cf5ad..86b17f695 100644 --- a/frontend/src/Utilities/requestAction.js +++ b/frontend/src/Utilities/requestAction.js @@ -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; }, {}); } diff --git a/frontend/src/Utilities/scrollLock.js b/frontend/src/Utilities/scrollLock.ts similarity index 85% rename from frontend/src/Utilities/scrollLock.js rename to frontend/src/Utilities/scrollLock.ts index cff50a34b..c63e8ff87 100644 --- a/frontend/src/Utilities/scrollLock.js +++ b/frontend/src/Utilities/scrollLock.ts @@ -8,6 +8,6 @@ export function isLocked() { return scrollLock; } -export function setScrollLock(locked) { +export function setScrollLock(locked: boolean) { scrollLock = locked; } diff --git a/frontend/src/Utilities/sectionTypes.js b/frontend/src/Utilities/sectionTypes.js deleted file mode 100644 index 5479b32b9..000000000 --- a/frontend/src/Utilities/sectionTypes.js +++ /dev/null @@ -1,6 +0,0 @@ -const sectionTypes = { - COLLECTION: 'collection', - MODEL: 'model' -}; - -export default sectionTypes; diff --git a/package.json b/package.json index c86ab70c5..cc608795a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 5cf57bfc8..55ee05650 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"