diff --git a/src/apps/dashboard/controllers/playback.html b/src/apps/dashboard/controllers/playback.html deleted file mode 100644 index 87438cf86ed..00000000000 --- a/src/apps/dashboard/controllers/playback.html +++ /dev/null @@ -1,42 +0,0 @@ -
-
-
-
-
-

${ButtonResume}

-
-
- -
- ${LabelMinResumePercentageHelp} -
-
-
- -
- ${LabelMaxResumePercentageHelp} -
-
-
- -
- ${LabelMinAudiobookResumeHelp} -
-
-
- -
- ${LabelMaxAudiobookResumeHelp} -
-
-
- -
- ${LabelMinResumeDurationHelp} -
-
-
-
-
-
-
diff --git a/src/apps/dashboard/controllers/playback.js b/src/apps/dashboard/controllers/playback.js deleted file mode 100644 index a21eb80c951..00000000000 --- a/src/apps/dashboard/controllers/playback.js +++ /dev/null @@ -1,39 +0,0 @@ -import 'jquery'; - -import loading from 'components/loading/loading'; -import Dashboard from 'utils/dashboard'; - -function loadPage(page, config) { - page.querySelector('#txtMinResumePct').value = config.MinResumePct; - page.querySelector('#txtMaxResumePct').value = config.MaxResumePct; - page.querySelector('#txtMinAudiobookResume').value = config.MinAudiobookResume; - page.querySelector('#txtMaxAudiobookResume').value = config.MaxAudiobookResume; - page.querySelector('#txtMinResumeDuration').value = config.MinResumeDurationSeconds; - loading.hide(); -} - -function onSubmit() { - loading.show(); - const form = this; - ApiClient.getServerConfiguration().then(function (config) { - config.MinResumePct = form.querySelector('#txtMinResumePct').value; - config.MaxResumePct = form.querySelector('#txtMaxResumePct').value; - config.MinAudiobookResume = form.querySelector('#txtMinAudiobookResume').value; - config.MaxAudiobookResume = form.querySelector('#txtMaxAudiobookResume').value; - config.MinResumeDurationSeconds = form.querySelector('#txtMinResumeDuration').value; - - ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult); - }); - - return false; -} - -$(document).on('pageinit', '#playbackConfigurationPage', function () { - $('.playbackConfigurationForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#playbackConfigurationPage', function () { - loading.show(); - const page = this; - ApiClient.getServerConfiguration().then(function (config) { - loadPage(page, config); - }); -}); diff --git a/src/apps/dashboard/controllers/streaming.html b/src/apps/dashboard/controllers/streaming.html deleted file mode 100644 index 54cb895623a..00000000000 --- a/src/apps/dashboard/controllers/streaming.html +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-
-
-
-

${TabStreaming}

-
-
- -
${LabelRemoteClientBitrateLimitHelp}
-
-
- -
-
-
-
diff --git a/src/apps/dashboard/controllers/streaming.js b/src/apps/dashboard/controllers/streaming.js deleted file mode 100644 index 3f87c292d6c..00000000000 --- a/src/apps/dashboard/controllers/streaming.js +++ /dev/null @@ -1,31 +0,0 @@ -import 'jquery'; - -import loading from 'components/loading/loading'; -import Dashboard from 'utils/dashboard'; - -function loadPage(page, config) { - page.querySelector('#txtRemoteClientBitrateLimit').value = config.RemoteClientBitrateLimit / 1e6 || ''; - loading.hide(); -} - -function onSubmit() { - loading.show(); - const form = this; - ApiClient.getServerConfiguration().then(function (config) { - config.RemoteClientBitrateLimit = parseInt(1e6 * parseFloat(form.querySelector('#txtRemoteClientBitrateLimit').value || '0'), 10); - ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult); - }); - - return false; -} - -$(document).on('pageinit', '#streamingSettingsPage', function () { - $('.streamingSettingsForm').off('submit', onSubmit).on('submit', onSubmit); -}).on('pageshow', '#streamingSettingsPage', function () { - loading.show(); - const page = this; - ApiClient.getServerConfiguration().then(function (config) { - loadPage(page, config); - }); -}); - diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 8c65b380609..cce1471000f 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -7,6 +7,8 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'devices', type: AppType.Dashboard }, { path: 'keys', type: AppType.Dashboard }, { path: 'logs', type: AppType.Dashboard }, + { path: 'playback/resume', type: AppType.Dashboard }, + { path: 'playback/streaming', type: AppType.Dashboard }, { path: 'playback/trickplay', type: AppType.Dashboard }, { path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard }, { path: 'users', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index a20083e3d2d..19a3bedc79b 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -58,13 +58,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'metadatanfo', view: 'metadatanfo.html' } - }, { - path: 'playback/resume', - pageProps: { - appType: AppType.Dashboard, - controller: 'playback', - view: 'playback.html' - } }, { path: 'plugins/catalog', pageProps: { @@ -128,12 +121,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'scheduledtasks/scheduledtasks', view: 'scheduledtasks/scheduledtasks.html' } - }, { - path: 'playback/streaming', - pageProps: { - appType: AppType.Dashboard, - view: 'streaming.html', - controller: 'streaming' - } } ]; diff --git a/src/apps/dashboard/routes/branding/index.tsx b/src/apps/dashboard/routes/branding/index.tsx index 7859610a5ca..a62cd929ba9 100644 --- a/src/apps/dashboard/routes/branding/index.tsx +++ b/src/apps/dashboard/routes/branding/index.tsx @@ -8,8 +8,8 @@ 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 React, { useCallback, useEffect, useState } from 'react'; -import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom'; +import React, { useCallback, useState } from 'react'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions'; import Loading from 'components/loading/LoadingComponent'; @@ -60,8 +60,9 @@ export const loader = () => { }; export const Component = () => { + const navigation = useNavigation(); const actionData = useActionData() as ActionData | undefined; - const [ isSubmitting, setIsSubmitting ] = useState(false); + const isSubmitting = navigation.state === 'submitting'; const { data: defaultBrandingOptions, @@ -69,14 +70,6 @@ export const Component = () => { } = useBrandingOptions(); const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {}); - useEffect(() => { - setIsSubmitting(false); - }, [ actionData ]); - - const onSubmit = useCallback(() => { - setIsSubmitting(true); - }, []); - const setSplashscreenEnabled = useCallback((_: React.ChangeEvent, isEnabled: boolean) => { setBrandingOptions({ ...brandingOptions, @@ -98,13 +91,11 @@ export const Component = () => { return ( -
+ {globalize.translate('HeaderBranding')} diff --git a/src/apps/dashboard/routes/logs/index.tsx b/src/apps/dashboard/routes/logs/index.tsx index bafab8f763c..1cb085c914e 100644 --- a/src/apps/dashboard/routes/logs/index.tsx +++ b/src/apps/dashboard/routes/logs/index.tsx @@ -11,7 +11,7 @@ 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 { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; import ServerConnections from 'components/ServerConnections'; import { useServerLogs } from 'apps/dashboard/features/logs/api/useServerLogs'; import { useConfiguration } from 'hooks/useConfiguration'; @@ -42,9 +42,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { }; }; -const Logs = () => { +export const Component = () => { + const navigation = useNavigation(); const actionData = useActionData() as ActionData | undefined; - const [ isSubmitting, setIsSubmitting ] = useState(false); + const isSubmitting = navigation.state === 'submitting'; const { isPending: isLogEntriesPending, data: logs } = useServerLogs(); const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration(); @@ -72,10 +73,6 @@ const Logs = () => { }); }, [configuration]); - const onSubmit = useCallback(() => { - setIsSubmitting(true); - }, []); - if (isLogEntriesPending || isConfigurationPending || loading || !logs) { return ; } @@ -87,13 +84,13 @@ const Logs = () => { className='mainAnimatedPage type-interior' > - + {globalize.translate('TabLogs')} - {isSubmitting && actionData?.isSaved && ( + {!isSubmitting && actionData?.isSaved && ( {globalize.translate('SettingsSaved')} @@ -113,7 +110,7 @@ const Logs = () => { { ); }; -export default Logs; +Component.displayName = 'LogsPage'; diff --git a/src/apps/dashboard/routes/playback/resume.tsx b/src/apps/dashboard/routes/playback/resume.tsx new file mode 100644 index 00000000000..ab0da25b7c4 --- /dev/null +++ b/src/apps/dashboard/routes/playback/resume.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import Page from 'components/Page'; +import globalize from 'lib/globalize'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; +import { ActionData } from 'types/actionData'; +import { useConfiguration } from 'hooks/useConfiguration'; +import Loading from 'components/loading/LoadingComponent'; +import ServerConnections from 'components/ServerConnections'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; + +export const action = async ({ request }: ActionFunctionArgs) => { + const api = ServerConnections.getCurrentApi(); + if (!api) throw new Error('No Api instance available'); + + const { data: config } = await getConfigurationApi(api).getConfiguration(); + const formData = await request.formData(); + + const minResumePercentage = formData.get('MinResumePercentage')?.toString(); + const maxResumePercentage = formData.get('MaxResumePercentage')?.toString(); + const minAudiobookResume = formData.get('MinAudiobookResume')?.toString(); + const maxAudiobookResume = formData.get('MaxAudiobookResume')?.toString(); + const minResumeDuration = formData.get('MinResumeDuration')?.toString(); + + if (minResumePercentage) config.MinResumePct = parseInt(minResumePercentage, 10); + if (maxResumePercentage) config.MaxResumePct = parseInt(maxResumePercentage, 10); + if (minAudiobookResume) config.MinAudiobookResume = parseInt(minAudiobookResume, 10); + if (maxAudiobookResume) config.MaxAudiobookResume = parseInt(maxAudiobookResume, 10); + if (minResumeDuration) config.MinResumeDurationSeconds = parseInt(minResumeDuration, 10); + + await getConfigurationApi(api) + .updateConfiguration({ serverConfiguration: config }); + + return { + isSaved: true + }; +}; + +export const Component = () => { + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const isSubmitting = navigation.state === 'submitting'; + + const { isPending: isConfigurationPending, data: config } = useConfiguration(); + + if (isConfigurationPending) { + return ; + } + + return ( + + + + + + {globalize.translate('ButtonResume')} + + + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + + + + + + + + + + + + + + + + + ); +}; + +Component.displayName = 'ResumePage'; diff --git a/src/apps/dashboard/routes/playback/streaming.tsx b/src/apps/dashboard/routes/playback/streaming.tsx new file mode 100644 index 00000000000..522b266b84a --- /dev/null +++ b/src/apps/dashboard/routes/playback/streaming.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import Page from 'components/Page'; +import globalize from 'lib/globalize'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; +import ServerConnections from 'components/ServerConnections'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import { useConfiguration } from 'hooks/useConfiguration'; +import Loading from 'components/loading/LoadingComponent'; +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 { data: config } = await getConfigurationApi(api).getConfiguration(); + const formData = await request.formData(); + + const bitrateLimit = formData.get('StreamingBitrateLimit')?.toString(); + config.RemoteClientBitrateLimit = Math.trunc(1e6 * parseFloat(bitrateLimit || '0')); + + await getConfigurationApi(api) + .updateConfiguration({ serverConfiguration: config }); + + return { + isSaved: true + }; +}; + +export const Component = () => { + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const isSubmitting = navigation.state === 'submitting'; + + const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration(); + + if (isConfigurationPending) { + return ; + } + + return ( + + +
+ + + {globalize.translate('TabStreaming')} + + + {!isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + + + + +
+
+
+ ); +}; + +Component.displayName = 'StreamingPage';