import React, { useMemo } from 'react'; import useSWR from 'swr'; import LoadingSpinner from '../Common/LoadingSpinner'; import type { MainSettings, Language } from '../../../server/lib/settings'; import CopyButton from './CopyButton'; import { Form, Formik, Field } from 'formik'; import axios from 'axios'; import Button from '../Common/Button'; import { defineMessages, useIntl } from 'react-intl'; import { useUser, Permission } from '../../hooks/useUser'; import { useToasts } from 'react-toast-notifications'; import Badge from '../Common/Badge'; import globalMessages from '../../i18n/globalMessages'; import * as Yup from 'yup'; import RegionSelector from '../RegionSelector'; import PageTitle from '../Common/PageTitle'; const messages = defineMessages({ general: 'General', generalsettings: 'General Settings', generalsettingsDescription: 'Configure global and default settings for Overseerr.', save: 'Save Changes', saving: 'Saving…', apikey: 'API Key', applicationTitle: 'Application Title', applicationurl: 'Application URL', region: 'Discover Region', regionTip: 'Filter content by regional availability', originallanguage: 'Discover Language', originallanguageTip: 'Filter content by original language', toastApiKeySuccess: 'New API key generated successfully!', toastApiKeyFailure: 'Something went wrong while generating a new API key.', toastSettingsSuccess: 'Settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', hideAvailable: 'Hide Available Media', csrfProtection: 'Enable CSRF Protection', csrfProtectionTip: 'Sets external API access to read-only (requires HTTPS, and Overseerr must be reloaded for changes to take effect)', csrfProtectionHoverTip: 'Do NOT enable this setting unless you understand what you are doing!', cacheImages: 'Cache & Optimize Images Locally', cacheImagesTip: 'Enabling this option will cause all images to be optimized and stored locally. This uses a significant amount of disk space.', trustProxy: 'Enable Proxy Support', trustProxyTip: 'Allows Overseerr to correctly register client IP addresses behind a proxy (Overseerr must be reloaded for changes to take effect)', validationApplicationTitle: 'You must provide an application title', validationApplicationUrl: 'You must provide a valid URL', validationApplicationUrlTrailingSlash: 'URL must not end in a trailing slash', originalLanguageDefault: 'All Languages', partialRequestsEnabled: 'Allow Partial Series Requests', }); const SettingsMain: React.FC = () => { const { addToast } = useToasts(); const { hasPermission: userHasPermission } = useUser(); const intl = useIntl(); const { data, error, revalidate } = useSWR( '/api/v1/settings/main' ); const { data: languages, error: languagesError } = useSWR( '/api/v1/languages' ); const MainSettingsSchema = Yup.object().shape({ applicationTitle: Yup.string().required( intl.formatMessage(messages.validationApplicationTitle) ), applicationUrl: Yup.string() .url(intl.formatMessage(messages.validationApplicationUrl)) .test( 'no-trailing-slash', intl.formatMessage(messages.validationApplicationUrlTrailingSlash), (value) => { if (value?.substr(value.length - 1) === '/') { return false; } return true; } ), }); const regenerate = async () => { try { await axios.post('/api/v1/settings/main/regenerate'); revalidate(); addToast(intl.formatMessage(messages.toastApiKeySuccess), { autoDismiss: true, appearance: 'success', }); } catch (e) { addToast(intl.formatMessage(messages.toastApiKeyFailure), { autoDismiss: true, appearance: 'error', }); } }; const sortedLanguages = useMemo( () => languages?.sort((lang1, lang2) => { const lang1Name = intl.formatDisplayName(lang1.iso_639_1, { type: 'language', fallback: 'none', }) ?? lang1.english_name; const lang2Name = intl.formatDisplayName(lang2.iso_639_1, { type: 'language', fallback: 'none', }) ?? lang2.english_name; return lang1Name === lang2Name ? 0 : lang1Name > lang2Name ? 1 : -1; }), [intl, languages] ); if (!data && !error && !languages && !languagesError) { return ; } return ( <>

{intl.formatMessage(messages.generalsettings)}

{intl.formatMessage(messages.generalsettingsDescription)}

{ try { await axios.post('/api/v1/settings/main', { applicationTitle: values.applicationTitle, applicationUrl: values.applicationUrl, csrfProtection: values.csrfProtection, hideAvailable: values.hideAvailable, region: values.region, originalLanguage: values.originalLanguage, partialRequestsEnabled: values.partialRequestsEnabled, trustProxy: values.trustProxy, cacheImages: values.cacheImages, }); addToast(intl.formatMessage(messages.toastSettingsSuccess), { autoDismiss: true, appearance: 'success', }); } catch (e) { addToast(intl.formatMessage(messages.toastSettingsFailure), { autoDismiss: true, appearance: 'error', }); } finally { revalidate(); } }} > {({ errors, touched, isSubmitting, values, setFieldValue }) => { return (
{userHasPermission(Permission.ADMIN) && (
)}
{errors.applicationTitle && touched.applicationTitle && (
{errors.applicationTitle}
)}
{errors.applicationUrl && touched.applicationUrl && (
{errors.applicationUrl}
)}
{ setFieldValue('trustProxy', !values.trustProxy); }} />
{ setFieldValue('csrfProtection', !values.csrfProtection); }} />
{sortedLanguages?.map((language) => ( ))}
{ setFieldValue('hideAvailable', !values.hideAvailable); }} />
{ setFieldValue( 'partialRequestsEnabled', !values.partialRequestsEnabled ); }} />
); }}
); }; export default SettingsMain;