Add new "external image processing" settings for downloaded images

This commit is contained in:
schroda
2025-12-22 16:49:10 +01:00
parent 083586c567
commit 1b0d907708
6 changed files with 514 additions and 103 deletions

View File

@@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- (**Settings**) Add hikari connection pool database setting
- (**Settings**) Add OPDS CBZ MIME-Type setting
- (**Settings**) Add automatic backup data inclusion setting
- (**Settings**) Add "external image processing" settings for downloaded images
- (**Browse**) Add "open in webview" button in source browse page
### Changed
@@ -82,6 +83,7 @@ Thanks to everyone that contributed to this release
- (**General**) Add support for the suwayomi WebView
- (**Settings**) Add new OPDS server settings
- (**Settings**) Add new "simple login" authentication setting
- (**Settings**) Add "download conversion" settings
- (**Reader**) Add option to change auto scroll direction by using the scroll backward/forward hotkeys while auto scrolling is active
- (**Manga**) Display score and total chapters, if available, in the tracker search results
- (**Manga**) Add support for private track bindings

View File

@@ -214,10 +214,29 @@
"title": "Auto-download"
},
"conversion": {
"call_timeout": "Call timeout",
"compression_level": "Compression level",
"connect_timeout": "Connect timeout",
"description": "In case no MIME-Type is defined, the \"default\" one will be used for the conversion.\nSet \"{{value}}\" as the target type to disable conversion for a MIME-Type",
"headers": {
"button": "Headers ({{count}})",
"name": "Name",
"title": "Headers",
"value": "Value"
},
"mime_type": "MIME-Type",
"target": "MIME-Type target",
"target_modes": {
"image": {
"description": "Convert images to different formats",
"title": "Image"
},
"title": "Target mode",
"url": {
"description": "Image conversion will be handled by an external service",
"title": "URL"
}
},
"title": "Download conversion"
},
"delete_chapters": {
@@ -397,6 +416,8 @@
"label": {
"day_one": "Day",
"day_other": "Days",
"second_one": "Second",
"second_other": "Seconds",
"today": "Today",
"today_at": "Today at {{timeString}}",
"yesterday": "Yesterday",
@@ -1241,6 +1262,9 @@
"value": "Delete backups that are older than $t(global.date.value.label.day)"
}
},
"flags": {
"title": "Backup data"
},
"label": {
"interval": "Backup interval",
"time": "Backup time"
@@ -1250,9 +1274,6 @@
"description": "The path to the directory on the server where automated backups should get saved in",
"title": "Backup location"
}
},
"flags": {
"title": "Backup data"
}
},
"flag": {

View File

@@ -6,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { d } from 'koration';
export const DOWNLOAD_AHEAD = {
min: 2,
max: 10,
@@ -18,3 +20,15 @@ export const DOWNLOAD_CONVERSION_COMPRESSION = {
max: 1,
step: 0.01,
};
export const IMAGE_CONVERSION_CALL_TIMEOUT = {
min: d(10).seconds.inWholeSeconds,
max: d(10).minutes.inWholeSeconds,
step: d(10).seconds.inWholeSeconds,
};
export const IMAGE_CONVERSION_CONNECT_TIMEOUT = {
min: d(10).seconds.inWholeSeconds,
max: d(10).minutes.inWholeSeconds,
step: d(10).seconds.inWholeSeconds,
};

View File

@@ -16,40 +16,163 @@ import InputAdornment from '@mui/material/InputAdornment';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import { useTheme } from '@mui/material/styles';
import MenuItem from '@mui/material/MenuItem';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import { d } from 'koration';
import Collapse from '@mui/material/Collapse';
import { useElementSize } from '@mantine/hooks';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { DOWNLOAD_CONVERSION_COMPRESSION } from '@/features/downloads/Downloads.constants.ts';
import { SettingsDownloadConversion } from '@/lib/graphql/generated/graphql.ts';
import {
DOWNLOAD_CONVERSION_COMPRESSION,
IMAGE_CONVERSION_CALL_TIMEOUT,
IMAGE_CONVERSION_CONNECT_TIMEOUT,
} from '@/features/downloads/Downloads.constants.ts';
import {
Maybe,
SettingsDownloadConversion,
SettingsDownloadConversionHeader,
SettingsDownloadConversionType,
} from '@/lib/graphql/generated/graphql.ts';
import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx';
import { Select } from '@/base/components/inputs/Select.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
export type TSettingsDownloadConversionHeader = SettingsDownloadConversionHeader & {
/**
* The conversion object does not have a stable key, which causes issues when editing the settings
*/
id: number;
};
export type TSettingsDownloadConversion = Omit<SettingsDownloadConversion, 'headers'> & {
/**
* The conversion object does not have a stable key, which causes issues when editing the settings
*/
id: number;
mode: TargetMode;
headers?: Maybe<TSettingsDownloadConversionHeader[]>;
};
let COUNTER = 0;
const INPUT_WIDTH = 250;
const DEFAULT_MIME_TYPE = 'default';
const MIME_TYPE_PREFIX = 'image/';
const DEFAULT_FOCUS_INDEX = -1;
enum TargetMode {
IMAGE = 'image',
URL = 'url',
}
const TARGET_MODES = Object.values(TargetMode);
const TARGET_MODES_TO_TRANSLATION_KEY: { [flavor in TargetMode]: SelectSettingValueDisplayInfo } = {
[TargetMode.IMAGE]: {
text: 'download.settings.conversion.target_modes.image.title',
description: 'download.settings.conversion.target_modes.image.description',
},
[TargetMode.URL]: {
text: 'download.settings.conversion.target_modes.url.title',
description: 'download.settings.conversion.target_modes.image.description',
},
};
export const TARGET_MODES_SELECT_VALUES: SelectSettingValue<TargetMode>[] = TARGET_MODES.map((mode) => [
mode,
TARGET_MODES_TO_TRANSLATION_KEY[mode],
]);
const normalizeMimeType = (mimeType: string): string => mimeType.replace(MIME_TYPE_PREFIX, '');
const isDefaultMimeType = (mimeType: string): boolean =>
normalizeMimeType(mimeType.toLowerCase().trim()) === DEFAULT_MIME_TYPE;
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;
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);
const isValidCallTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_CONVERSION_CALL_TIMEOUT.min,
IMAGE_CONVERSION_CALL_TIMEOUT.max,
);
const isValidConnectTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_CONVERSION_CONNECT_TIMEOUT.min,
IMAGE_CONVERSION_CONNECT_TIMEOUT.max,
);
const isValidCompressionLevel = (compression: number | null | undefined): boolean =>
compression == null ||
(compression >= DOWNLOAD_CONVERSION_COMPRESSION.min && compression <= DOWNLOAD_CONVERSION_COMPRESSION.max);
isValidNumberSetting(compression, DOWNLOAD_CONVERSION_COMPRESSION.min, DOWNLOAD_CONVERSION_COMPRESSION.max);
const isUnsetConversion = (mimeType: string, target: string): boolean => mimeType === '' && target === '';
const containsInvalidConversion = (conversions: SettingsDownloadConversion[]): boolean =>
const isInvalidTarget = (target: string, mode: TargetMode, mimeType: string): boolean => {
if (isDefaultMimeType(mimeType) && !target) {
return false;
}
return mode === TargetMode.URL && !isUrlTargetMode(target);
};
const containsInvalidConversion = (conversions: TSettingsDownloadConversion[]): boolean =>
conversions.some(
({ mimeType, compressionLevel, target }, index) =>
({ mimeType, compressionLevel, target, callTimeout, connectTimeout, headers, mode }, index) =>
isUnsetConversion(mimeType, target) ||
isInvalidTarget(target, mode, mimeType) ||
!isValidCompressionLevel(compressionLevel) ||
isDuplicateConversion(mimeType, index, conversions),
!isValidCallTimeoutSetting(callTimeout) ||
!isValidConnectTimeoutSetting(connectTimeout) ||
isDuplicateConversion(mimeType, index, conversions) ||
hasDuplicateHeaders(headers),
);
const normalizeConversions = (conversions: SettingsDownloadConversion[]): SettingsDownloadConversion[] =>
const getTargetMode = (target: string): TargetMode => {
if (isUrlTargetMode(target)) {
return TargetMode.URL;
}
return TargetMode.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,
}));
const normalizeConversions = (conversions: TSettingsDownloadConversion[]): TSettingsDownloadConversion[] =>
conversions.map((conversion) => ({
...conversion,
mimeType: normalizeMimeType(conversion.mimeType),
@@ -64,16 +187,17 @@ const toValidServerMimeType = (mimeType: string): string => {
return `${MIME_TYPE_PREFIX}${mimeType}`;
};
const toValidServerConversions = (conversions: SettingsDownloadConversion[]): SettingsDownloadConversion[] =>
const toValidServerConversions = (conversions: TSettingsDownloadConversion[]): SettingsDownloadConversion[] =>
conversions
.filter(({ mimeType, target }) => !!mimeType && !!target)
.map((conversion) => ({
.map(({ id, mode, ...conversion }) => ({
...conversion,
mimeType: toValidServerMimeType(conversion.mimeType),
target: `${MIME_TYPE_PREFIX}${conversion.target}`,
target: isUrlTargetMode(conversion.target) ? conversion.target : `${MIME_TYPE_PREFIX}${conversion.target}`,
headers: conversion.headers?.map(({ id: headerId, ...header }) => header) ?? null,
}));
const maybeAddDefault = (conversions: SettingsDownloadConversion[]) => {
const maybeAddDefault = (conversions: TSettingsDownloadConversion[]): TSettingsDownloadConversion[] => {
const isDefaultDefined = conversions.some(({ mimeType }) => isDefaultMimeType(mimeType));
return [
@@ -81,30 +205,50 @@ const maybeAddDefault = (conversions: SettingsDownloadConversion[]) => {
? []
: [
{
id: -1,
mode: TargetMode.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 !== '');
const didUpdateConversions = (
conversions: SettingsDownloadConversion[],
tmpConversions: SettingsDownloadConversion[],
conversions: TSettingsDownloadConversion[],
tmpConversions: TSettingsDownloadConversion[],
): boolean => {
if (conversions.length !== tmpConversions.length) {
return true;
}
return conversions.some(({ mimeType, target, compressionLevel }, index) => {
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
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,
)
);
});
};
@@ -116,6 +260,7 @@ const MimeTypeTextField = ({
label,
value,
onUpdate,
mode,
}: {
shouldAutoFocus: boolean;
isDefault: boolean;
@@ -123,9 +268,14 @@ const MimeTypeTextField = ({
label: string;
value: string;
onUpdate: (value: string) => void;
mode: TargetMode;
}) => {
const { t } = useTranslation();
const isImageMode = mode === TargetMode.IMAGE;
const isValidUrl = value === '' || isUrlTargetMode(value);
const isValid = !isDuplicate && (isImageMode || isValidUrl);
return (
<TextField
sx={{ width: INPUT_WIDTH }}
@@ -133,11 +283,13 @@ const MimeTypeTextField = ({
label={label}
value={value}
disabled={isDefault}
error={isDuplicate}
helperText={isDuplicate && t('global.error.label.invalid_input')}
error={!isValid}
helperText={!isValid && t('global.error.label.invalid_input')}
slotProps={{
input: {
startAdornment: <InputAdornment position="start">{MIME_TYPE_PREFIX}</InputAdornment>,
startAdornment: (
<InputAdornment position="start">{isImageMode ? MIME_TYPE_PREFIX : null}</InputAdornment>
),
},
}}
onChange={(e) => onUpdate(e.target.value.trim())}
@@ -145,26 +297,19 @@ const MimeTypeTextField = ({
);
};
const Conversion = ({
shouldAutoFocusMimeTypeTextField,
conversion: { mimeType, target, compressionLevel },
setFocusMimeTypeTextField,
const Header = ({
id,
name,
value,
onChange,
isDuplicate,
}: {
shouldAutoFocusMimeTypeTextField: boolean;
conversion: SettingsDownloadConversion;
setFocusMimeTypeTextField: (focus: boolean) => void;
onChange: (newConversion: SettingsDownloadConversion | null) => void;
isDuplicate: boolean;
}) => {
onChange: (header: TSettingsDownloadConversionHeader | null) => void;
} & TSettingsDownloadConversionHeader) => {
const { t } = useTranslation();
const theme = useTheme();
const isCompressionLevelValid = isValidCompressionLevel(compressionLevel);
const isDefault = isDefaultMimeType(mimeType) && !isDuplicate;
const isDisabled = isDefault && !target && compressionLevel == null;
return (
<Stack
sx={{
@@ -172,78 +317,49 @@ const Conversion = ({
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
pt: 1,
}}
>
<Stack
sx={{
gap: 1,
flexDirection: 'row',
alignItems: 'baseline',
flexWrap: 'wrap',
gap: 2,
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
width: '100%',
},
}}
>
<MimeTypeTextField
shouldAutoFocus={shouldAutoFocusMimeTypeTextField}
isDefault={isDefault}
isDuplicate={isDuplicate}
label={t('download.settings.conversion.mime_type')}
value={mimeType}
onUpdate={(value) => {
setFocusMimeTypeTextField(true);
<TextField
autoFocus
sx={{ width: INPUT_WIDTH }}
label={t('download.settings.conversion.headers.name')}
value={name}
error={isDuplicate}
helperText={isDuplicate && t('global.error.label.invalid_input')}
onChange={(e) =>
onChange({
mimeType: value,
target,
compressionLevel,
});
}}
/>
<TypographyMaxLines sx={{ mx: 1 }}></TypographyMaxLines>
<MimeTypeTextField
shouldAutoFocus={false}
isDefault={false}
isDuplicate={false}
label={t('download.settings.conversion.target')}
value={target}
onUpdate={(value) =>
onChange({
mimeType,
target: value,
compressionLevel,
id,
name: e.target.value,
value,
})
}
/>
<TextField
sx={{ width: INPUT_WIDTH }}
label={t('download.settings.conversion.compression_level')}
value={compressionLevel ?? ''}
type="number"
error={!isCompressionLevelValid}
helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: DOWNLOAD_CONVERSION_COMPRESSION,
},
}}
onChange={(e) => {
label={t('download.settings.conversion.headers.value')}
value={value}
onChange={(e) =>
onChange({
mimeType,
target,
compressionLevel: e.target.value ? Number(e.target.value) : undefined,
});
}}
id,
name,
value: e.target.value,
})
}
/>
</Stack>
<CustomTooltip disabled={isDisabled} title={t('chapter.action.download.delete.label.action')}>
<CustomTooltip disabled={false} title={t('chapter.action.download.delete.label.action')}>
<IconButton
disabled={isDisabled}
onClick={() => {
setFocusMimeTypeTextField(false);
onChange(null);
}}
>
@@ -254,6 +370,258 @@ const Conversion = ({
);
};
const Headers = ({
open,
headers,
onChange,
}: {
open: boolean;
headers: Maybe<TSettingsDownloadConversionHeader[]> | undefined;
onChange: (headers: Maybe<TSettingsDownloadConversionHeader[]>) => void;
}) => {
const { t } = useTranslation();
return (
<Collapse in={open}>
<Stack sx={{ justifyContent: 'start', gap: 2 }}>
<Typography>{t('download.settings.conversion.headers.title')}</Typography>
{headers?.map((header, index) => (
<Header
// eslint-disable-next-line react/no-array-index-key
key={index}
{...header}
isDuplicate={isDuplicateHeader(header.name, index, headers)}
onChange={(updatedHeader) => {
const isDeletion = updatedHeader == null;
if (isDeletion) {
onChange(headers?.toSpliced(index, 1));
return;
}
onChange((headers ?? []).toSpliced(index, 1, updatedHeader));
}}
/>
))}
<Button
onClick={() => onChange([...(headers ?? []), ...addStableIdToHeaders([{ name: '', value: '' }])])}
variant="contained"
sx={{ width: 'fit-content' }}
>
Add
</Button>
</Stack>
</Collapse>
);
};
const Conversion = ({
conversion,
conversion: { mimeType, target, compressionLevel, headers, callTimeout, connectTimeout },
onChange,
isDuplicate,
}: {
conversion: TSettingsDownloadConversion;
onChange: (newConversion: TSettingsDownloadConversion | null) => void;
isDuplicate: boolean;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { ref: textFieldRef, height: textFieldHeight } = useElementSize();
const [targetMode, setTargetMode] = useState(getTargetMode(target));
const [areHeadersCollapsed, setAreHeadersCollapsed] = useState(true);
const isImageMode = targetMode === TargetMode.IMAGE;
const isCallTimeoutValid = isValidCallTimeoutSetting(callTimeout);
const isConnectTimeoutValid = isValidConnectTimeoutSetting(connectTimeout);
const isCompressionLevelValid = isValidCompressionLevel(compressionLevel);
const isDefault = isDefaultMimeType(mimeType) && !isDuplicate;
const isDisabled = isDefault && !target && compressionLevel == null;
return (
<Stack sx={{ gap: 2 }}>
<Stack
sx={{
gap: 1,
flexDirection: 'row',
flexWrap: 'nowrap',
alignItems: 'center',
pt: 1,
}}
>
<Stack
sx={{
gap: 1,
flexDirection: 'row',
alignItems: 'baseline',
flexWrap: 'wrap',
[theme.breakpoints.down('md')]: {
flexDirection: 'column',
width: '100%',
},
}}
>
<MimeTypeTextField
mode={TargetMode.IMAGE}
shouldAutoFocus
isDefault={isDefault}
isDuplicate={isDuplicate}
label={t('download.settings.conversion.mime_type')}
value={mimeType}
onUpdate={(value) => {
onChange({
...conversion,
mimeType: value,
});
}}
/>
<TypographyMaxLines sx={{ mx: 1 }}></TypographyMaxLines>
<FormControl sx={{ minWidth: '100px' }}>
<InputLabel id="image-conversion-target-mode-label">
{t('download.settings.conversion.target_modes.title')}
</InputLabel>
<Select
ref={textFieldRef}
id="image-conversion-target-mode"
labelId="image-conversion-target-mode-label"
label={t('download.settings.conversion.target_modes.title')}
value={targetMode}
onChange={(e) => setTargetMode(e.target.value)}
>
{TARGET_MODES_SELECT_VALUES.map(([selectValue, { text: selectText }]) => (
<MenuItem key={selectValue} value={selectValue}>
{t(selectText as TranslationKey)}
</MenuItem>
))}
</Select>
</FormControl>
<MimeTypeTextField
mode={targetMode}
shouldAutoFocus={false}
isDefault={false}
isDuplicate={false}
label={t('download.settings.conversion.target')}
value={target}
onUpdate={(value) =>
onChange({
...conversion,
target: value,
})
}
/>
{isImageMode ? (
<TextField
sx={{ width: INPUT_WIDTH }}
label={t('download.settings.conversion.compression_level')}
value={compressionLevel ?? ''}
type="number"
error={!isCompressionLevelValid}
helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: DOWNLOAD_CONVERSION_COMPRESSION,
},
}}
onChange={(e) => {
onChange({
...conversion,
compressionLevel: e.target.value ? Number(e.target.value) : null,
});
}}
/>
) : (
<>
<TextField
sx={{ width: INPUT_WIDTH }}
label={t('download.settings.conversion.call_timeout')}
value={callTimeout ? d(callTimeout).seconds.inWholeSeconds : ''}
type="number"
error={!isCallTimeoutValid}
helperText={!isCallTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: IMAGE_CONVERSION_CALL_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
</InputAdornment>
),
},
}}
onChange={(e) => {
onChange({
...conversion,
callTimeout: e.target.value
? d(Number(e.target.value)).seconds.toISOString()
: null,
});
}}
/>
<TextField
sx={{ width: INPUT_WIDTH }}
label={t('download.settings.conversion.connect_timeout')}
value={connectTimeout ? d(connectTimeout).seconds.inWholeSeconds : ''}
type="number"
error={!isConnectTimeoutValid}
helperText={!isConnectTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: IMAGE_CONVERSION_CONNECT_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
</InputAdornment>
),
},
}}
onChange={(e) => {
onChange({
...conversion,
connectTimeout: e.target.value
? d(Number(e.target.value)).seconds.toISOString()
: null,
});
}}
/>
<Button
onClick={() => setAreHeadersCollapsed(!areHeadersCollapsed)}
variant={areHeadersCollapsed ? 'outlined' : 'contained'}
sx={{ height: textFieldHeight }}
>
{t('download.settings.conversion.headers.button', {
count: headers?.length ?? 0,
})}
</Button>
</>
)}
</Stack>
<CustomTooltip disabled={isDisabled} title={t('chapter.action.download.delete.label.action')}>
<IconButton
disabled={isDisabled}
onClick={() => {
onChange(null);
}}
>
<DeleteIcon />
</IconButton>
</CustomTooltip>
</Stack>
<Headers
open={!areHeadersCollapsed}
headers={headers}
onChange={(updatedHeaders) =>
onChange({
...conversion,
headers: updatedHeaders,
})
}
/>
</Stack>
);
};
export const DownloadConversionSetting = ({
conversions,
updateSetting,
@@ -263,12 +631,15 @@ export const DownloadConversionSetting = ({
}) => {
const { t } = useTranslation();
const [tmpConversions, setTmpConversions] = useState(normalizeConversions(maybeAddDefault(conversions)));
const [focusedMimeTypeTextFieldIndex, setFocusedMimeTypeTextFieldIndex] = useState(DEFAULT_FOCUS_INDEX);
const [tmpConversions, setTmpConversions] = useState(
normalizeConversions(maybeAddDefault(addStableIdToConversions(conversions))),
);
const hasInvalidConversion = containsInvalidConversion(tmpConversions);
const hasChanged = didUpdateConversions(normalizeConversions(maybeAddDefault(conversions)), tmpConversions);
const hasChanged = didUpdateConversions(
normalizeConversions(maybeAddDefault(addStableIdToConversions(conversions))),
tmpConversions,
);
const onSubmit = async () => {
try {
@@ -286,18 +657,12 @@ export const DownloadConversionSetting = ({
const { mimeType } = conversion;
const isDuplicate = isDuplicateConversion(mimeType, index, tmpConversions);
const shouldAutoFocusMimeTypeTextField = index === focusedMimeTypeTextFieldIndex;
return (
<Conversion
// eslint-disable-next-line react/no-array-index-key
key={`${mimeType}-${index}`}
key={conversion.id}
conversion={conversion}
isDuplicate={isDuplicate}
setFocusMimeTypeTextField={(focus) =>
setFocusedMimeTypeTextFieldIndex(focus ? index : DEFAULT_FOCUS_INDEX)
}
shouldAutoFocusMimeTypeTextField={shouldAutoFocusMimeTypeTextField}
onChange={(newConversion) => {
setTmpConversions((prev) =>
maybeAddDefault(
@@ -312,13 +677,16 @@ export const DownloadConversionSetting = ({
<Stack
direction="row"
sx={{
gap: 2,
gap: 1,
}}
>
<Button
variant="outlined"
onClick={() => {
setTmpConversions((prev) => [...prev, { mimeType: '', target: '' }]);
setTmpConversions((prev) => [
...prev,
...addStableIdToConversions([{ mimeType: '', target: '' }]),
]);
}}
>
{t('global.button.add')}

View File

@@ -3931,26 +3931,26 @@ export type WebuiUpdateSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type WebuiUpdateSubscription = { __typename?: 'Subscription', webUIUpdateStatusChange: { __typename?: 'WebUIUpdateStatus', progress: number, state: UpdateState, info: { __typename?: 'WebUIUpdateInfo', channel: WebUiChannel, tag: string } } };
export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> };
export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null, callTimeout?: string | null, connectTimeout?: string | null, headers?: Array<{ __typename?: 'SettingsDownloadConversionHeaderType', name: string, value: string }> | null }> };
export type ResetServerSettingsMutationVariables = Exact<{
input: ResetSettingsInput;
}>;
export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null, callTimeout?: string | null, connectTimeout?: string | null, headers?: Array<{ __typename?: 'SettingsDownloadConversionHeaderType', name: string, value: string }> | null }> } } };
export type UpdateServerSettingsMutationVariables = Exact<{
input: SetSettingsInput;
}>;
export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null, callTimeout?: string | null, connectTimeout?: string | null, headers?: Array<{ __typename?: 'SettingsDownloadConversionHeaderType', name: string, value: string }> | null }> } } };
export type GetServerSettingsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } };
export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: string, jwtRefreshExpiry: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, autoBackupIncludeCategories: boolean, autoBackupIncludeChapters: boolean, autoBackupIncludeClientData: boolean, autoBackupIncludeHistory: boolean, autoBackupIncludeManga: boolean, autoBackupIncludeServerSettings: boolean, autoBackupIncludeTracking: boolean, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, opdsCbzMimetype: CbzMediaType, koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod, koreaderSyncStrategyBackward: KoreaderSyncConflictStrategy, koreaderSyncStrategyForward: KoreaderSyncConflictStrategy, koreaderSyncPercentageTolerance: number, databaseType: DatabaseType, databaseUrl: string, databaseUsername: string, databasePassword: string, useHikariConnectionPool: boolean, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null, callTimeout?: string | null, connectTimeout?: string | null, headers?: Array<{ __typename?: 'SettingsDownloadConversionHeaderType', name: string, value: string }> | null }> } };
export type SourceMetaFieldsFragment = { __typename?: 'SourceMetaType', sourceId: string, key: string, value: string };

View File

@@ -41,6 +41,12 @@ export const SERVER_SETTINGS = gql`
mimeType
target
compressionLevel
headers {
name
value
}
callTimeout
connectTimeout
}
# extensions