From 0b47abc0090b9555c946a2e96ba9df00ac843c1a Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Tue, 25 Feb 2025 15:39:17 +0300
Subject: [PATCH 1/4] Migrate libraries display to React
---
.../dashboard/controllers/librarydisplay.html | 57 ------
.../dashboard/controllers/librarydisplay.js | 52 ------
src/apps/dashboard/routes/_asyncRoutes.ts | 1 +
src/apps/dashboard/routes/_legacyRoutes.ts | 7 -
.../dashboard/routes/libraries/display.tsx | 166 ++++++++++++++++++
src/hooks/useConfiguration.ts | 9 +-
src/hooks/useNamedConfiguration.ts | 27 +++
src/strings/en-us.json | 1 +
8 files changed, 197 insertions(+), 123 deletions(-)
delete mode 100644 src/apps/dashboard/controllers/librarydisplay.html
delete mode 100644 src/apps/dashboard/controllers/librarydisplay.js
create mode 100644 src/apps/dashboard/routes/libraries/display.tsx
create mode 100644 src/hooks/useNamedConfiguration.ts
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.",
From b0243adc5b4731dbde1c8faa68c8614d6e8d8d63 Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Tue, 25 Feb 2025 16:57:32 +0300
Subject: [PATCH 2/4] Small refactor
---
.../dashboard/routes/libraries/display.tsx | 156 +++++++++---------
1 file changed, 77 insertions(+), 79 deletions(-)
diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx
index 697b448ce5..c45a2cd983 100644
--- a/src/apps/dashboard/routes/libraries/display.tsx
+++ b/src/apps/dashboard/routes/libraries/display.tsx
@@ -74,90 +74,88 @@ export const Component = () => {
className='mainAnimatedPage type-interior'
>
-
+ )}
);
From 1ab2197200b2d7989c97184dd76da012221fec9b Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Tue, 25 Feb 2025 18:20:29 +0300
Subject: [PATCH 3/4] Invalidate queries
---
src/apps/dashboard/routes/libraries/display.tsx | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx
index c45a2cd983..8b99942805 100644
--- a/src/apps/dashboard/routes/libraries/display.tsx
+++ b/src/apps/dashboard/routes/libraries/display.tsx
@@ -14,11 +14,12 @@ 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 { QUERY_KEY as CONFIG_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
+import { fetchNamedConfiguration, QUERY_KEY as NAMED_CONFIG_QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration';
import globalize from 'lib/globalize';
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
import { ActionData } from 'types/actionData';
+import { queryClient } from 'utils/query/queryClient';
export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi();
@@ -42,6 +43,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
await getConfigurationApi(api)
.updateNamedConfiguration({ key: 'metadata', body: namedConfig });
+ void queryClient.invalidateQueries({
+ queryKey: [ CONFIG_QUERY_KEY ]
+ });
+ void queryClient.invalidateQueries({
+ queryKey: [ NAMED_CONFIG_QUERY_KEY ]
+ });
+
return {
isSaved: true
};
From 7713e31b44bed4c6bba3fbaaae47c0bb41325716 Mon Sep 17 00:00:00 2001
From: viown <48097677+viown@users.noreply.github.com>
Date: Sat, 1 Mar 2025 21:32:15 +0300
Subject: [PATCH 4/4] Add key to named configuration hook
---
src/apps/dashboard/routes/libraries/display.tsx | 12 +++++++-----
src/hooks/useConfiguration.ts | 2 +-
src/hooks/useNamedConfiguration.ts | 6 +++---
3 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/src/apps/dashboard/routes/libraries/display.tsx b/src/apps/dashboard/routes/libraries/display.tsx
index 8b99942805..3c958bcb33 100644
--- a/src/apps/dashboard/routes/libraries/display.tsx
+++ b/src/apps/dashboard/routes/libraries/display.tsx
@@ -15,7 +15,7 @@ import Page from 'components/Page';
import ServerConnections from 'components/ServerConnections';
import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api';
import { QUERY_KEY as CONFIG_QUERY_KEY, useConfiguration } from 'hooks/useConfiguration';
-import { fetchNamedConfiguration, QUERY_KEY as NAMED_CONFIG_QUERY_KEY, useNamedConfiguration } from 'hooks/useNamedConfiguration';
+import { QUERY_KEY as NAMED_CONFIG_QUERY_KEY, NamedConfiguration, useNamedConfiguration } from 'hooks/useNamedConfiguration';
import globalize from 'lib/globalize';
import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
import { ActionData } from 'types/actionData';
@@ -29,9 +29,11 @@ export const action = async ({ request }: ActionFunctionArgs) => {
const data = Object.fromEntries(formData);
const { data: config } = await getConfigurationApi(api).getConfiguration();
- const namedConfig = await fetchNamedConfiguration(api, 'metadata');
- namedConfig.UseFileCreationTimeForDateAdded = data.DateAddedBehavior.toString() === '1';
+ const metadataConfig: NamedConfiguration = {
+ UseFileCreationTimeForDateAdded: data.DateAddedBehavior.toString() === '1'
+ };
+
config.EnableFolderView = data.DisplayFolderView?.toString() === 'on';
config.DisplaySpecialsWithinSeasons = data.DisplaySpecialsWithinSeasons?.toString() === 'on';
config.EnableGroupingIntoCollections = data.GroupMoviesIntoCollections?.toString() === 'on';
@@ -41,13 +43,13 @@ export const action = async ({ request }: ActionFunctionArgs) => {
.updateConfiguration({ serverConfiguration: config });
await getConfigurationApi(api)
- .updateNamedConfiguration({ key: 'metadata', body: namedConfig });
+ .updateNamedConfiguration({ key: 'metadata', body: metadataConfig });
void queryClient.invalidateQueries({
queryKey: [ CONFIG_QUERY_KEY ]
});
void queryClient.invalidateQueries({
- queryKey: [ NAMED_CONFIG_QUERY_KEY ]
+ queryKey: [ NAMED_CONFIG_QUERY_KEY, 'metadata' ]
});
return {
diff --git a/src/hooks/useConfiguration.ts b/src/hooks/useConfiguration.ts
index ea6d865c92..81ddb79f03 100644
--- a/src/hooks/useConfiguration.ts
+++ b/src/hooks/useConfiguration.ts
@@ -16,7 +16,7 @@ export const useConfiguration = () => {
const { api } = useApi();
return useQuery({
- queryKey: [QUERY_KEY],
+ queryKey: [ QUERY_KEY ],
queryFn: ({ signal }) => fetchConfiguration(api!, { signal }),
enabled: !!api
});
diff --git a/src/hooks/useNamedConfiguration.ts b/src/hooks/useNamedConfiguration.ts
index 66652b510a..138355b608 100644
--- a/src/hooks/useNamedConfiguration.ts
+++ b/src/hooks/useNamedConfiguration.ts
@@ -6,11 +6,11 @@ import type { AxiosRequestConfig } from 'axios';
export const QUERY_KEY = 'NamedConfiguration';
-interface NamedConfiguration {
+export interface NamedConfiguration {
[key: string]: unknown;
}
-export const fetchNamedConfiguration = async (api: Api, key: string, options?: AxiosRequestConfig) => {
+const fetchNamedConfiguration = async (api: Api, key: string, options?: AxiosRequestConfig) => {
const response = await getConfigurationApi(api).getNamedConfiguration({ key }, options);
return response.data as unknown as NamedConfiguration;
@@ -20,7 +20,7 @@ export const useNamedConfiguration = (key: string) => {
const { api } = useApi();
return useQuery({
- queryKey: [ QUERY_KEY ],
+ queryKey: [ QUERY_KEY, key ],
queryFn: ({ signal }) => fetchNamedConfiguration(api!, key, { signal }),
enabled: !!api
});