1
0
Fork 0
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:
viown 2025-03-06 10:05:09 +03:00
parent 3c62c1dc51
commit f94e16d56a
7 changed files with 294 additions and 198 deletions

View file

@ -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>

View file

@ -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]);
});
});
}

View file

@ -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
});
};

View file

@ -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 },

View file

@ -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: {

View 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';

View file

@ -822,7 +822,7 @@
"LabelOriginalTitle": "Original title",
"LabelOverview": "Overview",
"LabelParallelImageEncodingLimit": "Parallel image encoding limit",
"LabelParallelImageEncodingLimitHelp": "Maximum number of image encodings that are allowed to run in parallel. Setting this to 0 will choose a limit based on your systems core count.",
"LabelParallelImageEncodingLimitHelp": "Maximum number of image encodings that are allowed to run in parallel. Leaving this empty will choose a limit based on your systems core count.",
"LabelParentalRating": "Parental rating",
"LabelParentNumber": "Parent number",
"LabelPassword": "Password",
@ -1019,7 +1019,7 @@
"LibraryAccessHelp": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.",
"LibraryNameInvalid": "Library name cannot be empty.",
"LibraryScanFanoutConcurrency": "Parallel library scan tasks limit",
"LibraryScanFanoutConcurrencyHelp": "Maximum number of parallel tasks during library scans. Setting this to 0 will choose a limit based on your systems core count. WARNING: Setting this number too high may cause issues with network file systems; if you encounter problems lower this number.",
"LibraryScanFanoutConcurrencyHelp": "Maximum number of parallel tasks during library scans. Leaving this empty will choose a limit based on your systems core count. WARNING: Setting this number too high may cause issues with network file systems; if you encounter problems lower this number.",
"LibraryInvalidItemIdError": "The library is in an invalid state and cannot be edited. You are possibly encountering a bug: the path in the database is not the correct path on the filesystem.",
"LimitSupportedVideoResolution": "Limit maximum supported video resolution",
"LimitSupportedVideoResolutionHelp": "Use 'Maximum Allowed Video Transcoding Resolution' as maximum supported video resolution.",
@ -1495,6 +1495,7 @@
"ServerRestartNeededAfterPluginInstall": "Jellyfin will need to be restarted after installing the plugin.",
"ServerUpdateNeeded": "This server needs to be updated. To download the latest version, please visit {0}",
"Settings": "Settings",
"SettingsPageLoadError": "Failed to load settings page",
"SettingsSaved": "Settings saved.",
"SettingsWarning": "Changing these values may cause instability or connectivity failures. If you experience any problems, we recommend changing them back to default.",
"Share": "Share",