1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Migrate NFO Settings to React

This commit is contained in:
viown 2025-03-04 19:46:54 +03:00
parent e80b890bd2
commit 10d731e697
8 changed files with 240 additions and 120 deletions

View file

@ -1,49 +0,0 @@
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${TabNfoSettings}">
<div>
<div class="content-primary">
<form class="metadataNfoForm">
<p>${HeaderKodiMetadataHelp}</p>
<br />
<div class="selectContainer">
<select is="emby-select" name="selectUser" id="selectUser" label="${LabelKodiMetadataUser}"></select>
<div class="fieldDescription">${LabelKodiMetadataUserHelp}</div>
</div>
<div class="selectContainer">
<select is="emby-select" name="selectReleaseDateFormat" id="selectReleaseDateFormat" label="${LabelKodiMetadataDateFormat}">
<option value="yyyy-MM-dd">yyyy-MM-dd</option>
</select>
<div class="fieldDescription">${LabelKodiMetadataDateFormatHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkSaveImagePaths" />
<span>${LabelKodiMetadataSaveImagePaths}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelKodiMetadataSaveImagePathsHelp}</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnablePathSubstitution" />
<span>${LabelKodiMetadataEnablePathSubstitution}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">
<div>${LabelKodiMetadataEnablePathSubstitutionHelp}</div>
</div>
</div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableExtraThumbs" />
<span>${LabelKodiMetadataEnableExtraThumbs}</span>
</label>
<div class="fieldDescription checkboxFieldDescription">${LabelKodiMetadataEnableExtraThumbsHelp}</div>
</div>
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
</form>
</div>
</div>
</div>

View file

@ -1,61 +0,0 @@
import escapeHtml from 'escape-html';
import 'jquery';
import loading from 'components/loading/loading';
import globalize from 'lib/globalize';
import Dashboard from 'utils/dashboard';
import alert from 'components/alert';
function loadPage(page, config, users) {
let html = '<option value="" selected="selected">' + globalize.translate('None') + '</option>';
html += users.map(function (user) {
return '<option value="' + user.Id + '">' + escapeHtml(user.Name) + '</option>';
}).join('');
const elem = page.querySelector('#selectUser');
elem.innerHTML = html;
elem.value = config.UserId || '';
page.querySelector('#selectReleaseDateFormat').value = config.ReleaseDateFormat;
page.querySelector('#chkSaveImagePaths').checked = config.SaveImagePathsInNfo;
page.querySelector('#chkEnablePathSubstitution').checked = config.EnablePathSubstitution;
page.querySelector('#chkEnableExtraThumbs').checked = config.EnableExtraThumbsDuplication;
loading.hide();
}
function onSubmit() {
loading.show();
const form = this;
ApiClient.getNamedConfiguration(metadataKey).then(function (config) {
config.UserId = form.querySelector('#selectUser').value || null;
config.ReleaseDateFormat = form.querySelector('#selectReleaseDateFormat').value;
config.SaveImagePathsInNfo = form.querySelector('#chkSaveImagePaths').checked;
config.EnablePathSubstitution = form.querySelector('#chkEnablePathSubstitution').checked;
config.EnableExtraThumbsDuplication = form.querySelector('#chkEnableExtraThumbs').checked;
ApiClient.updateNamedConfiguration(metadataKey, config).then(function () {
Dashboard.processServerConfigurationUpdateResult();
showConfirmMessage();
});
});
return false;
}
function showConfirmMessage() {
const msg = [];
msg.push(globalize.translate('MetadataSettingChangeHelp'));
alert({
text: msg.join('<br/><br/>')
});
}
const metadataKey = 'xbmcmetadata';
$(document).on('pageinit', '#metadataNfoPage', function () {
$('.metadataNfoForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#metadataNfoPage', function () {
loading.show();
const page = this;
const promise1 = ApiClient.getUsers();
const promise2 = ApiClient.getNamedConfiguration(metadataKey);
Promise.all([promise1, promise2]).then(function (responses) {
loadPage(page, responses[1], responses[0]);
});
});

View file

@ -8,6 +8,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'keys', type: AppType.Dashboard }, { path: 'keys', type: AppType.Dashboard },
{ path: 'libraries/display', type: AppType.Dashboard }, { path: 'libraries/display', type: AppType.Dashboard },
{ path: 'libraries/metadata', type: AppType.Dashboard }, { path: 'libraries/metadata', type: AppType.Dashboard },
{ path: 'libraries/nfo', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard }, { path: 'logs', type: AppType.Dashboard },
{ path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard }, { path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard },
{ path: 'playback/resume', type: AppType.Dashboard }, { path: 'playback/resume', type: AppType.Dashboard },

View file

@ -37,13 +37,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'encodingsettings', controller: 'encodingsettings',
view: 'encodingsettings.html' view: 'encodingsettings.html'
} }
}, {
path: 'libraries/nfo',
pageProps: {
appType: AppType.Dashboard,
controller: 'metadatanfo',
view: 'metadatanfo.html'
}
}, { }, {
path: 'plugins/catalog', path: 'plugins/catalog',
pageProps: { pageProps: {

View file

@ -21,6 +21,8 @@ import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'rea
import { ActionData } from 'types/actionData'; import { ActionData } from 'types/actionData';
import { queryClient } from 'utils/query/queryClient'; import { queryClient } from 'utils/query/queryClient';
const CONFIG_KEY = 'metadata';
export const action = async ({ request }: ActionFunctionArgs) => { export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi(); const api = ServerConnections.getCurrentApi();
if (!api) throw new Error('No Api instance available'); if (!api) throw new Error('No Api instance available');
@ -43,13 +45,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
.updateConfiguration({ serverConfiguration: config }); .updateConfiguration({ serverConfiguration: config });
await getConfigurationApi(api) await getConfigurationApi(api)
.updateNamedConfiguration({ key: 'metadata', body: metadataConfig }); .updateNamedConfiguration({ key: CONFIG_KEY, body: metadataConfig });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
queryKey: [ CONFIG_QUERY_KEY ] queryKey: [ CONFIG_QUERY_KEY ]
}); });
void queryClient.invalidateQueries({ void queryClient.invalidateQueries({
queryKey: [ NAMED_CONFIG_QUERY_KEY, 'metadata' ] queryKey: [ NAMED_CONFIG_QUERY_KEY, CONFIG_KEY ]
}); });
return { return {
@ -67,7 +69,7 @@ export const Component = () => {
data: namedConfig, data: namedConfig,
isPending: isNamedConfigPending, isPending: isNamedConfigPending,
isError: isNamedConfigError isError: isNamedConfigError
} = useNamedConfiguration('metadata'); } = useNamedConfiguration(CONFIG_KEY);
const navigation = useNavigation(); const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined; const actionData = useActionData() as ActionData | undefined;

View file

@ -0,0 +1,197 @@
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControl from '@mui/material/FormControl';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormHelperText from '@mui/material/FormHelperText';
import MenuItem from '@mui/material/MenuItem';
import Stack from '@mui/material/Stack';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Loading from 'components/loading/LoadingComponent';
import Page from 'components/Page';
import ServerConnections from 'components/ServerConnections';
import SimpleAlert from 'components/SimpleAlert';
import { QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration';
import { useUsers } from 'hooks/useUsers';
import globalize from 'lib/globalize';
import React, { useCallback, useState } from 'react';
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
import { ActionData } from 'types/actionData';
import { queryClient } from 'utils/query/queryClient';
const CONFIG_KEY = 'xbmcmetadata';
interface NFOSettingsConfig {
UserId?: string;
EnableExtraThumbsDuplication?: boolean;
EnablePathSubstitution?: boolean;
ReleaseDateFormat?: string;
SaveImagePathsInNfo?: boolean;
};
export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi();
if (!api) throw new Error('No Api instance available');
const formData = await request.formData();
const data = Object.fromEntries(formData);
const newConfig: NFOSettingsConfig = {
UserId: data.UserId?.toString(),
ReleaseDateFormat: data.ReleaseDateFormat?.toString(),
SaveImagePathsInNfo: data.SaveImagePathsInNfo?.toString() === 'on',
EnablePathSubstitution: data.EnablePathSubstitution?.toString() === 'on',
EnableExtraThumbsDuplication: data.EnableExtraThumbsDuplication?.toString() === 'on'
};
await getConfigurationApi(api)
.updateNamedConfiguration({ key: CONFIG_KEY, body: newConfig });
void queryClient.invalidateQueries({
queryKey: [QUERY_KEY, CONFIG_KEY]
});
return {
isSaved: true
};
};
export const Component = () => {
const {
data: config,
isPending: isConfigPending,
isError: isConfigError
} = useNamedConfiguration(CONFIG_KEY);
const {
data: users,
isPending: isUsersPending,
isError: isUsersError
} = useUsers();
const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const [isAlertOpen, setIsAlertOpen] = useState(false);
const nfoConfig = config as NFOSettingsConfig;
const onAlertClose = useCallback(() => {
setIsAlertOpen(false);
}, []);
const onSubmit = useCallback(() => {
setIsAlertOpen(true);
}, []);
if (isConfigPending || isUsersPending) {
return <Loading />;
}
return (
<Page
id='metadataNfoPage'
title={globalize.translate('TabNfoSettings')}
className='type-interior mainAnimatedPage'
>
<SimpleAlert
open={isAlertOpen}
title={''}
text={globalize.translate('MetadataSettingChangeHelp')}
onClose={onAlertClose}
/>
<Box className='content-primary'>
{isConfigError || isUsersError ? (
<Alert severity='error'>{globalize.translate('MetadataNfoLoadError')}</Alert>
) : (
<Form method='POST' onSubmit={onSubmit}>
<Stack spacing={3}>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<Typography variant='h2'>{globalize.translate('TabNfoSettings')}</Typography>
<Typography>{globalize.translate('HeaderKodiMetadataHelp')}</Typography>
<TextField
name={'UserId'}
label={globalize.translate('LabelKodiMetadataUser')}
defaultValue={nfoConfig.UserId || ''}
select
SelectProps={{
displayEmpty: true
}}
InputLabelProps={{
shrink: true
}}
helperText={globalize.translate('LabelKodiMetadataUserHelp')}
>
<MenuItem value=''>{globalize.translate('None')}</MenuItem>
{users.map(user =>
<MenuItem key={user.Id} value={user.Id}>{user.Name}</MenuItem>
)}
</TextField>
<TextField
name={'ReleaseDateFormat'}
label={globalize.translate('LabelKodiMetadataDateFormat')}
defaultValue={nfoConfig.ReleaseDateFormat}
select
helperText={globalize.translate('LabelKodiMetadataDateFormatHelp')}
>
<MenuItem value='yyyy-MM-dd'>yyyy-MM-dd</MenuItem>
</TextField>
<FormControl>
<FormControlLabel
control={
<Switch
name={'SaveImagePathsInNfo'}
defaultChecked={nfoConfig.SaveImagePathsInNfo}
/>
}
label={globalize.translate('LabelKodiMetadataSaveImagePaths')}
/>
<FormHelperText>{globalize.translate('LabelKodiMetadataSaveImagePathsHelp')}</FormHelperText>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Switch
name={'EnablePathSubstitution'}
defaultChecked={nfoConfig.EnablePathSubstitution}
/>
}
label={globalize.translate('LabelKodiMetadataEnablePathSubstitution')}
/>
<FormHelperText>{globalize.translate('LabelKodiMetadataEnablePathSubstitutionHelp')}</FormHelperText>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Switch
name={'EnableExtraThumbsDuplication'}
defaultChecked={nfoConfig.EnableExtraThumbsDuplication}
/>
}
label={globalize.translate('LabelKodiMetadataEnableExtraThumbs')}
/>
<FormHelperText>{globalize.translate('LabelKodiMetadataEnableExtraThumbsHelp')}</FormHelperText>
</FormControl>
<Button type='submit' size='large'>
{globalize.translate('Save')}
</Button>
</Stack>
</Form>
)}
</Box>
</Page>
);
};
Component.displayName = 'NFOSettingsPage';

View file

@ -0,0 +1,36 @@
import Button from '@mui/material/Button';
import Dialog, { type DialogProps } from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import globalize from 'lib/globalize';
import React from 'react';
interface SimpleAlertDialog extends DialogProps {
title: string;
text: string;
onClose: () => void
};
const SimpleAlert = ({ open, title, text, onClose }: SimpleAlertDialog) => {
return (
<Dialog open={open} onClose={onClose}>
<DialogTitle>
{title}
</DialogTitle>
<DialogContent>
<DialogContentText>
{text}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>
{globalize.translate('ButtonGotIt')}
</Button>
</DialogActions>
</Dialog>
);
};
export default SimpleAlert;

View file

@ -1174,6 +1174,7 @@
"MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.", "MessageUnsetContentHelp": "Content will be displayed as plain folders. For best results use the metadata manager to set the content types of sub-folders.",
"MetadataImagesLoadError": "Failed to load metadata settings", "MetadataImagesLoadError": "Failed to load metadata settings",
"MetadataManager": "Metadata Manager", "MetadataManager": "Metadata Manager",
"MetadataNfoLoadError": "Failed to load metadata NFO settings",
"MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the 'Refresh' button, or do bulk refreshes using the 'Metadata Manager'.", "MetadataSettingChangeHelp": "Changing metadata settings will affect new content added going forward. To refresh existing content, open the detail screen and click the 'Refresh' button, or do bulk refreshes using the 'Metadata Manager'.",
"MillisecondsUnit": "ms", "MillisecondsUnit": "ms",
"MinutesAfter": "minutes after", "MinutesAfter": "minutes after",