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 @@ -
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