/* * 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 { useMemo } from 'react'; import Link from '@mui/material/Link'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; import ListItemText from '@mui/material/ListItemText'; import Switch from '@mui/material/Switch'; import ListSubheader from '@mui/material/ListSubheader'; import { d } from 'koration'; import { Trans, useLingui } from '@lingui/react/macro'; import { plural, t as translate } from '@lingui/core/macro'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx'; import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx'; import { SelectSetting } from '@/base/components/settings/SelectSetting.tsx'; import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { createUpdateMetadataServerSettings, useMetadataServerSettings, } from '@/features/settings/services/ServerSettingsMetadata.ts'; import { makeToast } from '@/base/utils/Toast.ts'; import { MetadataUpdateSettings } from '@/features/app-updates/AppUpdateChecker.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; import { AuthMode, CbzMediaType, DatabaseType, SortOrder } from '@/lib/graphql/generated/graphql'; import { AUTH_MODES_SELECT_VALUES, JWT_ACCESS_TOKEN_EXPIRY, JWT_REFRESH_TOKEN_EXPIRY, } from '@/features/settings/Settings.constants.ts'; import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; import { ServerSettings as ServerSettingsType } from '@/features/settings/Settings.types.ts'; import { KoreaderSyncSettings } from '@/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx'; const getLogFilesCleanupDisplayValue = (ttl: number): string => { if (ttl === 0) { return translate`Never`; } return plural(ttl, { one: 'Delete log files that are older than # day', other: 'Delete log files that are older than # days', }); }; export const ServerSettings = () => { const { t } = useLingui(); useAppTitle(t`Server`); const { settings: { serverInformAvailableUpdate, serverInformVersionUpdated }, loading: areMetadataServerSettingsLoading, request: { error: metadataServerSettingsError, refetch: refetchServerMetadataSettings }, } = useMetadataServerSettings(); const updateMetadataServerSettings = createUpdateMetadataServerSettings< keyof Pick >((e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e))); const { data, loading: areServerSettingsLoading, error: serverSettingsError, refetch: refetchServerSettings, } = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true, }); const [mutateSettings] = requestManager.useUpdateServerSettings(); const koSyncStatus = requestManager.useKoSyncStatus(); const updateSetting = async ( setting: Setting, value: ServerSettingsType[Setting], onCompletion?: (success: boolean) => void, ) => { try { await mutateSettings({ variables: { input: { settings: { [setting]: value } } } }); onCompletion?.(true); } catch (e) { makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)); onCompletion?.(false); } }; const localSettings = useMemo( () => ( {t`Client`} } > updateMetadataServerSettings('serverInformVersionUpdated', e.target.checked)} /> updateMetadataServerSettings('serverInformAvailableUpdate', e.target.checked)} /> ), [serverInformVersionUpdated, serverInformAvailableUpdate], ); const loading = areMetadataServerSettingsLoading || areServerSettingsLoading || koSyncStatus.loading; if (loading) { return ( <> {localSettings} ); } const error = metadataServerSettingsError ?? serverSettingsError ?? koSyncStatus.error; if (error) { return ( <> {localSettings} { if (metadataServerSettingsError) { refetchServerMetadataSettings().catch( defaultPromiseErrorHandler('ServerSettings::refetchServerMetadataSettings'), ); } if (serverSettingsError) { refetchServerSettings().catch( defaultPromiseErrorHandler('ServerSettings::refetchServerSettings'), ); } if (koSyncStatus.error) { koSyncStatus .refetch() .catch(defaultPromiseErrorHandler('ServerSettings::koSyncStatus.refetch')); } }} /> ); } const serverSettings = data!.settings; const koreaderSyncStatus = koSyncStatus.data!.koSyncStatus; const authModeDisabled = !serverSettings.authUsername?.trim() || !serverSettings.authPassword?.trim(); const isH2Database = serverSettings.databaseType === DatabaseType.H2; return ( {localSettings} {t`Server bindings`} } > updateSetting('ip', ip)} value={serverSettings.ip} placeholder="0.0.0.0" /> updateSetting('port', port)} value={serverSettings.port} defaultValue={4567} valueUnit={t`Port`} /> {t`SOCKS proxy`} } > updateSetting('socksProxyEnabled', e.target.checked)} /> settingName={t`SOCKS version`} value={serverSettings.socksProxyVersion} values={[ [4, { text: '4' }], [5, { text: '5' }], ]} handleChange={(socksProxyVersion) => updateSetting('socksProxyVersion', socksProxyVersion)} /> updateSetting('socksProxyHost', proxyHost)} /> updateSetting('socksProxyPort', proxyPort)} /> updateSetting('socksProxyUsername', proxyUsername)} /> updateSetting('socksProxyPassword', proxyPassword)} isPassword /> {t`Authentication`} } > settingName={t`Authentication Mode`} value={serverSettings.authMode} values={AUTH_MODES_SELECT_VALUES} handleChange={(mode) => { updateSetting('authMode', mode, (success) => { if (!success) { return; } if (mode !== AuthMode.UiLogin) { AuthManager.removeTokens(); } AuthManager.setAuthRequired(mode === AuthMode.UiLogin); }); }} disabled={authModeDisabled} /> serverSettings.authMode === AuthMode.None || !!value.trim()} handleChange={(authUsername) => updateSetting('authUsername', authUsername)} /> serverSettings.authMode === AuthMode.None || !!value.trim()} handleChange={(authPassword) => updateSetting('authPassword', authPassword)} /> {serverSettings.authMode === AuthMode.UiLogin && ( <> updateSetting('jwtAudience', audience)} /> updateSetting('jwtTokenExpiry', d(expiry).minutes.toISOString())} showSlider /> updateSetting('jwtRefreshExpiry', d(expiry).days.toISOString())} showSlider /> )} {t`Cloudflare bypass`} } > See{' '} FlareSolverr {' '} for information on how to set it up } /> updateSetting('flareSolverrEnabled', e.target.checked)} /> updateSetting('flareSolverrUrl', url)} /> updateSetting('flareSolverrTimeout', timeout)} /> updateSetting('flareSolverrSessionName', sessionName)} /> updateSetting('flareSolverrSessionTtl', sessionTTL)} /> updateSetting('flareSolverrAsResponseFallback', e.target.checked)} /> {t`OPDS`} } > updateSetting('opdsUseBinaryFileSizes', e.target.checked)} /> updateSetting('opdsItemsPerPage', value)} /> updateSetting('opdsEnablePageReadProgress', e.target.checked)} /> updateSetting('opdsMarkAsReadOnDownload', e.target.checked)} /> updateSetting('opdsShowOnlyUnreadChapters', e.target.checked)} /> updateSetting('opdsShowOnlyDownloadedChapters', e.target.checked)} /> settingName={t`Chapter sort order`} dialogDescription={t`Choose the order in which chapters are displayed.`} value={serverSettings.opdsChapterSortOrder} values={[ [ SortOrder.Asc, { text: t`Ascending`, }, ], [ SortOrder.Desc, { text: t`Descending`, }, ], ]} handleChange={(value) => updateSetting('opdsChapterSortOrder', value)} /> settingName={t`CBZ MIME-Type`} dialogDescription={t`Controls the MimeType that Suwayomi sends in OPDS entries for CBZ archives. Also affects global CBZ download.\nModern follows recent IANA standard (2017), while LEGACY (deprecated mimetype for .cbz) and COMPATIBLE (deprecated mimetype for all comic archives) might be more compatible with older clients.`} value={serverSettings.opdsCbzMimetype} values={[ [ CbzMediaType.Legacy, { text: t`Legacy`, }, ], [ CbzMediaType.Modern, { text: t`Modern`, }, ], [ CbzMediaType.Compatible, { text: t`Compatible`, }, ], ]} handleChange={(value) => updateSetting('opdsCbzMimetype', value)} /> {t`Database`} } > settingName={t`Type`} value={serverSettings.databaseType} values={[ [ DatabaseType.H2, { text: t`H2`, }, ], [ DatabaseType.Postgresql, { text: t`PostgreSQL`, }, ], ]} handleChange={(value) => updateSetting('databaseType', value)} /> updateSetting('databaseUrl', url)} value={serverSettings.databaseUrl} placeholder="postgresql://localhost:5432/suwayomi" disabled={isH2Database} /> updateSetting('databaseUsername', username)} disabled={isH2Database} /> updateSetting('databasePassword', password)} disabled={isH2Database} isPassword /> updateSetting('useHikariConnectionPool', e.target.checked)} /> {t`Misc`} } > updateSetting('debugLogsEnabled', e.target.checked)} /> updateSetting('systemTrayEnabled', e.target.checked)} /> updateSetting('maxLogFiles', maxFiles)} /> !!value.match(/^[0-9]+(|kb|KB|mb|MB|gb|GB)$/g)} handleChange={(maxLogFileSize) => updateSetting('maxLogFileSize', maxLogFileSize)} /> !!value.match(/^[0-9]+(|kb|KB|mb|MB|gb|GB)$/g)} handleChange={(maxLogFolderSize) => updateSetting('maxLogFolderSize', maxLogFolderSize)} /> ); };