/* * 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 { Trans, useTranslation } from 'react-i18next'; 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 { t as translate } from 'i18next'; import { d } from 'koration'; 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, 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('global.label.never'); } return translate('settings.server.misc.log_files.file_cleanup.value', { days: ttl, count: ttl }); }; export const ServerSettings = () => { const { t } = useTranslation(); useAppTitle(t('settings.server.title.server')); const { settings: { serverInformAvailableUpdate }, loading: areMetadataServerSettingsLoading, request: { error: metadataServerSettingsError, refetch: refetchServerMetadataSettings }, } = useMetadataServerSettings(); const updateMetadataServerSettings = createUpdateMetadataServerSettings< keyof Pick >((e) => makeToast(t('global.error.label.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('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)); onCompletion?.(false); } }; const localSettings = useMemo( () => ( {t('global.label.client')} } > updateMetadataServerSettings('serverInformAvailableUpdate', e.target.checked)} /> ), [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('settings.server.address.server.title')} } > updateSetting('ip', ip)} value={serverSettings.ip} placeholder="0.0.0.0" /> updateSetting('port', port)} value={serverSettings.port} defaultValue={4567} valueUnit={t('settings.server.address.server.label.port')} /> {t('settings.server.socks_proxy.title')} } > updateSetting('socksProxyEnabled', e.target.checked)} /> settingName={t('settings.server.socks_proxy.label.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('settings.server.auth.title')} } > settingName={t('settings.server.auth.label.title')} 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('settings.server.cloudflare.title')} } > 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('settings.server.opds.title')} } > 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('settings.server.opds.chapter_sort_order.label.title')} dialogDescription={t('settings.server.opds.chapter_sort_order.label.description')} value={serverSettings.opdsChapterSortOrder} values={[ [SortOrder.Asc, { text: t('global.sort.label.asc') }], [SortOrder.Desc, { text: t('global.sort.label.desc') }], ]} handleChange={(value) => updateSetting('opdsChapterSortOrder', value)} /> {t('settings.server.database.title')} } > settingName={t('settings.server.database.type.title')} value={serverSettings.databaseType} values={[ [DatabaseType.H2, { text: t('settings.server.database.type.h2') }], [DatabaseType.Postgresql, { text: t('settings.server.database.type.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 /> {t('settings.server.misc.title')} } > 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)} /> ); };