Convert Utilities to TypeScript

This commit is contained in:
Mark McDowall 2024-07-23 15:52:44 -07:00 committed by Mark McDowall
parent 76650af9fd
commit d46f4b2154
85 changed files with 614 additions and 412 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules\\typescript\\lib"
}

View File

@ -43,6 +43,13 @@ export interface AppSectionSchemaState<T> {
}; };
} }
export interface AppSectionItemSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: T;
}
export interface AppSectionItemState<T> { export interface AppSectionItemState<T> {
isFetching: boolean; isFetching: boolean;
isPopulated: boolean; isPopulated: boolean;

View File

@ -35,14 +35,14 @@ export interface PropertyFilter {
export interface Filter { export interface Filter {
key: string; key: string;
label: string; label: string;
filers: PropertyFilter[]; filters: PropertyFilter[];
} }
export interface CustomFilter { export interface CustomFilter {
id: number; id: number;
type: string; type: string;
label: string; label: string;
filers: PropertyFilter[]; filters: PropertyFilter[];
} }
export interface AppSectionState { export interface AppSectionState {

View File

@ -1,8 +1,8 @@
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionItemSchemaState,
AppSectionItemState, AppSectionItemState,
AppSectionSaveState, AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState, PagedAppSectionState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
import Language from 'Language/Language'; import Language from 'Language/Language';
@ -40,7 +40,7 @@ export interface NotificationAppState
export interface QualityProfilesAppState export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>, extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {} AppSectionItemSchemaState<QualityProfile> {}
export interface ImportListOptionsSettingsAppState export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>, extends AppSectionItemState<ImportListOptionsSettings>,

View File

@ -1,5 +1,16 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
export type CommandStatus =
| 'queued'
| 'started'
| 'completed'
| 'failed'
| 'aborted'
| 'cancelled'
| 'orphaned';
export type CommandResult = 'unknown' | 'successful' | 'unsuccessful';
export interface CommandBody { export interface CommandBody {
sendUpdatesToClient: boolean; sendUpdatesToClient: boolean;
updateScheduledTask: boolean; updateScheduledTask: boolean;
@ -15,6 +26,7 @@ export interface CommandBody {
seriesId?: number; seriesId?: number;
seriesIds?: number[]; seriesIds?: number[];
seasonNumber?: number; seasonNumber?: number;
[key: string]: string | number | boolean | undefined | number[] | undefined;
} }
interface Command extends ModelBase { interface Command extends ModelBase {
@ -23,8 +35,8 @@ interface Command extends ModelBase {
message: string; message: string;
body: CommandBody; body: CommandBody;
priority: string; priority: string;
status: string; status: CommandStatus;
result: string; result: CommandResult;
queued: string; queued: string;
started: string; started: string;
ended: string; ended: string;

View File

@ -1,7 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { Error } from 'App/State/AppSectionState';
import AppState from 'App/State/AppState'; import AppState from 'App/State/AppState';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
@ -21,21 +20,14 @@ import { CheckInputChanged } from 'typings/inputs';
import getQualities from 'Utilities/Quality/getQualities'; import getQualities from 'Utilities/Quality/getQualities';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
interface QualitySchemaState {
isFetching: boolean;
isPopulated: boolean;
error: Error;
items: Quality[];
}
function createQualitySchemaSelector() { function createQualitySchemaSelector() {
return createSelector( return createSelector(
(state: AppState) => state.settings.qualityProfiles, (state: AppState) => state.settings.qualityProfiles,
(qualityProfiles): QualitySchemaState => { (qualityProfiles) => {
const { isSchemaFetching, isSchemaPopulated, schemaError, schema } = const { isSchemaFetching, isSchemaPopulated, schemaError, schema } =
qualityProfiles; qualityProfiles;
const items = getQualities(schema.items) as Quality[]; const items = getQualities(schema.items);
return { return {
isFetching: isSchemaFetching, isFetching: isSchemaFetching,

View File

@ -2,6 +2,20 @@ import ModelBase from 'App/ModelBase';
import Language from 'Language/Language'; import Language from 'Language/Language';
export type SeriesType = 'anime' | 'daily' | 'standard'; 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 { export interface Image {
coverType: string; coverType: string;
@ -34,7 +48,15 @@ export interface Ratings {
export interface AlternateTitle { export interface AlternateTitle {
seasonNumber: number; seasonNumber: number;
sceneSeasonNumber?: number;
title: string; title: string;
sceneOrigin: 'unknown' | 'unknown:tvdb' | 'mixed' | 'tvdb';
}
export interface SeriesAddOptions {
monitor: SeriesMonitor;
searchForMissingEpisodes: boolean;
searchForCutoffUnmetEpisodes: boolean;
} }
interface Series extends ModelBase { interface Series extends ModelBase {
@ -48,6 +70,7 @@ interface Series extends ModelBase {
images: Image[]; images: Image[];
imdbId: string; imdbId: string;
monitored: boolean; monitored: boolean;
monitorNewItems: MonitorNewItems;
network: string; network: string;
originalLanguage: Language; originalLanguage: Language;
overview: string; overview: string;
@ -74,6 +97,7 @@ interface Series extends ModelBase {
useSceneNumbering: boolean; useSceneNumbering: boolean;
year: number; year: number;
isSaving?: boolean; isSaving?: boolean;
addOptions: SeriesAddOptions;
} }
export default Series; export default Series;

View File

@ -1,5 +1,5 @@
import pages from 'Utilities/pages'; import pages from 'Utilities/State/pages';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler'; import createFetchServerSideCollectionHandler from './createFetchServerSideCollectionHandler';
import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler'; import createSetServerSideCollectionFilterHandler from './createSetServerSideCollectionFilterHandler';
import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler'; import createSetServerSideCollectionPageHandler from './createSetServerSideCollectionPageHandler';

View File

@ -1,5 +1,5 @@
import pages from 'Utilities/pages';
import getSectionState from 'Utilities/State/getSectionState'; import getSectionState from 'Utilities/State/getSectionState';
import pages from 'Utilities/State/pages';
function createSetServerSideCollectionPageHandler(section, page, fetchHandler) { function createSetServerSideCollectionPageHandler(section, page, fetchHandler) {
return function(getState, payload, dispatch) { return function(getState, payload, dispatch) {

View File

@ -5,7 +5,7 @@ import createServerSideCollectionHandlers from 'Store/Actions/Creators/createSer
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer'; import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer'; import createSetTableOptionReducer from 'Store/Actions/Creators/Reducers/createSetTableOptionReducer';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
// //
// Variables // Variables

View File

@ -3,7 +3,7 @@ import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { set, updateItem } from './baseActions'; import { set, updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';

View File

@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { updateItem } from './baseActions'; import { updateItem } from './baseActions';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';

View File

@ -6,7 +6,7 @@ import Icon from 'Components/Icon';
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { set, updateItem } from './baseActions'; import { set, updateItem } from './baseActions';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';

View File

@ -3,7 +3,7 @@ import { filterTypes, sortDirections } from 'Helpers/Props';
import { setAppValue } from 'Store/Actions/appActions'; import { setAppValue } from 'Store/Actions/appActions';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest'; import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import { pingServer } from './appActions'; import { pingServer } from './appActions';
import { set } from './baseActions'; import { set } from './baseActions';

View File

@ -1,7 +1,7 @@
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { filterTypes, sortDirections } from 'Helpers/Props'; import { filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; import serverSideCollectionHandlers from 'Utilities/State/serverSideCollectionHandlers';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler'; import createBatchToggleEpisodeMonitoredHandler from './Creators/createBatchToggleEpisodeMonitoredHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';

View File

@ -4,7 +4,7 @@ import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name: string, contraints = {}) { function createCommandExecutingSelector(name: string, contraints = {}) {
return createSelector(createCommandSelector(name, contraints), (command) => { return createSelector(createCommandSelector(name, contraints), (command) => {
return isCommandExecuting(command); return command ? isCommandExecuting(command) : false;
}); });
} }

View File

@ -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;
});
}

View 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;
});
}

View File

@ -1,7 +1,8 @@
import _ from 'lodash'; import _ from 'lodash';
import Command, { CommandBody } from 'Commands/Command';
import isSameCommand from './isSameCommand'; import isSameCommand from './isSameCommand';
function findCommand(commands, options) { function findCommand(commands: Command[], options: Partial<CommandBody>) {
return _.findLast(commands, (command) => { return _.findLast(commands, (command) => {
return isSameCommand(command.body, options); return isSameCommand(command.body, options);
}); });

View File

@ -1,9 +0,0 @@
function isCommandComplete(command) {
if (!command) {
return false;
}
return command.status === 'complete';
}
export default isCommandComplete;

View 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;

View File

@ -1,4 +1,6 @@
function isCommandExecuting(command) { import Command from 'Commands/Command';
function isCommandExecuting(command?: Command) {
if (!command) { if (!command) {
return false; return false;
} }

View File

@ -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;

View 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;

View File

@ -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;

View 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;

View File

@ -1,12 +1,17 @@
import _ from 'lodash'; import Episode from 'Episode/Episode';
import { update } from 'Store/Actions/baseActions'; import { update } from 'Store/Actions/baseActions';
function updateEpisodes(section, episodes, episodeIds, options) { function updateEpisodes(
const data = _.reduce(episodes, (result, item) => { section: string,
episodes: Episode[],
episodeIds: number[],
options: Partial<Episode>
) {
const data = episodes.reduce<Episode[]>((result, item) => {
if (episodeIds.indexOf(item.id) > -1) { if (episodeIds.indexOf(item.id) > -1) {
result.push({ result.push({
...item, ...item,
...options ...options,
}); });
} else { } else {
result.push(item); result.push(item);

View File

@ -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;
}

View 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;
}

View File

@ -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); const filter = filters.find((f) => f.key === filterKey);
if (!filter) { if (!filter) {

View File

@ -1,5 +1,4 @@
function convertToBytes(input: number, power: number, binaryPrefix: boolean) {
function convertToBytes(input, power, binaryPrefix) {
const size = Number(input); const size = Number(input);
if (isNaN(size)) { if (isNaN(size)) {

View File

@ -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;

View 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;

View File

@ -1,6 +1,10 @@
import { filesize } from 'filesize'; import { filesize } from 'filesize';
function formatBytes(input) { function formatBytes(input?: string | number) {
if (!input) {
return '';
}
const size = Number(input); const size = Number(input);
if (isNaN(size)) { if (isNaN(size)) {
@ -9,7 +13,7 @@ function formatBytes(input) {
return `${filesize(size, { return `${filesize(size, {
base: 2, base: 2,
round: 1 round: 1,
})}`; })}`;
} }

View File

@ -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;

View 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;

View File

@ -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); const multiplier = Math.pow(10, decimalPlaces);
return Math.round(input * multiplier) / multiplier; return Math.round(input * multiplier) / multiplier;

View File

@ -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) { if (!xhr || !xhr.responseJSON || !xhr.responseJSON.message) {
return fallbackErrorMessage; return fallbackErrorMessage;
} }

View File

@ -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;

View File

@ -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) { if (prevItems === currentItems) {
return false; return false;
} }

View File

@ -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) { if (prevItems === currentItems) {
return false; return false;
} }

View File

@ -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;
}, []);
}

View 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;
}, []);
}

View File

@ -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;
}
};

View 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;
},
};

View File

@ -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;

View 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;

View File

@ -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 { const {
rootFolderPath, rootFolderPath,
monitor, monitor,
@ -9,13 +26,13 @@ function getNewSeries(series, payload) {
seasonFolder, seasonFolder,
tags, tags,
searchForMissingEpisodes = false, searchForMissingEpisodes = false,
searchForCutoffUnmetEpisodes = false searchForCutoffUnmetEpisodes = false,
} = payload; } = payload;
const addOptions = { const addOptions = {
monitor, monitor,
searchForMissingEpisodes, searchForMissingEpisodes,
searchForCutoffUnmetEpisodes searchForCutoffUnmetEpisodes,
}; };
series.addOptions = addOptions; series.addOptions = addOptions;

View File

@ -5,14 +5,14 @@ const monitorNewItemsOptions = [
key: 'all', key: 'all',
get value() { get value() {
return translate('MonitorAllSeasons'); return translate('MonitorAllSeasons');
} },
}, },
{ {
key: 'none', key: 'none',
get value() { get value() {
return translate('MonitorNoNewSeasons'); return translate('MonitorNoNewSeasons');
} },
} },
]; ];
export default monitorNewItemsOptions; export default monitorNewItemsOptions;

View File

@ -5,68 +5,68 @@ const monitorOptions = [
key: 'all', key: 'all',
get value() { get value() {
return translate('MonitorAllEpisodes'); return translate('MonitorAllEpisodes');
} },
}, },
{ {
key: 'future', key: 'future',
get value() { get value() {
return translate('MonitorFutureEpisodes'); return translate('MonitorFutureEpisodes');
} },
}, },
{ {
key: 'missing', key: 'missing',
get value() { get value() {
return translate('MonitorMissingEpisodes'); return translate('MonitorMissingEpisodes');
} },
}, },
{ {
key: 'existing', key: 'existing',
get value() { get value() {
return translate('MonitorExistingEpisodes'); return translate('MonitorExistingEpisodes');
} },
}, },
{ {
key: 'recent', key: 'recent',
get value() { get value() {
return translate('MonitorRecentEpisodes'); return translate('MonitorRecentEpisodes');
} },
}, },
{ {
key: 'pilot', key: 'pilot',
get value() { get value() {
return translate('MonitorPilotEpisode'); return translate('MonitorPilotEpisode');
} },
}, },
{ {
key: 'firstSeason', key: 'firstSeason',
get value() { get value() {
return translate('MonitorFirstSeason'); return translate('MonitorFirstSeason');
} },
}, },
{ {
key: 'lastSeason', key: 'lastSeason',
get value() { get value() {
return translate('MonitorLastSeason'); return translate('MonitorLastSeason');
} },
}, },
{ {
key: 'monitorSpecials', key: 'monitorSpecials',
get value() { get value() {
return translate('MonitorSpecialEpisodes'); return translate('MonitorSpecialEpisodes');
} },
}, },
{ {
key: 'unmonitorSpecials', key: 'unmonitorSpecials',
get value() { get value() {
return translate('UnmonitorSpecialEpisodes'); return translate('UnmonitorSpecialEpisodes');
} },
}, },
{ {
key: 'none', key: 'none',
get value() { get value() {
return translate('MonitorNoEpisodes'); return translate('MonitorNoEpisodes');
} },
} },
]; ];
export default monitorOptions; export default monitorOptions;

View File

@ -1,5 +0,0 @@
function getNextId(items) {
return items.reduce((id, x) => Math.max(id, x.id), 1) + 1;
}
export default getNextId;

View 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;

View File

@ -1,5 +0,0 @@
export default function combinePath(isWindows, basePath, paths = []) {
const slash = isWindows ? '\\' : '/';
return `${basePath}${slash}${paths.join(slash)}`;
}

View File

@ -0,0 +1,9 @@
export default function combinePath(
isWindows: boolean,
basePath: string,
paths: string[] = []
) {
const slash = isWindows ? '\\' : '/';
return `${basePath}${slash}${paths.join(slash)}`;
}

View File

@ -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)
);
}

View File

@ -1,3 +1,3 @@
export default function isString(possibleString) { export default function isString(possibleString: unknown) {
return typeof possibleString === 'string' || possibleString instanceof String; return typeof possibleString === 'string' || possibleString instanceof String;
} }

View File

@ -1,6 +1,6 @@
const regex = /\d+/g; const regex = /\d+/g;
function naturalExpansion(input) { function naturalExpansion(input: string) {
if (!input) { if (!input) {
return ''; return '';
} }

View File

@ -4,13 +4,13 @@ import qs from 'qs';
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
const anchor = document.createElement('a'); const anchor = document.createElement('a');
export default function parseUrl(url) { export default function parseUrl(url: string) {
anchor.href = url; anchor.href = url;
// The `origin`, `password`, and `username` properties are unavailable in // The `origin`, `password`, and `username` properties are unavailable in
// Opera Presto. We synthesize `origin` if it's not present. While `password` // Opera Presto. We synthesize `origin` if it's not present. While `password`
// and `username` are ignored intentionally. // and `username` are ignored intentionally.
const properties = _.pick( const properties: Record<string, string | number | boolean | object> = _.pick(
anchor, anchor,
'hash', 'hash',
'host', 'host',
@ -23,11 +23,11 @@ export default function parseUrl(url) {
'search' 'search'
); );
properties.isAbsolute = (/^[\w:]*\/\//).test(url); properties.isAbsolute = /^[\w:]*\/\//.test(url);
if (properties.search) { if (properties.search) {
// Remove leading ? from querystring before parsing. // 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 { } else {
properties.params = {}; properties.params = {};
} }

View File

@ -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;

View 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;

View File

@ -1,6 +1,6 @@
const regex = /\b\w+/g; const regex = /\b\w+/g;
function titleCase(input) { function titleCase(input: string | undefined) {
if (!input) { if (!input) {
return ''; return '';
} }

View File

@ -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
};
}

View 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,
};
}

View File

@ -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;

View 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;

View File

@ -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
};
}

View 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,
};
}

View File

@ -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;

View 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;

View File

@ -1,11 +1,19 @@
import ModelBase from 'App/ModelBase';
import { SelectState } from 'Helpers/Hooks/useSelectState';
import areAllSelected from './areAllSelected'; import areAllSelected from './areAllSelected';
import getToggledRange from './getToggledRange'; import getToggledRange from './getToggledRange';
function toggleSelected(selectedState, items, id, selected, shiftKey) { function toggleSelected<T extends ModelBase>(
const lastToggled = selectedState.lastToggled; selectState: SelectState,
items: T[],
id: number,
selected: boolean,
shiftKey: boolean
) {
const lastToggled = selectState.lastToggled;
const nextSelectedState = { const nextSelectedState = {
...selectedState.selectedState, ...selectState.selectedState,
[id]: selected [id]: selected,
}; };
if (selected == null) { if (selected == null) {
@ -23,7 +31,7 @@ function toggleSelected(selectedState, items, id, selected, shiftKey) {
return { return {
...areAllSelected(nextSelectedState), ...areAllSelected(nextSelectedState),
lastToggled: id, lastToggled: id,
selectedState: nextSelectedState selectedState: nextSelectedState,
}; };
} }

View File

@ -3,7 +3,6 @@ import MobileDetect from 'mobile-detect';
const mobileDetect = new MobileDetect(window.navigator.userAgent); const mobileDetect = new MobileDetect(window.navigator.userAgent);
export function isMobile() { export function isMobile() {
return mobileDetect.mobile() != null; return mobileDetect.mobile() != null;
} }

View File

@ -1,3 +0,0 @@
export default function getPathWithUrlBase(path) {
return `${window.Sonarr.urlBase}${path}`;
}

View File

@ -0,0 +1,3 @@
export default function getPathWithUrlBase(path: string) {
return `${window.Sonarr.urlBase}${path}`;
}

View File

@ -1,19 +1,24 @@
let currentPopulator = null; type Populator = () => void;
let currentReasons = [];
export function registerPagePopulator(populator, reasons = []) { let currentPopulator: Populator | null = null;
let currentReasons: string[] = [];
export function registerPagePopulator(
populator: Populator,
reasons: string[] = []
) {
currentPopulator = populator; currentPopulator = populator;
currentReasons = reasons; currentReasons = reasons;
} }
export function unregisterPagePopulator(populator) { export function unregisterPagePopulator(populator: Populator) {
if (currentPopulator === populator) { if (currentPopulator === populator) {
currentPopulator = null; currentPopulator = null;
currentReasons = []; currentReasons = [];
} }
} }
export function repopulatePage(reason) { export function repopulatePage(reason: string) {
if (!currentPopulator) { if (!currentPopulator) {
return; return;
} }

View File

@ -1,18 +1,17 @@
import $ from 'jquery'; import $ from 'jquery';
import _ from 'lodash';
import createAjaxRequest from './createAjaxRequest'; import createAjaxRequest from './createAjaxRequest';
function flattenProviderData(providerData) { function flattenProviderData(providerData) {
return _.reduce(Object.keys(providerData), (result, key) => { return Object.keys(providerData).reduce((acc, key) => {
const property = providerData[key]; const property = providerData[key];
if (key === 'fields') { if (key === 'fields') {
result[key] = property; acc[key] = property;
} else { } else {
result[key] = property.value; acc[key] = property.value;
} }
return result; return acc;
}, {}); }, {});
} }

View File

@ -8,6 +8,6 @@ export function isLocked() {
return scrollLock; return scrollLock;
} }
export function setScrollLock(locked) { export function setScrollLock(locked: boolean) {
scrollLock = locked; scrollLock = locked;
} }

View File

@ -1,6 +0,0 @@
const sectionTypes = {
COLLECTION: 'collection',
MODEL: 'model'
};
export default sectionTypes;

View File

@ -92,6 +92,7 @@
"@babel/preset-react": "7.24.1", "@babel/preset-react": "7.24.1",
"@babel/preset-typescript": "7.24.1", "@babel/preset-typescript": "7.24.1",
"@types/lodash": "4.14.194", "@types/lodash": "4.14.194",
"@types/qs": "6.9.15",
"@types/react-lazyload": "3.2.0", "@types/react-lazyload": "3.2.0",
"@types/react-router-dom": "5.3.3", "@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1", "@types/react-text-truncate": "0.14.1",

View File

@ -1458,6 +1458,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== 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": "@types/react-dom@18.2.25":
version "18.2.25" version "18.2.25"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.25.tgz#2946a30081f53e7c8d585eb138277245caedc521"