/* * 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 dayjs from 'dayjs'; 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 { ServerSettings as GqlServerSettings, ServerSettingsType } from '@/features/settings/Settings.types.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts'; import { AuthMode, KoreaderSyncChecksumMethod, KoreaderSyncStrategy, SortOrder } from '@/lib/graphql/generated/graphql'; import { AUTH_MODES_SELECT_VALUES, JWT_ACCESS_TOKEN_EXPIRY, JWT_REFRESH_TOKEN_EXPIRY, KOREADER_SYNC_PERCENTAGE_TOLERANCE, } from '@/features/settings/Settings.constants.ts'; import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; const extractServerSettings = (settings: GqlServerSettings): ServerSettingsType => ({ ip: settings.ip, port: settings.port, socksProxyEnabled: settings.socksProxyEnabled, socksProxyVersion: settings.socksProxyVersion, socksProxyHost: settings.socksProxyHost, socksProxyPort: settings.socksProxyPort, socksProxyUsername: settings.socksProxyUsername, socksProxyPassword: settings.socksProxyPassword, debugLogsEnabled: settings.debugLogsEnabled, systemTrayEnabled: settings.systemTrayEnabled, maxLogFiles: settings.maxLogFiles, maxLogFileSize: settings.maxLogFileSize, maxLogFolderSize: settings.maxLogFolderSize, authMode: settings.authMode, authUsername: settings.authUsername, authPassword: settings.authPassword, jwtAudience: settings.jwtAudience, jwtTokenExpiry: settings.jwtTokenExpiry, jwtRefreshExpiry: settings.jwtRefreshExpiry, flareSolverrEnabled: settings.flareSolverrEnabled, flareSolverrTimeout: settings.flareSolverrTimeout, flareSolverrUrl: settings.flareSolverrUrl, flareSolverrSessionName: settings.flareSolverrSessionName, flareSolverrSessionTtl: settings.flareSolverrSessionTtl, flareSolverrAsResponseFallback: settings.flareSolverrAsResponseFallback, opdsUseBinaryFileSizes: settings.opdsUseBinaryFileSizes, opdsItemsPerPage: settings.opdsItemsPerPage, opdsEnablePageReadProgress: settings.opdsEnablePageReadProgress, opdsMarkAsReadOnDownload: settings.opdsMarkAsReadOnDownload, opdsShowOnlyUnreadChapters: settings.opdsShowOnlyUnreadChapters, opdsShowOnlyDownloadedChapters: settings.opdsShowOnlyDownloadedChapters, opdsChapterSortOrder: settings.opdsChapterSortOrder, koreaderSyncServerUrl: settings.koreaderSyncServerUrl, koreaderSyncUsername: settings.koreaderSyncUsername, koreaderSyncUserkey: settings.koreaderSyncUserkey, koreaderSyncDeviceId: settings.koreaderSyncDeviceId, koreaderSyncChecksumMethod: settings.koreaderSyncChecksumMethod, koreaderSyncStrategy: settings.koreaderSyncStrategy, koreaderSyncPercentageTolerance: settings.koreaderSyncPercentageTolerance, }); 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 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; if (loading) { return ( <> {localSettings} ); } const error = metadataServerSettingsError ?? serverSettingsError; if (error) { return ( <> {localSettings} { if (metadataServerSettingsError) { refetchServerMetadataSettings().catch( defaultPromiseErrorHandler('ServerSettings::refetchServerMetadataSettings'), ); } if (serverSettingsError) { refetchServerSettings().catch( defaultPromiseErrorHandler('ServerSettings::refetchServerSettings'), ); } }} /> ); } const serverSettings = extractServerSettings(data!.settings); const authModeDisabled = !serverSettings.authUsername?.trim() || !serverSettings.authPassword?.trim(); 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', dayjs.duration(expiry, 'minute').toISOString()) } showSlider /> updateSetting('jwtRefreshExpiry', dayjs.duration(expiry, 'day').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.koreader.sync.title')} } > updateSetting('koreaderSyncServerUrl', url)} value={serverSettings.koreaderSyncServerUrl} placeholder="http://localhost:17200" /> updateSetting('koreaderSyncUsername', username)} /> updateSetting('koreaderSyncUserkey', userkey)} isPassword /> updateSetting('koreaderSyncDeviceId', deviceId)} /> settingName={t('settings.server.koreader.sync.check_sum_method.title')} value={serverSettings.koreaderSyncChecksumMethod} values={[ [ KoreaderSyncChecksumMethod.Binary, { text: t('settings.server.koreader.sync.check_sum_method.binary') }, ], [ KoreaderSyncChecksumMethod.Filename, { text: t('settings.server.koreader.sync.check_sum_method.filename') }, ], ]} handleChange={(value) => updateSetting('koreaderSyncChecksumMethod', value)} /> settingName={t('settings.server.koreader.sync.strategy.title')} value={serverSettings.koreaderSyncStrategy} values={[ [KoreaderSyncStrategy.Disabled, { text: t('settings.server.koreader.sync.strategy.disabled') }], [KoreaderSyncStrategy.Prompt, { text: t('settings.server.koreader.sync.strategy.prompt') }], [KoreaderSyncStrategy.Silent, { text: t('settings.server.koreader.sync.strategy.silent') }], [KoreaderSyncStrategy.Send, { text: t('settings.server.koreader.sync.strategy.send') }], [KoreaderSyncStrategy.Receive, { text: t('settings.server.koreader.sync.strategy.receive') }], ]} handleChange={(value) => updateSetting('koreaderSyncStrategy', value)} /> updateSetting('koreaderSyncPercentageTolerance', tolerance)} /> {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)} /> ); };