diff --git a/src/apps/dashboard/controllers/metadataImages.js b/src/apps/dashboard/controllers/metadataImages.js deleted file mode 100644 index 5df096a6f1..0000000000 --- a/src/apps/dashboard/controllers/metadataImages.js +++ /dev/null @@ -1,94 +0,0 @@ -import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution'; - -import 'jquery'; - -import loading from 'components/loading/loading'; -import globalize from 'lib/globalize'; -import Dashboard from 'utils/dashboard'; - -import 'components/listview/listview.scss'; - -function populateImageResolutionOptions(select) { - let html = ''; - [ - { - name: globalize.translate('ResolutionMatchSource'), - value: ImageResolution.MatchSource - }, - { name: '2160p', value: ImageResolution.P2160 }, - { name: '1440p', value: ImageResolution.P1440 }, - { name: '1080p', value: ImageResolution.P1080 }, - { name: '720p', value: ImageResolution.P720 }, - { name: '480p', value: ImageResolution.P480 }, - { name: '360p', value: ImageResolution.P360 }, - { name: '240p', value: ImageResolution.P240 }, - { name: '144p', value: ImageResolution.P144 } - ].forEach(({ value, name }) => { - html += ``; - }); - select.innerHTML = html; -} - -function populateLanguages(select) { - return ApiClient.getCultures().then(function(languages) { - let html = ''; - html += ""; - for (let i = 0, length = languages.length; i < length; i++) { - const culture = languages[i]; - html += "'; - } - select.innerHTML = html; - }); -} - -function populateCountries(select) { - return ApiClient.getCountries().then(function(allCountries) { - let html = ''; - html += ""; - for (let i = 0, length = allCountries.length; i < length; i++) { - const culture = allCountries[i]; - html += "'; - } - select.innerHTML = html; - }); -} - -function loadPage(page) { - const promises = [ - ApiClient.getServerConfiguration(), - populateLanguages(page.querySelector('#selectLanguage')), - populateCountries(page.querySelector('#selectCountry')) - ]; - - populateImageResolutionOptions(page.querySelector('#txtChapterImageResolution')); - - Promise.all(promises).then(function(responses) { - const config = responses[0]; - page.querySelector('#selectLanguage').value = config.PreferredMetadataLanguage || ''; - page.querySelector('#selectCountry').value = config.MetadataCountryCode || ''; - page.querySelector('#valDummyChapterDuration').value = config.DummyChapterDuration || '0'; - page.querySelector('#txtChapterImageResolution').value = config.ChapterImageResolution || ''; - loading.hide(); - }); -} - -function onSubmit() { - const form = this; - loading.show(); - ApiClient.getServerConfiguration().then(function(config) { - config.PreferredMetadataLanguage = form.querySelector('#selectLanguage').value; - config.MetadataCountryCode = form.querySelector('#selectCountry').value; - config.DummyChapterDuration = form.querySelector('#valDummyChapterDuration').value; - config.ChapterImageResolution = form.querySelector('#txtChapterImageResolution').value; - ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult); - }); - return false; -} - -$(document).on('pageinit', '#metadataImagesConfigurationPage', function() { - $('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#metadataImagesConfigurationPage', function() { - loading.show(); - loadPage(this); -}); - diff --git a/src/apps/dashboard/controllers/metadataimages.html b/src/apps/dashboard/controllers/metadataimages.html deleted file mode 100644 index a79d59fc63..0000000000 --- a/src/apps/dashboard/controllers/metadataimages.html +++ /dev/null @@ -1,44 +0,0 @@ -
- -
- -
- -
-
-

${HeaderPreferredMetadataLanguage}

- -

${DefaultMetadataLangaugeDescription}

- -
- -
- -
- -
-
- -
-

${HeaderDummyChapter}

-
- -
${LabelDummyChapterDurationHelp}
-
-
- -
-
${LabelChapterImageResolutionHelp}
-
-
-
- -
-
- -
-
-
- -
-
diff --git a/src/apps/dashboard/features/libraries/api/useCountries.ts b/src/apps/dashboard/features/libraries/api/useCountries.ts new file mode 100644 index 0000000000..8acba7726f --- /dev/null +++ b/src/apps/dashboard/features/libraries/api/useCountries.ts @@ -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 fetchCountries = async (api: Api, options?: AxiosRequestConfig) => { + const response = await getLocalizationApi(api).getCountries(options); + + return response.data; +}; + +export const useCountries = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'Countries' ], + queryFn: ({ signal }) => fetchCountries(api!, { signal }), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/libraries/api/useCultures.ts b/src/apps/dashboard/features/libraries/api/useCultures.ts new file mode 100644 index 0000000000..0df27178bf --- /dev/null +++ b/src/apps/dashboard/features/libraries/api/useCultures.ts @@ -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 fetchCultures = async (api: Api, options?: AxiosRequestConfig) => { + const response = await getLocalizationApi(api).getCultures(options); + + return response.data; +}; + +export const useCultures = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'Cultures' ], + queryFn: ({ signal }) => fetchCultures(api!, { signal }), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/libraries/utils/metadataOptions.ts b/src/apps/dashboard/features/libraries/utils/metadataOptions.ts new file mode 100644 index 0000000000..2f49789e97 --- /dev/null +++ b/src/apps/dashboard/features/libraries/utils/metadataOptions.ts @@ -0,0 +1,19 @@ +import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution'; +import globalize from 'lib/globalize'; + +export function getImageResolutionOptions() { + return [ + { + name: globalize.translate('ResolutionMatchSource'), + value: ImageResolution.MatchSource + }, + { name: '2160p', value: ImageResolution.P2160 }, + { name: '1440p', value: ImageResolution.P1440 }, + { name: '1080p', value: ImageResolution.P1080 }, + { name: '720p', value: ImageResolution.P720 }, + { name: '480p', value: ImageResolution.P480 }, + { name: '360p', value: ImageResolution.P360 }, + { name: '240p', value: ImageResolution.P240 }, + { name: '144p', value: ImageResolution.P144 } + ]; +}; diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 3d71e5f018..5a7398da39 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -7,6 +7,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'devices', type: AppType.Dashboard }, { path: 'keys', type: AppType.Dashboard }, { path: 'libraries/display', type: AppType.Dashboard }, + { path: 'libraries/metadata', 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 c606c4f1e1..b690b45440 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/metadata', - pageProps: { - appType: AppType.Dashboard, - controller: 'metadataImages', - view: 'metadataimages.html' - } }, { path: 'libraries/nfo', pageProps: { diff --git a/src/apps/dashboard/routes/libraries/metadata.tsx b/src/apps/dashboard/routes/libraries/metadata.tsx new file mode 100644 index 0000000000..01de039cca --- /dev/null +++ b/src/apps/dashboard/routes/libraries/metadata.tsx @@ -0,0 +1,159 @@ +import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution'; +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 MenuItem from '@mui/material/MenuItem'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { useCountries } from 'apps/dashboard/features/libraries/api/useCountries'; +import { useCultures } from 'apps/dashboard/features/libraries/api/useCultures'; +import { getImageResolutionOptions } from 'apps/dashboard/features/libraries/utils/metadataOptions'; +import Loading from 'components/loading/LoadingComponent'; +import Page from 'components/Page'; +import ServerConnections from 'components/ServerConnections'; +import { useConfiguration } from 'hooks/useConfiguration'; +import globalize from 'lib/globalize'; +import React from 'react'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; +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 formData = await request.formData(); + const data = Object.fromEntries(formData); + + const { data: config } = await getConfigurationApi(api).getConfiguration(); + + config.PreferredMetadataLanguage = data.Language.toString(); + config.MetadataCountryCode = data.Country.toString(); + config.DummyChapterDuration = parseInt(data.DummyChapterDuration.toString(), 10); + config.ChapterImageResolution = data.ChapterImageResolution.toString() as ImageResolution; + + await getConfigurationApi(api) + .updateConfiguration({ serverConfiguration: config }); + + return { + isSaved: true + }; +}; + +export const Component = () => { + const { + data: config, + isPending: isConfigPending, + isError: isConfigError + } = useConfiguration(); + const { + data: cultures, + isPending: isCulturesPending, + isError: isCulturesError + } = useCultures(); + const { + data: countries, + isPending: isCountriesPending, + isError: isCountriesError + } = useCountries(); + + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const isSubmitting = navigation.state === 'submitting'; + + const imageResolutions = getImageResolutionOptions(); + + if (isConfigPending || isCulturesPending || isCountriesPending) { + return ; + } + + return ( + + + {isConfigError || isCulturesError || isCountriesError ? ( + {globalize.translate('MetadataImagesLoadError')} + ) : ( +
+ + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + {globalize.translate('HeaderPreferredMetadataLanguage')} + {globalize.translate('DefaultMetadataLangaugeDescription')} + + + {cultures.map(culture => { + return {culture.DisplayName}; + })} + + + + {countries.map(country => { + return {country.DisplayName}; + })} + + + {globalize.translate('HeaderDummyChapter')} + + + + + {imageResolutions.map(resolution => { + return {resolution.name}; + })} + + + + +
+ )} +
+
+ ); +}; + +Component.displayName = 'MetadataImagesPage'; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 0601312d34..8007057b71 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1172,6 +1172,7 @@ "MessageUnableToConnectToServer": "We're unable to connect to the selected server right now. Please ensure it is running and try again.", "MessageUnauthorizedUser": "You are not authorized to access the server at this time. Please contact your server administrator for more information.", "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", "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",