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:
commit
4334303632
8 changed files with 231 additions and 120 deletions
|
@ -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>
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
@ -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 },
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
|
186
src/apps/dashboard/routes/libraries/nfo.tsx
Normal file
186
src/apps/dashboard/routes/libraries/nfo.tsx
Normal 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';
|
38
src/components/SimpleAlert.tsx
Normal file
38
src/components/SimpleAlert.tsx
Normal 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;
|
|
@ -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",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue