diff --git a/src/apps/dashboard/controllers/metadatanfo.html b/src/apps/dashboard/controllers/metadatanfo.html deleted file mode 100644 index 62f18b2641..0000000000 --- a/src/apps/dashboard/controllers/metadatanfo.html +++ /dev/null @@ -1,49 +0,0 @@ -
- -
- -
-
- -

${HeaderKodiMetadataHelp}

-
-
- -
${LabelKodiMetadataUserHelp}
-
- -
- -
${LabelKodiMetadataDateFormatHelp}
-
-
- -
${LabelKodiMetadataSaveImagePathsHelp}
-
-
- -
-
${LabelKodiMetadataEnablePathSubstitutionHelp}
-
-
-
- -
${LabelKodiMetadataEnableExtraThumbsHelp}
-
-
-
-
- -
-
diff --git a/src/apps/dashboard/controllers/metadatanfo.js b/src/apps/dashboard/controllers/metadatanfo.js deleted file mode 100644 index 51b7eee36b..0000000000 --- a/src/apps/dashboard/controllers/metadatanfo.js +++ /dev/null @@ -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 = ''; - html += users.map(function (user) { - return ''; - }).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('

') - }); -} - -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]); - }); -}); - diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 5a7398da39..1a11b608b4 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -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 }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index b690b45440..2af71b7473 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -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: { diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx index 3c958bcb33..8da3f2453b 100644 --- a/src/apps/dashboard/routes/libraries/display.tsx +++ b/src/apps/dashboard/routes/libraries/display.tsx @@ -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; diff --git a/src/apps/dashboard/routes/libraries/nfo.tsx b/src/apps/dashboard/routes/libraries/nfo.tsx new file mode 100644 index 0000000000..c0dff0f336 --- /dev/null +++ b/src/apps/dashboard/routes/libraries/nfo.tsx @@ -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 ; + } + + return ( + + + + {isConfigError || isUsersError ? ( + {globalize.translate('MetadataNfoLoadError')} + ) : ( +
+ + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + {globalize.translate('TabNfoSettings')} + {globalize.translate('HeaderKodiMetadataHelp')} + + + {globalize.translate('None')} + {users.map(user => + {user.Name} + )} + + + + yyyy-MM-dd + + + + + } + label={globalize.translate('LabelKodiMetadataSaveImagePaths')} + /> + {globalize.translate('LabelKodiMetadataSaveImagePathsHelp')} + + + + + } + label={globalize.translate('LabelKodiMetadataEnablePathSubstitution')} + /> + {globalize.translate('LabelKodiMetadataEnablePathSubstitutionHelp')} + + + + + } + label={globalize.translate('LabelKodiMetadataEnableExtraThumbs')} + /> + {globalize.translate('LabelKodiMetadataEnableExtraThumbsHelp')} + + + + +
+ )} +
+
+ ); +}; + +Component.displayName = 'NFOSettingsPage'; diff --git a/src/components/SimpleAlert.tsx b/src/components/SimpleAlert.tsx new file mode 100644 index 0000000000..c67465211f --- /dev/null +++ b/src/components/SimpleAlert.tsx @@ -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 ( + + + {title} + + + + {text} + + + + + + + ); +}; + +export default SimpleAlert; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 8007057b71..71de76a2ab 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -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",