/* * 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, Trans } from 'react-i18next'; import { useContext, useLayoutEffect, 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 { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { requestManager } from '@/lib/requests/requests/RequestManager.ts'; import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; import { TextSetting } from '@/modules/core/components/settings/text/TextSetting.tsx'; import { ServerSettings as GqlServerSettings } from '@/typings.ts'; import { NumberSetting } from '@/modules/core/components/settings/NumberSetting.tsx'; import { SelectSetting } from '@/modules/core/components/settings/SelectSetting.tsx'; import { LoadingPlaceholder } from '@/modules/core/components/placeholder/LoadingPlaceholder.tsx'; import { EmptyViewAbsoluteCentered } from '@/modules/core/components/placeholder/EmptyViewAbsoluteCentered.tsx'; import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts'; import { createUpdateMetadataServerSettings, useMetadataServerSettings, } from '@/lib/metadata/metadataServerSettings.ts'; import { makeToast } from '@/lib/ui/Toast.ts'; import { MetadataUpdateSettings } from '@/modules/app-updates/AppUpdateChecker.types.ts'; type ServerSettingsType = Pick< GqlServerSettings, | 'ip' | 'port' | 'socksProxyEnabled' | 'socksProxyVersion' | 'socksProxyHost' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyPassword' | 'debugLogsEnabled' | 'gqlDebugLogsEnabled' | 'systemTrayEnabled' | 'maxLogFiles' | 'maxLogFileSize' | 'maxLogFolderSize' | 'basicAuthEnabled' | 'basicAuthUsername' | 'basicAuthPassword' | 'flareSolverrEnabled' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrAsResponseFallback' >; 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, gqlDebugLogsEnabled: settings.gqlDebugLogsEnabled, systemTrayEnabled: settings.systemTrayEnabled, maxLogFiles: settings.maxLogFiles, maxLogFileSize: settings.maxLogFileSize, maxLogFolderSize: settings.maxLogFolderSize, basicAuthEnabled: settings.basicAuthEnabled, basicAuthUsername: settings.basicAuthUsername, basicAuthPassword: settings.basicAuthPassword, flareSolverrEnabled: settings.flareSolverrEnabled, flareSolverrTimeout: settings.flareSolverrTimeout, flareSolverrUrl: settings.flareSolverrUrl, flareSolverrSessionName: settings.flareSolverrSessionName, flareSolverrSessionTtl: settings.flareSolverrSessionTtl, flareSolverrAsResponseFallback: settings.flareSolverrAsResponseFallback, }); 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(); const { setTitle, setAction } = useContext(NavBarContext); useLayoutEffect(() => { setTitle(t('settings.server.title.settings')); setAction(null); return () => { setTitle(''); setAction(null); }; }, [t]); const { settings: { serverInformAvailableUpdate }, loading: areMetadataServerSettingsLoading, request: { error: metadataServerSettingsError, refetch: refetchServerMetadataSettings }, } = useMetadataServerSettings(); const updateMetadataServerSettings = createUpdateMetadataServerSettings< keyof Pick >(() => makeToast(t('global.error.label.failed_to_save_changes'), 'error')); const { data, loading: areServerSettingsLoading, error: serverSettingsError, refetch: refetchServerSettings, } = requestManager.useGetServerSettings({ notifyOnNetworkStatusChange: true, }); const [mutateSettings] = requestManager.useUpdateServerSettings(); const [serverAddress, setServerAddress] = useLocalStorage('serverBaseURL', window.location.origin); const handleServerAddressChange = (address: string) => { const serverBaseUrl = address.replaceAll(/(\/)+$/g, ''); setServerAddress(serverBaseUrl); requestManager.reset(); }; const updateSetting = ( setting: Setting, value: ServerSettingsType[Setting], ) => { mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch(() => makeToast(t('global.error.label.failed_to_save_changes'), 'error'), ); }; const localSettings = useMemo( () => ( {t('global.label.client')} } > updateMetadataServerSettings('serverInformAvailableUpdate', e.target.checked)} /> ), [serverAddress, 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); 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')} } > updateSetting('basicAuthEnabled', e.target.checked)} /> updateSetting('basicAuthUsername', authUsername)} /> updateSetting('basicAuthPassword', authPassword)} /> {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.misc.title')} } > updateSetting('debugLogsEnabled', e.target.checked)} /> updateSetting('gqlDebugLogsEnabled', 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)} /> ); };