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

Merge pull request #6594 from viown/react-libraries-nfo-settings

Migrate NFO Settings to React
This commit is contained in:
Bill Thornton 2025-03-06 12:43:30 -05:00 committed by GitHub
commit 4334303632
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 231 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: 'libraries/display', type: AppType.Dashboard },
{ path: 'libraries/metadata', type: AppType.Dashboard },
{ path: 'libraries/nfo', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard },
{ path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard },
{ path: 'playback/resume', type: AppType.Dashboard },

View file

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

View file

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

View file

@ -0,0 +1,186 @@
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: 'yyyy-MM-dd',
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}
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>
<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,38 @@
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}>
{title && (
<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.",
"MetadataImagesLoadError": "Failed to load metadata settings",
"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'.",
"MillisecondsUnit": "ms",
"MinutesAfter": "minutes after",