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 @@ -
-
-
-
-
- -
${LabelDateAddedBehaviorHelp}
-
- -
- -
${OptionDisplayFolderViewHelp}
-
- - - -
- -
${LabelGroupMoviesIntoCollectionsHelp}
-
- -
- -
${OptionEnableExternalContentInSuggestionsHelp}
-
- -
- -
${OptionSaveMetadataAsHiddenHelp}
-
- -
- -
-
-
-
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 ( + + +
+ + {isConfigError || isNamedConfigError ? ( + {globalize.translate('DisplayLoadError')} + ) : ( + <> + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + {globalize.translate('Display')} + + {globalize.translate('OptionDateAddedImportTime')} + {globalize.translate('OptionDateAddedFileTime')} + + + + + } + label={globalize.translate('OptionDisplayFolderView')} + /> + {globalize.translate('OptionDisplayFolderViewHelp')} + + + + + } + label={globalize.translate('LabelDisplaySpecialsWithinSeasons')} + /> + + + + + } + label={globalize.translate('LabelGroupMoviesIntoCollections')} + /> + {globalize.translate('LabelGroupMoviesIntoCollectionsHelp')} + + + + + } + label={globalize.translate('OptionEnableExternalContentInSuggestions')} + /> + {globalize.translate('OptionEnableExternalContentInSuggestionsHelp')} + + + + + )} + +
+
+
+ ); +}; + +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' > -
- - {isConfigError || isNamedConfigError ? ( - {globalize.translate('DisplayLoadError')} - ) : ( - <> - {!isSubmitting && actionData?.isSaved && ( - - {globalize.translate('SettingsSaved')} - - )} - {globalize.translate('Display')} - - {globalize.translate('OptionDateAddedImportTime')} - {globalize.translate('OptionDateAddedFileTime')} - + {isConfigError || isNamedConfigError ? ( + {globalize.translate('DisplayLoadError')} + ) : ( + + + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + {globalize.translate('Display')} + + {globalize.translate('OptionDateAddedImportTime')} + {globalize.translate('OptionDateAddedFileTime')} + - - - } - label={globalize.translate('OptionDisplayFolderView')} - /> - {globalize.translate('OptionDisplayFolderViewHelp')} - + + + } + label={globalize.translate('OptionDisplayFolderView')} + /> + {globalize.translate('OptionDisplayFolderViewHelp')} + - - - } - label={globalize.translate('LabelDisplaySpecialsWithinSeasons')} - /> - + + + } + label={globalize.translate('LabelDisplaySpecialsWithinSeasons')} + /> + - - - } - label={globalize.translate('LabelGroupMoviesIntoCollections')} - /> - {globalize.translate('LabelGroupMoviesIntoCollectionsHelp')} - + + + } + label={globalize.translate('LabelGroupMoviesIntoCollections')} + /> + {globalize.translate('LabelGroupMoviesIntoCollectionsHelp')} + - - - } - label={globalize.translate('OptionEnableExternalContentInSuggestions')} - /> - {globalize.translate('OptionEnableExternalContentInSuggestionsHelp')} - + + + } + label={globalize.translate('OptionEnableExternalContentInSuggestions')} + /> + {globalize.translate('OptionEnableExternalContentInSuggestionsHelp')} + - - - )} - - + +
+ + )}
); 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 });