mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate General to React
This commit is contained in:
parent
3c62c1dc51
commit
f94e16d56a
7 changed files with 294 additions and 198 deletions
269
src/apps/dashboard/routes/settings/index.tsx
Normal file
269
src/apps/dashboard/routes/settings/index.tsx
Normal file
|
@ -0,0 +1,269 @@
|
|||
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 Paper from '@mui/material/Paper';
|
||||
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={(
|
||||
<Stack>
|
||||
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
|
||||
<Link href='https://jellyfin.org/docs/general/contributing/#translating'>
|
||||
{globalize.translate('LearnHowYouCanContribute')}
|
||||
</Link>
|
||||
</Stack>
|
||||
)}
|
||||
defaultValue={config.UICulture}
|
||||
>
|
||||
{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}
|
||||
InputProps={{
|
||||
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}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position='end'>
|
||||
<IconButton edge='end' onClick={showMetadataPathPicker}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography variant='h2'>{globalize.translate('QuickConnect')}</Typography>
|
||||
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<FormControl>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
name='QuickConnectAvailable'
|
||||
defaultChecked={config.QuickConnectAvailable}
|
||||
/>
|
||||
}
|
||||
label={globalize.translate('EnableQuickConnect')}
|
||||
/>
|
||||
</FormControl>
|
||||
</Paper>
|
||||
|
||||
<Typography variant='h2'>{globalize.translate('HeaderPerformance')}</Typography>
|
||||
|
||||
<TextField
|
||||
name='LibraryScanFanoutConcurrency'
|
||||
type='number'
|
||||
inputProps={{
|
||||
min: 0,
|
||||
step: 1
|
||||
}}
|
||||
label={globalize.translate('LibraryScanFanoutConcurrency')}
|
||||
helperText={globalize.translate('LibraryScanFanoutConcurrencyHelp')}
|
||||
defaultValue={config.LibraryScanFanoutConcurrency || ''}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
name='ParallelImageEncodingLimit'
|
||||
type='number'
|
||||
inputProps={{
|
||||
min: 0,
|
||||
step: 1
|
||||
}}
|
||||
label={globalize.translate('LabelParallelImageEncodingLimit')}
|
||||
helperText={globalize.translate('LabelParallelImageEncodingLimitHelp')}
|
||||
defaultValue={config.ParallelImageEncodingLimit || ''}
|
||||
/>
|
||||
|
||||
<Button type='submit' size='large'>
|
||||
{globalize.translate('Save')}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Form>
|
||||
)}
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
Component.displayName = 'SettingsPage';
|
Loading…
Add table
Add a link
Reference in a new issue