Add "images" settings page

This commit is contained in:
schroda
2025-12-22 21:11:12 +01:00
parent 1b0d907708
commit cedb865a42
10 changed files with 155 additions and 98 deletions

View File

@@ -217,7 +217,7 @@
"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",
"description": "In case no MIME-Type is defined, the \"default\" one will be used for the processing.\nSet \"{{value}}\" as the target type to disable processing for a MIME-Type",
"headers": {
"button": "Headers ({{count}})",
"name": "Name",
@@ -237,7 +237,7 @@
"title": "URL"
}
},
"title": "Download conversion"
"title": "Downloaded image processing"
},
"delete_chapters": {
"label": {
@@ -1320,6 +1320,12 @@
"device": "Device"
}
},
"images": {
"processing": {
"title": "Image processing"
},
"title": "Images"
},
"label": {
"hide_library_entries": "Hide entries already in library",
"language_description": "Feel free to translate the project on",

View File

@@ -60,8 +60,9 @@ const { DownloadSettings } = loadable(
() => import('@/features/downloads/screens/DownloadSettings.tsx'),
lazyLoadFallback,
);
const { DownloadConversionSettings } = loadable(
() => import('@/features/downloads/screens/DownloadConversionSettings.tsx'),
const { ImagesSettings } = loadable(() => import('@/features/settings/screens/ImagesSettings.tsx'), lazyLoadFallback);
const { ImageProcessingSetting } = loadable(
() => import('@/features/settings/screens/ImageProcessingSetting.tsx'),
lazyLoadFallback,
);
const { ServerSettings } = loadable(() => import('@/features/settings/screens/ServerSettings.tsx'), lazyLoadFallback);
@@ -227,9 +228,25 @@ const MainApp = () => {
</Route>
<Route path={AppRoutes.settings.childRoutes.download.match}>
<Route index element={<DownloadSettings />} />
{/* TODO: deprecated - got moved to "settings/images/processing/downloads" */}
<Route
path={AppRoutes.settings.childRoutes.download.childRoutes.conversions.match}
element={<DownloadConversionSettings />}
element={
<Navigate
to={
AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads
.path
}
replace
/>
}
/>
</Route>
<Route path={AppRoutes.settings.childRoutes.images.match}>
<Route index element={<ImagesSettings />} />
<Route
path={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.match}
element={<ImageProcessingSetting />}
/>
</Route>
<Route path={AppRoutes.settings.childRoutes.backup.match} element={<Backup />} />
@@ -255,6 +272,7 @@ const MainApp = () => {
<Route path={AppRoutes.sources.childRoutes.searchAll.match} element={<SearchAll />} />
</Route>
<Route path={AppRoutes.extension.match}>
{/* TODO: deprecated - "source" and "extension" page got merged into "browse" */}
<Route
index
element={<Navigate to={AppRoutes.browse.path(BrowseTab.EXTENSIONS)} replace />}

View File

@@ -70,12 +70,23 @@ export const AppRoutes = {
match: 'download',
path: '/settings/download',
childRoutes: {
// TODO: deprecated - got moved to "settings/images/processing/downloads"
conversions: {
match: 'conversions',
path: '/settings/download/conversions',
},
},
},
images: {
match: 'images',
path: '/settings/images',
childRoutes: {
processingDownloads: {
match: 'processing/downloads',
path: '/settings/images/processing/downloads',
},
},
},
backup: {
match: 'backup',
path: '/settings/backup',

View File

@@ -6,29 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { d } from 'koration';
export const DOWNLOAD_AHEAD = {
min: 2,
max: 10,
default: 2,
step: 1,
};
export const DOWNLOAD_CONVERSION_COMPRESSION = {
min: 0,
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

@@ -1,52 +0,0 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { DownloadConversionSetting } from '@/features/downloads/components/DownloadConversionSetting.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { ServerSettings } from '@/features/settings/Settings.types.ts';
export const DownloadConversionSettings = () => {
const { t } = useTranslation();
useAppTitle(t('download.title.download'));
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
notifyOnNetworkStatusChange: true,
});
const [mutateSettings] = requestManager.useUpdateServerSettings();
const updateSetting = (value: ServerSettings['downloadConversions']): Promise<any> => {
const mutation = mutateSettings({ variables: { input: { settings: { downloadConversions: value } } } });
mutation.catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
return mutation;
};
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('DownloadConversionSetting::refetch'))}
/>
);
}
return <DownloadConversionSetting conversions={data!.settings.downloadConversions} updateSetting={updateSetting} />;
};

View File

@@ -129,7 +129,7 @@ export const DownloadSettings = () => {
onChange={(e) => updateSetting('downloadAsCbz', e.target.checked)}
/>
</ListItem>
<ListItemLink to={AppRoutes.settings.childRoutes.download.childRoutes.conversions.path}>
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.path}>
<ListItemText primary={t('download.settings.conversion.title')} />
</ListItemLink>
<List

View File

@@ -256,3 +256,19 @@ export const KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES: SelectSettingValue<Kor
method,
KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS[method],
]);
export const IMAGE_PROCESSING_COMPRESSION = {
min: 0,
max: 1,
step: 0.01,
};
export const IMAGE_PROCESSING_CALL_TIMEOUT = {
min: d(10).seconds.inWholeSeconds,
max: d(10).minutes.inWholeSeconds,
step: d(10).seconds.inWholeSeconds,
};
export const IMAGE_PROCESSING_CONNECT_TIMEOUT = {
min: d(10).seconds.inWholeSeconds,
max: d(10).minutes.inWholeSeconds,
step: d(10).seconds.inWholeSeconds,
};

View File

@@ -24,11 +24,6 @@ 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,
IMAGE_CONVERSION_CALL_TIMEOUT,
IMAGE_CONVERSION_CONNECT_TIMEOUT,
} from '@/features/downloads/Downloads.constants.ts';
import {
Maybe,
SettingsDownloadConversion,
@@ -38,6 +33,20 @@ import {
import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx';
import { Select } from '@/base/components/inputs/Select.tsx';
import { TranslationKey } from '@/base/Base.types.ts';
import {
IMAGE_PROCESSING_CALL_TIMEOUT,
IMAGE_PROCESSING_COMPRESSION,
IMAGE_PROCESSING_CONNECT_TIMEOUT,
} from '@/features/settings/Settings.constants.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { ServerSettings } from '@/features/settings/Settings.types.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx';
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import { assertIsDefined } from '@/base/Asserts.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
export type TSettingsDownloadConversionHeader = SettingsDownloadConversionHeader & {
/**
@@ -108,19 +117,19 @@ const isValidNumberSetting = (value: number | null | undefined, min: number, 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,
IMAGE_PROCESSING_CALL_TIMEOUT.min,
IMAGE_PROCESSING_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,
IMAGE_PROCESSING_CONNECT_TIMEOUT.min,
IMAGE_PROCESSING_CONNECT_TIMEOUT.max,
);
const isValidCompressionLevel = (compression: number | null | undefined): boolean =>
isValidNumberSetting(compression, DOWNLOAD_CONVERSION_COMPRESSION.min, DOWNLOAD_CONVERSION_COMPRESSION.max);
isValidNumberSetting(compression, IMAGE_PROCESSING_COMPRESSION.min, IMAGE_PROCESSING_COMPRESSION.max);
const isUnsetConversion = (mimeType: string, target: string): boolean => mimeType === '' && target === '';
@@ -521,7 +530,7 @@ const Conversion = ({
helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: DOWNLOAD_CONVERSION_COMPRESSION,
inputProps: IMAGE_PROCESSING_COMPRESSION,
},
}}
onChange={(e) => {
@@ -542,7 +551,7 @@ const Conversion = ({
helperText={!isCallTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: IMAGE_CONVERSION_CALL_TIMEOUT,
inputProps: IMAGE_PROCESSING_CALL_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
@@ -568,7 +577,7 @@ const Conversion = ({
helperText={!isConnectTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{
input: {
inputProps: IMAGE_CONVERSION_CONNECT_TIMEOUT,
inputProps: IMAGE_PROCESSING_CONNECT_TIMEOUT,
endAdornment: (
<InputAdornment position="end">
{t('global.date.label.second_other')}
@@ -622,15 +631,34 @@ const Conversion = ({
);
};
export const DownloadConversionSetting = ({
conversions,
updateSetting,
}: {
conversions: SettingsDownloadConversion[];
updateSetting: (conversions: SettingsDownloadConversion[]) => Promise<void>;
}) => {
export const ImageProcessingSetting = () => {
const { t } = useTranslation();
useAppTitle(t('download.settings.conversion.title'));
const { data, loading, error, refetch } = requestManager.useGetServerSettings({
notifyOnNetworkStatusChange: true,
});
const [mutateSettings] = requestManager.useUpdateServerSettings();
if (loading) {
return <LoadingPlaceholder />;
}
if (error) {
return (
<EmptyViewAbsoluteCentered
message={t('global.error.label.failed_to_load_data')}
messageExtra={getErrorMessage(error)}
retry={() => refetch().catch(defaultPromiseErrorHandler('DownloadConversionSetting::refetch'))}
/>
);
}
assertIsDefined(data?.settings?.downloadConversions);
const conversions = data?.settings?.downloadConversions;
const [tmpConversions, setTmpConversions] = useState(
normalizeConversions(maybeAddDefault(addStableIdToConversions(conversions))),
);
@@ -641,6 +669,13 @@ export const DownloadConversionSetting = ({
tmpConversions,
);
const updateSetting = (value: ServerSettings['downloadConversions']): Promise<any> => {
const mutation = mutateSettings({ variables: { input: { settings: { downloadConversions: value } } } });
mutation.catch((e) => makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)));
return mutation;
};
const onSubmit = async () => {
try {
await updateSetting(toValidServerConversions(tmpConversions));

View File

@@ -0,0 +1,36 @@
/*
* 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 { useTranslation } from 'react-i18next';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import ListSubheader from '@mui/material/ListSubheader';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
export const ImagesSettings = () => {
const { t } = useTranslation();
useAppTitle(t('settings.images.title'));
return (
<List
sx={{ pt: 0 }}
subheader={
<ListSubheader component="div" id="image-processing-settings">
{t('settings.images.processing.title')}
</ListSubheader>
}
>
<ListItemLink to={AppRoutes.settings.childRoutes.images.childRoutes.processingDownloads.path}>
<ListItemText primary={t('download.settings.conversion.title')} />
</ListItemLink>
</List>
);
};

View File

@@ -23,6 +23,7 @@ import DevicesIcon from '@mui/icons-material/Devices';
import SyncIcon from '@mui/icons-material/Sync';
import PaletteIcon from '@mui/icons-material/Palette';
import HistoryIcon from '@mui/icons-material/History';
import ImageIcon from '@mui/icons-material/Image';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
@@ -73,6 +74,12 @@ export function Settings() {
</ListItemIcon>
<ListItemText primary={t('download.title.download')} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.images.path}>
<ListItemIcon>
<ImageIcon />
</ListItemIcon>
<ListItemText primary={t('settings.images.title')} />
</ListItemLink>
<ListItemLink to={AppRoutes.settings.childRoutes.tracking.path}>
<ListItemIcon>
<SyncIcon />