1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00
jellyfin-web/src/apps/dashboard/routes/settings/index.tsx
2025-03-27 08:03:40 +01:00

277 lines
12 KiB
TypeScript

import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import IconButton from '@mui/material/IconButton';
import MenuItem from '@mui/material/MenuItem';
import Stack from '@mui/material/Stack';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { useLocalizationOptions } from 'apps/dashboard/features/settings/api/useLocalizationOptions';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
import { useSystemInfo } from 'hooks/useSystemInfo';
import globalize from 'lib/globalize';
import React, { useCallback, useEffect, useState } from 'react';
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
import SearchIcon from '@mui/icons-material/Search';
import InputAdornment from '@mui/material/InputAdornment';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import Button from '@mui/material/Button';
import Link from '@mui/material/Link';
import DirectoryBrowser from 'components/directorybrowser/directorybrowser';
import ServerConnections from 'components/ServerConnections';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
import { queryClient } from 'utils/query/queryClient';
import { ActionData } from 'types/actionData';
export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi();
if (!api) throw new Error('No Api instance available');
const { data: config } = await getConfigurationApi(api).getConfiguration();
const formData = await request.formData();
config.ServerName = formData.get('ServerName')?.toString();
config.UICulture = formData.get('UICulture')?.toString();
config.CachePath = formData.get('CachePath')?.toString();
config.MetadataPath = formData.get('MetadataPath')?.toString();
config.QuickConnectAvailable = formData.get('QuickConnectAvailable')?.toString() === 'on';
config.LibraryScanFanoutConcurrency = parseInt(formData.get('LibraryScanFanoutConcurrency')?.toString() || '0', 10);
config.ParallelImageEncodingLimit = parseInt(formData.get('ParallelImageEncodingLimit')?.toString() || '0', 10);
await getConfigurationApi(api)
.updateConfiguration({ serverConfiguration: config });
void queryClient.invalidateQueries({
queryKey: [ QUERY_KEY ]
});
return {
isSaved: true
};
};
export const Component = () => {
const {
data: config,
isPending: isConfigPending,
isError: isConfigError
} = useConfiguration();
const {
data: languageOptions,
isPending: isLocalizationOptionsPending,
isError: isLocalizationOptionsError
} = useLocalizationOptions();
const {
data: systemInfo,
isPending: isSystemInfoPending,
isError: isSystemInfoError
} = useSystemInfo();
const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const [ cachePath, setCachePath ] = useState<string | null | undefined>('');
const [ metadataPath, setMetadataPath ] = useState<string | null | undefined>('');
const onCachePathChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setCachePath(event.target.value);
}, []);
const onMetadataPathChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setMetadataPath(event.target.value);
}, []);
const showCachePathPicker = useCallback(() => {
const picker = new DirectoryBrowser();
picker.show({
callback: function (path: string) {
if (path) {
setCachePath(path);
}
picker.close();
},
validateWriteable: true,
header: globalize.translate('HeaderSelectServerCachePath'),
instruction: globalize.translate('HeaderSelectServerCachePathHelp')
});
}, []);
const showMetadataPathPicker = useCallback(() => {
const picker = new DirectoryBrowser();
picker.show({
path: metadataPath,
callback: function (path: string) {
if (path) {
setMetadataPath(path);
}
picker.close();
},
validateWriteable: true,
header: globalize.translate('HeaderSelectMetadataPath'),
instruction: globalize.translate('HeaderSelectMetadataPathHelp')
});
}, [metadataPath]);
useEffect(() => {
if (!isSystemInfoPending && !isSystemInfoError) {
setCachePath(systemInfo.CachePath);
setMetadataPath(systemInfo.InternalMetadataPath);
}
}, [systemInfo, isSystemInfoPending, isSystemInfoError]);
if (isConfigPending || isLocalizationOptionsPending || isSystemInfoPending) {
return <Loading />;
}
return (
<Page
id='dashboardGeneralPage'
title={globalize.translate('General')}
className='type-interior mainAnimatedPage'
>
<Box className='content-primary'>
{isConfigError || isLocalizationOptionsError || isSystemInfoError ? (
<Alert severity='error'>{globalize.translate('SettingsPageLoadError')}</Alert>
) : (
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>{globalize.translate('Settings')}</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<TextField
name='ServerName'
label={globalize.translate('LabelServerName')}
helperText={globalize.translate('LabelServerNameHelp')}
defaultValue={systemInfo.ServerName}
/>
<TextField
select
name='UICulture'
label={globalize.translate('LabelPreferredDisplayLanguage')}
helperText={(
<>
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
<Link href='https://jellyfin.org/docs/general/contributing/#translating'>
{globalize.translate('LearnHowYouCanContribute')}
</Link>
</>
)}
defaultValue={config.UICulture}
slotProps={{
formHelperText: { component: Stack }
}}
>
{languageOptions.map((language) =>
<MenuItem key={language.Name} value={language.Value || ''}>{language.Name}</MenuItem>
)}
</TextField>
<Typography variant='h2'>{globalize.translate('HeaderPaths')}</Typography>
<TextField
name='CachePath'
label={globalize.translate('LabelCachePath')}
helperText={globalize.translate('LabelCachePathHelp')}
value={cachePath}
onChange={onCachePathChange}
slotProps={{
input: {
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showCachePathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
}
}}
/>
<TextField
name={'MetadataPath'}
label={globalize.translate('LabelMetadataPath')}
helperText={globalize.translate('LabelMetadataPathHelp')}
value={metadataPath}
onChange={onMetadataPathChange}
slotProps={{
input: {
endAdornment: (
<InputAdornment position='end'>
<IconButton edge='end' onClick={showMetadataPathPicker}>
<SearchIcon />
</IconButton>
</InputAdornment>
)
}
}}
/>
<Typography variant='h2'>{globalize.translate('QuickConnect')}</Typography>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='QuickConnectAvailable'
defaultChecked={config.QuickConnectAvailable}
/>
}
label={globalize.translate('EnableQuickConnect')}
/>
</FormControl>
<Typography variant='h2'>{globalize.translate('HeaderPerformance')}</Typography>
<TextField
name='LibraryScanFanoutConcurrency'
type='number'
label={globalize.translate('LibraryScanFanoutConcurrency')}
helperText={globalize.translate('LibraryScanFanoutConcurrencyHelp')}
defaultValue={config.LibraryScanFanoutConcurrency || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/>
<TextField
name='ParallelImageEncodingLimit'
type='number'
label={globalize.translate('LabelParallelImageEncodingLimit')}
helperText={globalize.translate('LabelParallelImageEncodingLimitHelp')}
defaultValue={config.ParallelImageEncodingLimit || ''}
slotProps={{
htmlInput: {
min: 0,
step: 1
}
}}
/>
<Button type='submit' size='large'>
{globalize.translate('Save')}
</Button>
</Stack>
</Form>
)}
</Box>
</Page>
);
};
Component.displayName = 'SettingsPage';