diff --git a/src/apps/dashboard/controllers/librarydisplay.html b/src/apps/dashboard/controllers/librarydisplay.html
deleted file mode 100644
index d641e30c1e..0000000000
--- a/src/apps/dashboard/controllers/librarydisplay.html
+++ /dev/null
@@ -1,57 +0,0 @@
-
diff --git a/src/apps/dashboard/controllers/librarydisplay.js b/src/apps/dashboard/controllers/librarydisplay.js
deleted file mode 100644
index 74be51e762..0000000000
--- a/src/apps/dashboard/controllers/librarydisplay.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import loading from 'components/loading/loading';
-import 'elements/emby-checkbox/emby-checkbox';
-import 'elements/emby-button/emby-button';
-import Dashboard from 'utils/dashboard';
-
-export default function(view) {
- function loadData() {
- ApiClient.getServerConfiguration().then(function(config) {
- view.querySelector('.chkFolderView').checked = config.EnableFolderView;
- view.querySelector('.chkGroupMoviesIntoCollections').checked = config.EnableGroupingIntoCollections;
- view.querySelector('.chkDisplaySpecialsWithinSeasons').checked = config.DisplaySpecialsWithinSeasons;
- view.querySelector('.chkExternalContentInSuggestions').checked = config.EnableExternalContentInSuggestions;
- view.querySelector('#chkSaveMetadataHidden').checked = config.SaveMetadataHidden;
- });
- ApiClient.getNamedConfiguration('metadata').then(function(metadata) {
- view.querySelector('#selectDateAdded').selectedIndex = metadata.UseFileCreationTimeForDateAdded ? 1 : 0;
- });
- }
-
- view.querySelector('form').addEventListener('submit', function(e) {
- loading.show();
- const form = this;
- ApiClient.getServerConfiguration().then(function(config) {
- config.EnableFolderView = form.querySelector('.chkFolderView').checked;
- config.EnableGroupingIntoCollections = form.querySelector('.chkGroupMoviesIntoCollections').checked;
- config.DisplaySpecialsWithinSeasons = form.querySelector('.chkDisplaySpecialsWithinSeasons').checked;
- config.EnableExternalContentInSuggestions = form.querySelector('.chkExternalContentInSuggestions').checked;
- config.SaveMetadataHidden = form.querySelector('#chkSaveMetadataHidden').checked;
- ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
- });
- ApiClient.getNamedConfiguration('metadata').then(function(config) {
- config.UseFileCreationTimeForDateAdded = form.querySelector('#selectDateAdded').value === '1';
- ApiClient.updateNamedConfiguration('metadata', config);
- });
-
- e.preventDefault();
- loading.hide();
- return false;
- });
-
- view.addEventListener('viewshow', function() {
- loadData();
- ApiClient.getSystemInfo().then(function(info) {
- if (info.OperatingSystem === 'Windows') {
- view.querySelector('.fldSaveMetadataHidden').classList.remove('hide');
- } else {
- view.querySelector('.fldSaveMetadataHidden').classList.add('hide');
- }
- });
- });
-}
-
diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts
index 19cf7ea1a5..700b175cbe 100644
--- a/src/apps/dashboard/routes/_asyncRoutes.ts
+++ b/src/apps/dashboard/routes/_asyncRoutes.ts
@@ -6,6 +6,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'branding', type: AppType.Dashboard },
{ path: 'devices', type: AppType.Dashboard },
{ path: 'keys', type: AppType.Dashboard },
+ { path: 'libraries/display', type: AppType.Dashboard },
{ path: 'logs', type: AppType.Dashboard },
{ path: 'playback/resume', type: AppType.Dashboard },
{ path: 'playback/streaming', type: AppType.Dashboard },
diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts
index 848c242b67..c606c4f1e1 100644
--- a/src/apps/dashboard/routes/_legacyRoutes.ts
+++ b/src/apps/dashboard/routes/_legacyRoutes.ts
@@ -30,13 +30,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'library',
view: 'library.html'
}
- }, {
- path: 'libraries/display',
- pageProps: {
- appType: AppType.Dashboard,
- controller: 'librarydisplay',
- view: 'librarydisplay.html'
- }
}, {
path: 'playback/transcoding',
pageProps: {
diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx
new file mode 100644
index 0000000000..697b448ce5
--- /dev/null
+++ b/src/apps/dashboard/routes/libraries/display.tsx
@@ -0,0 +1,166 @@
+import React from 'react';
+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 { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
+import { useConfiguration } from 'hooks/useConfiguration';
+import { fetchNamedConfiguration, useNamedConfiguration } from 'hooks/useNamedConfiguration';
+import globalize from 'lib/globalize';
+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();
+ const namedConfig = await fetchNamedConfiguration(api, 'metadata');
+
+ namedConfig.UseFileCreationTimeForDateAdded = data.DateAddedBehavior.toString() === '1';
+ config.EnableFolderView = data.DisplayFolderView?.toString() === 'on';
+ config.DisplaySpecialsWithinSeasons = data.DisplaySpecialsWithinSeasons?.toString() === 'on';
+ config.EnableGroupingIntoCollections = data.GroupMoviesIntoCollections?.toString() === 'on';
+ config.EnableExternalContentInSuggestions = data.EnableExternalContentInSuggestions?.toString() === 'on';
+
+ await getConfigurationApi(api)
+ .updateConfiguration({ serverConfiguration: config });
+
+ await getConfigurationApi(api)
+ .updateNamedConfiguration({ key: 'metadata', body: namedConfig });
+
+ return {
+ isSaved: true
+ };
+};
+
+export const Component = () => {
+ const {
+ data: config,
+ isPending: isConfigPending,
+ isError: isConfigError
+ } = useConfiguration();
+ const {
+ data: namedConfig,
+ isPending: isNamedConfigPending,
+ isError: isNamedConfigError
+ } = useNamedConfiguration('metadata');
+
+ const navigation = useNavigation();
+ const actionData = useActionData() as ActionData | undefined;
+ const isSubmitting = navigation.state === 'submitting';
+
+ if (isConfigPending || isNamedConfigPending) {
+ return ;
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+Component.displayName = 'DisplayPage';
diff --git a/src/hooks/useConfiguration.ts b/src/hooks/useConfiguration.ts
index f1b9d47c3d..ea6d865c92 100644
--- a/src/hooks/useConfiguration.ts
+++ b/src/hooks/useConfiguration.ts
@@ -6,12 +6,7 @@ import type { AxiosRequestConfig } from 'axios';
export const QUERY_KEY = 'Configuration';
-export const fetchConfiguration = async (api?: Api, options?: AxiosRequestConfig) => {
- if (!api) {
- console.error('[useLogOptions] No API instance available');
- return;
- }
-
+export const fetchConfiguration = async (api: Api, options?: AxiosRequestConfig) => {
const response = await getConfigurationApi(api).getConfiguration(options);
return response.data;
@@ -22,7 +17,7 @@ export const useConfiguration = () => {
return useQuery({
queryKey: [QUERY_KEY],
- queryFn: ({ signal }) => fetchConfiguration(api, { signal }),
+ queryFn: ({ signal }) => fetchConfiguration(api!, { signal }),
enabled: !!api
});
};
diff --git a/src/hooks/useNamedConfiguration.ts b/src/hooks/useNamedConfiguration.ts
new file mode 100644
index 0000000000..66652b510a
--- /dev/null
+++ b/src/hooks/useNamedConfiguration.ts
@@ -0,0 +1,27 @@
+import { Api } from '@jellyfin/sdk';
+import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
+import { useQuery } from '@tanstack/react-query';
+import { useApi } from 'hooks/useApi';
+import type { AxiosRequestConfig } from 'axios';
+
+export const QUERY_KEY = 'NamedConfiguration';
+
+interface NamedConfiguration {
+ [key: string]: unknown;
+}
+
+export const fetchNamedConfiguration = async (api: Api, key: string, options?: AxiosRequestConfig) => {
+ const response = await getConfigurationApi(api).getNamedConfiguration({ key }, options);
+
+ return response.data as unknown as NamedConfiguration;
+};
+
+export const useNamedConfiguration = (key: string) => {
+ const { api } = useApi();
+
+ return useQuery({
+ queryKey: [ QUERY_KEY ],
+ queryFn: ({ signal }) => fetchNamedConfiguration(api!, key, { signal }),
+ enabled: !!api
+ });
+};
diff --git a/src/strings/en-us.json b/src/strings/en-us.json
index 5377cbd88a..6f71ba9898 100644
--- a/src/strings/en-us.json
+++ b/src/strings/en-us.json
@@ -241,6 +241,7 @@
"Display": "Display",
"DisplayInMyMedia": "Display on home screen",
"DisplayInOtherHomeScreenSections": "Display in home screen sections such as 'Recently Added Media' and 'Continue Watching'",
+ "DisplayLoadError": "An error occurred while loading display configuration data.",
"DisplayMissingEpisodesWithinSeasons": "Display missing episodes within seasons",
"DisplayMissingEpisodesWithinSeasonsHelp": "This must also be enabled for TV libraries in the server configuration.",
"DisplayModeHelp": "Select the layout style you want for the interface.",