mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6596 from viown/react-general
Migrate General to React
This commit is contained in:
commit
ccc9f52aec
7 changed files with 292 additions and 198 deletions
|
@ -1,84 +0,0 @@
|
|||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage" data-title="${General}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardGeneralForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${Settings}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtServerName" label="${LabelServerName}" />
|
||||
<div class="fieldDescription">${LabelServerNameHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectLocalizationLanguage" label="${LabelPreferredDisplayLanguage}"></select>
|
||||
<div class="fieldDescription">
|
||||
<div>${LabelDisplayLanguageHelp}</div>
|
||||
<div style="margin-top: .25em;">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/contributing/#translating" target="_blank">${LearnHowYouCanContribute}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<h2>${HeaderPaths}</h2>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtCachePath" label="${LabelCachePath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectCachePath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelCachePathHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtMetadataPath" label="${LabelMetadataPath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectMetadataPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelMetadataPathHelp}</div>
|
||||
<input type="hidden" id="txtMetadataNetworkPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${QuickConnect}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkQuickConnectAvailable" />
|
||||
<span>${EnableQuickConnect}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2>${HeaderPerformance}</h2>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtLibraryScanFanoutConcurrency" label="${LibraryScanFanoutConcurrency}" placeholder="0" type="number" pattern="[0-9]*" min="0" step="1" />
|
||||
<div class="fieldDescription">${LibraryScanFanoutConcurrencyHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtParallelImageEncodingLimit" label="${LabelParallelImageEncodingLimit}" placeholder="0" type="number" pattern="[0-9]*" min="0" step="1" />
|
||||
<div class="fieldDescription">${LabelParallelImageEncodingLimitHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,105 +0,0 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-textarea/emby-textarea';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, languageOptions, systemInfo) {
|
||||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
page.querySelector('#txtMetadataPath').value = systemInfo.InternalMetadataPath || '';
|
||||
page.querySelector('#txtMetadataNetworkPath').value = systemInfo.MetadataNetworkPath || '';
|
||||
const localizationLanguageElem = page.querySelector('#selectLocalizationLanguage');
|
||||
localizationLanguageElem.innerHTML = languageOptions.map(function (language) {
|
||||
return '<option value="' + language.Value + '">' + language.Name + '</option>';
|
||||
}).join('');
|
||||
localizationLanguageElem.value = config.UICulture;
|
||||
page.querySelector('#txtLibraryScanFanoutConcurrency').value = config.LibraryScanFanoutConcurrency || '';
|
||||
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.ServerName = form.querySelector('#txtServerName').value;
|
||||
config.UICulture = form.querySelector('#selectLocalizationLanguage').value;
|
||||
config.CachePath = form.querySelector('#txtCachePath').value;
|
||||
config.MetadataPath = form.querySelector('#txtMetadataPath').value;
|
||||
config.MetadataNetworkPath = form.querySelector('#txtMetadataNetworkPath').value;
|
||||
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
|
||||
config.LibraryScanFanoutConcurrency = parseInt(form.querySelector('#txtLibraryScanFanoutConcurrency').value || '0', 10);
|
||||
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
|
||||
|
||||
return ApiClient.updateServerConfiguration(config)
|
||||
.then(() => {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
}).catch(() => {
|
||||
loading.hide();
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCachePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectServerCachePath'),
|
||||
instruction: globalize.translate('HeaderSelectServerCachePathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
path: view.querySelector('#txtMetadataPath').value,
|
||||
networkSharePath: view.querySelector('#txtMetadataNetworkPath').value,
|
||||
callback: function (path, networkPath) {
|
||||
if (path) {
|
||||
view.querySelector('#txtMetadataPath').value = path;
|
||||
}
|
||||
|
||||
if (networkPath) {
|
||||
view.querySelector('#txtMetadataNetworkPath').value = networkPath;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectMetadataPath'),
|
||||
instruction: globalize.translate('HeaderSelectMetadataPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('.dashboardGeneralForm', view).off('submit', onSubmit).on('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
const promiseConfig = ApiClient.getServerConfiguration();
|
||||
const promiseLanguageOptions = ApiClient.getJSON(ApiClient.getUrl('Localization/Options'));
|
||||
const promiseSystemInfo = ApiClient.getSystemInfo();
|
||||
Promise.all([promiseConfig, promiseLanguageOptions, promiseSystemInfo]).then(function (responses) {
|
||||
loadPage(view, responses[0], responses[1], responses[2]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Api } from '@jellyfin/sdk';
|
||||
import { getLocalizationApi } from '@jellyfin/sdk/lib/utils/api/localization-api';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useApi } from 'hooks/useApi';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
|
||||
const fetchLocalizationOptions = async (api: Api, options?: AxiosRequestConfig) => {
|
||||
const response = await getLocalizationApi(api).getLocalizationOptions(options);
|
||||
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const useLocalizationOptions = () => {
|
||||
const { api } = useApi();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [ 'LocalizationOptions' ],
|
||||
queryFn: ({ signal }) => fetchLocalizationOptions(api!, { signal }),
|
||||
enabled: !!api
|
||||
});
|
||||
};
|
|
@ -5,6 +5,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
|||
{ path: 'activity', type: AppType.Dashboard },
|
||||
{ path: 'branding', type: AppType.Dashboard },
|
||||
{ path: 'devices', type: AppType.Dashboard },
|
||||
{ path: 'settings', type: AppType.Dashboard },
|
||||
{ path: 'keys', type: AppType.Dashboard },
|
||||
{ path: 'libraries/display', type: AppType.Dashboard },
|
||||
{ path: 'libraries/metadata', type: AppType.Dashboard },
|
||||
|
|
|
@ -9,13 +9,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
controller: 'dashboard',
|
||||
view: 'dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'general',
|
||||
view: 'general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking',
|
||||
pageProps: {
|
||||
|
|
267
src/apps/dashboard/routes/settings/index.tsx
Normal file
267
src/apps/dashboard/routes/settings/index.tsx
Normal file
|
@ -0,0 +1,267 @@
|
|||
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')}
|
||||
FormHelperTextProps={{ component: Stack }}
|
||||
helperText={(
|
||||
<>
|
||||
<span>{globalize.translate('LabelDisplayLanguageHelp')}</span>
|
||||
<Link href='https://jellyfin.org/docs/general/contributing/#translating'>
|
||||
{globalize.translate('LearnHowYouCanContribute')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
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>
|
||||
|
||||
<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'
|
||||
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