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", "call_timeout": "Call timeout",
"compression_level": "Compression level", "compression_level": "Compression level",
"connect_timeout": "Connect timeout", "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": { "headers": {
"button": "Headers ({{count}})", "button": "Headers ({{count}})",
"name": "Name", "name": "Name",
@@ -237,7 +237,7 @@
"title": "URL" "title": "URL"
} }
}, },
"title": "Download conversion" "title": "Downloaded image processing"
}, },
"delete_chapters": { "delete_chapters": {
"label": { "label": {
@@ -1320,6 +1320,12 @@
"device": "Device" "device": "Device"
} }
}, },
"images": {
"processing": {
"title": "Image processing"
},
"title": "Images"
},
"label": { "label": {
"hide_library_entries": "Hide entries already in library", "hide_library_entries": "Hide entries already in library",
"language_description": "Feel free to translate the project on", "language_description": "Feel free to translate the project on",

View File

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

View File

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

View File

@@ -6,29 +6,9 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. * file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/ */
import { d } from 'koration';
export const DOWNLOAD_AHEAD = { export const DOWNLOAD_AHEAD = {
min: 2, min: 2,
max: 10, max: 10,
default: 2, default: 2,
step: 1, 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)} onChange={(e) => updateSetting('downloadAsCbz', e.target.checked)}
/> />
</ListItem> </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')} /> <ListItemText primary={t('download.settings.conversion.title')} />
</ListItemLink> </ListItemLink>
<List <List

View File

@@ -256,3 +256,19 @@ export const KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES: SelectSettingValue<Kor
method, method,
KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS[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 { useElementSize } from '@mantine/hooks';
import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx'; import { TypographyMaxLines } from '@/base/components/texts/TypographyMaxLines.tsx';
import { CustomTooltip } from '@/base/components/CustomTooltip.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 { import {
Maybe, Maybe,
SettingsDownloadConversion, SettingsDownloadConversion,
@@ -38,6 +33,20 @@ import {
import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx'; import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx';
import { Select } from '@/base/components/inputs/Select.tsx'; import { Select } from '@/base/components/inputs/Select.tsx';
import { TranslationKey } from '@/base/Base.types.ts'; 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 & { export type TSettingsDownloadConversionHeader = SettingsDownloadConversionHeader & {
/** /**
@@ -108,19 +117,19 @@ const isValidNumberSetting = (value: number | null | undefined, min: number, max
const isValidCallTimeoutSetting = (timeout: string | null | undefined): boolean => const isValidCallTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting( isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null, timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_CONVERSION_CALL_TIMEOUT.min, IMAGE_PROCESSING_CALL_TIMEOUT.min,
IMAGE_CONVERSION_CALL_TIMEOUT.max, IMAGE_PROCESSING_CALL_TIMEOUT.max,
); );
const isValidConnectTimeoutSetting = (timeout: string | null | undefined): boolean => const isValidConnectTimeoutSetting = (timeout: string | null | undefined): boolean =>
isValidNumberSetting( isValidNumberSetting(
timeout ? d(timeout).seconds.inWholeSeconds : null, timeout ? d(timeout).seconds.inWholeSeconds : null,
IMAGE_CONVERSION_CONNECT_TIMEOUT.min, IMAGE_PROCESSING_CONNECT_TIMEOUT.min,
IMAGE_CONVERSION_CONNECT_TIMEOUT.max, IMAGE_PROCESSING_CONNECT_TIMEOUT.max,
); );
const isValidCompressionLevel = (compression: number | null | undefined): boolean => 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 === ''; const isUnsetConversion = (mimeType: string, target: string): boolean => mimeType === '' && target === '';
@@ -521,7 +530,7 @@ const Conversion = ({
helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''} helperText={!isCompressionLevelValid ? t('global.error.label.invalid_input') : ''}
slotProps={{ slotProps={{
input: { input: {
inputProps: DOWNLOAD_CONVERSION_COMPRESSION, inputProps: IMAGE_PROCESSING_COMPRESSION,
}, },
}} }}
onChange={(e) => { onChange={(e) => {
@@ -542,7 +551,7 @@ const Conversion = ({
helperText={!isCallTimeoutValid ? t('global.error.label.invalid_input') : ''} helperText={!isCallTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{ slotProps={{
input: { input: {
inputProps: IMAGE_CONVERSION_CALL_TIMEOUT, inputProps: IMAGE_PROCESSING_CALL_TIMEOUT,
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
{t('global.date.label.second_other')} {t('global.date.label.second_other')}
@@ -568,7 +577,7 @@ const Conversion = ({
helperText={!isConnectTimeoutValid ? t('global.error.label.invalid_input') : ''} helperText={!isConnectTimeoutValid ? t('global.error.label.invalid_input') : ''}
slotProps={{ slotProps={{
input: { input: {
inputProps: IMAGE_CONVERSION_CONNECT_TIMEOUT, inputProps: IMAGE_PROCESSING_CONNECT_TIMEOUT,
endAdornment: ( endAdornment: (
<InputAdornment position="end"> <InputAdornment position="end">
{t('global.date.label.second_other')} {t('global.date.label.second_other')}
@@ -622,15 +631,34 @@ const Conversion = ({
); );
}; };
export const DownloadConversionSetting = ({ export const ImageProcessingSetting = () => {
conversions,
updateSetting,
}: {
conversions: SettingsDownloadConversion[];
updateSetting: (conversions: SettingsDownloadConversion[]) => Promise<void>;
}) => {
const { t } = useTranslation(); 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( const [tmpConversions, setTmpConversions] = useState(
normalizeConversions(maybeAddDefault(addStableIdToConversions(conversions))), normalizeConversions(maybeAddDefault(addStableIdToConversions(conversions))),
); );
@@ -641,6 +669,13 @@ export const DownloadConversionSetting = ({
tmpConversions, 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 () => { const onSubmit = async () => {
try { try {
await updateSetting(toValidServerConversions(tmpConversions)); 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 SyncIcon from '@mui/icons-material/Sync';
import PaletteIcon from '@mui/icons-material/Palette'; import PaletteIcon from '@mui/icons-material/Palette';
import HistoryIcon from '@mui/icons-material/History'; import HistoryIcon from '@mui/icons-material/History';
import ImageIcon from '@mui/icons-material/Image';
import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx'; import { ListItemLink } from '@/base/components/lists/ListItemLink.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts'; import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts'; import { makeToast } from '@/base/utils/Toast.ts';
@@ -73,6 +74,12 @@ export function Settings() {
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t('download.title.download')} /> <ListItemText primary={t('download.title.download')} />
</ListItemLink> </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}> <ListItemLink to={AppRoutes.settings.childRoutes.tracking.path}>
<ListItemIcon> <ListItemIcon>
<SyncIcon /> <SyncIcon />