Files
suwayomi-material-you-webui/src/features/settings/ImageProcessing.utils.ts
2025-12-23 02:08:24 +01:00

202 lines
7.7 KiB
TypeScript

/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { d } from 'koration';
import {
ImageProcessingTargetMode,
TSettingsDownloadConversion,
TSettingsDownloadConversionHeader,
} from '@/features/settings/Settings.types.ts';
import {
DEFAULT_MIME_TYPE,
IMAGE_PROCESSING_CALL_TIMEOUT,
IMAGE_PROCESSING_COMPRESSION,
IMAGE_PROCESSING_CONNECT_TIMEOUT,
MIME_TYPE_PREFIX,
} from '@/features/settings/Settings.constants.ts';
import {
SettingsDownloadConversion,
SettingsDownloadConversionHeader,
SettingsDownloadConversionType,
} from '@/lib/graphql/generated/graphql.ts';
let COUNTER = 0;
const normalizeMimeType = (mimeType: string): string => mimeType.replace(MIME_TYPE_PREFIX, '');
export const isDefaultMimeType = (mimeType: string): boolean =>
normalizeMimeType(mimeType.toLowerCase().trim()) === DEFAULT_MIME_TYPE;
export const isDuplicateHeader = (
header: string,
index: number,
headers: SettingsDownloadConversionType['headers'],
): boolean => headers?.slice(0, index).some(({ name }) => name === header) ?? false;
const hasDuplicateHeaders = (headers: SettingsDownloadConversionType['headers']): boolean =>
headers?.some((header, index) => isDuplicateHeader(header.name, index, headers)) ?? false;
export const isDuplicateConversion = (
mimeType: string,
index: number,
conversions: SettingsDownloadConversion[],
): boolean => conversions.slice(0, index).some((conversion) => conversion.mimeType === mimeType);
export const isUrlTargetMode = (target: string): boolean => target !== '' && !!target.match(/^https?:\/\/.+/);
const isValidNumberSetting = (value: number | null | undefined, min: number, max: number): boolean =>
value == null || (value >= min && value <= max);
export const isValidCallTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_PROCESSING_CALL_TIMEOUT.min,
IMAGE_PROCESSING_CALL_TIMEOUT.max,
);
export const isValidConnectTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_PROCESSING_CONNECT_TIMEOUT.min,
IMAGE_PROCESSING_CONNECT_TIMEOUT.max,
);
export const isValidCompressionLevel = (compression: number | null | undefined): boolean =>
isValidNumberSetting(compression, IMAGE_PROCESSING_COMPRESSION.min, IMAGE_PROCESSING_COMPRESSION.max);
const isUnsetConversion = (mimeType: string, target: string): boolean => mimeType === '' && target === '';
const isInvalidTarget = (target: string, mode: ImageProcessingTargetMode, mimeType: string): boolean => {
if (isDefaultMimeType(mimeType) && !target) {
return false;
}
return mode === ImageProcessingTargetMode.URL && !isUrlTargetMode(target);
};
export const containsInvalidConversion = (conversions: TSettingsDownloadConversion[]): boolean =>
conversions.some(
({ mimeType, compressionLevel, target, callTimeout, connectTimeout, headers, mode }, index) =>
isUnsetConversion(mimeType, target) ||
isInvalidTarget(target, mode, mimeType) ||
!isValidCompressionLevel(compressionLevel) ||
!isValidCallTimeoutSetting(callTimeout) ||
!isValidConnectTimeoutSetting(connectTimeout) ||
isDuplicateConversion(mimeType, index, conversions) ||
hasDuplicateHeaders(headers),
);
export const getTargetMode = (target: string): ImageProcessingTargetMode => {
if (isUrlTargetMode(target)) {
return ImageProcessingTargetMode.URL;
}
return ImageProcessingTargetMode.IMAGE;
};
export const addStableIdToHeaders = (
headers: (SettingsDownloadConversionHeader | TSettingsDownloadConversionHeader)[],
): TSettingsDownloadConversionHeader[] =>
headers.map((header) => ({
// eslint-disable-next-line no-plusplus
id: (header as TSettingsDownloadConversionHeader).id ?? COUNTER++,
...header,
}));
export const addStableIdToConversions = (
conversions: (SettingsDownloadConversion | TSettingsDownloadConversion)[],
): TSettingsDownloadConversion[] =>
conversions.map((conversion) => ({
// eslint-disable-next-line no-plusplus
id: (conversion as TSettingsDownloadConversion).id ?? COUNTER++,
...conversion,
mode: getTargetMode(conversion.target),
headers: conversion.headers ? addStableIdToHeaders(conversion.headers) : null,
}));
export const normalizeConversions = (conversions: TSettingsDownloadConversion[]): TSettingsDownloadConversion[] =>
conversions.map((conversion) => ({
...conversion,
mimeType: normalizeMimeType(conversion.mimeType),
target: normalizeMimeType(conversion.target),
}));
const toValidServerMimeType = (mimeType: string): string => {
if (isDefaultMimeType(mimeType)) {
return DEFAULT_MIME_TYPE;
}
return `${MIME_TYPE_PREFIX}${mimeType}`;
};
export const toValidServerConversions = (conversions: TSettingsDownloadConversion[]): SettingsDownloadConversion[] =>
conversions
.filter(({ mimeType, target }) => !!mimeType && !!target)
.map(({ id, mode, ...conversion }) => ({
...conversion,
mimeType: toValidServerMimeType(conversion.mimeType),
target: isUrlTargetMode(conversion.target) ? conversion.target : `${MIME_TYPE_PREFIX}${conversion.target}`,
headers: conversion.headers?.map(({ id: headerId, ...header }) => header) ?? null,
}));
export const maybeAddDefault = (conversions: TSettingsDownloadConversion[]): TSettingsDownloadConversion[] => {
const isDefaultDefined = conversions.some(({ mimeType }) => isDefaultMimeType(mimeType));
return [
...(isDefaultDefined
? []
: [
{
id: -1,
mode: ImageProcessingTargetMode.IMAGE,
mimeType: DEFAULT_MIME_TYPE,
target: '',
compressionLevel: null,
headers: null,
callTimeout: null,
connectTimeout: null,
},
]),
...conversions,
];
};
const normalizeHeaders = (
headers: SettingsDownloadConversionType['headers'],
): SettingsDownloadConversionType['headers'] => headers?.filter(({ name }) => name !== '');
export const didUpdateConversions = (
conversions: TSettingsDownloadConversion[],
tmpConversions: TSettingsDownloadConversion[],
): boolean => {
if (conversions.length !== tmpConversions.length) {
return true;
}
return conversions.some(({ mimeType, target, compressionLevel, callTimeout, connectTimeout, headers }, index) => {
const tmpConversion = tmpConversions[index];
const orgHeaders = normalizeHeaders(headers);
const tmpHeaders = normalizeHeaders(tmpConversion.headers);
return (
normalizeMimeType(mimeType) !== tmpConversion.mimeType ||
normalizeMimeType(target) !== tmpConversion.target ||
compressionLevel !== tmpConversion.compressionLevel ||
callTimeout !== tmpConversion.callTimeout ||
connectTimeout !== tmpConversion.connectTimeout ||
(orgHeaders?.length ?? 0) !== (tmpHeaders?.length ?? 0) ||
!!orgHeaders?.some(
(header, headerIndex) =>
header.name !== tmpHeaders?.[headerIndex]?.name ||
header.value !== tmpHeaders?.[headerIndex]?.value,
)
);
});
};