diff --git a/src/apps/dashboard/routes/playback/resume.tsx b/src/apps/dashboard/routes/playback/resume.tsx index ab0da25b7c..3009825cfd 100644 --- a/src/apps/dashboard/routes/playback/resume.tsx +++ b/src/apps/dashboard/routes/playback/resume.tsx @@ -9,10 +9,11 @@ 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 { QUERY_KEY, 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'; +import { queryClient } from 'utils/query/queryClient'; export const action = async ({ request }: ActionFunctionArgs) => { const api = ServerConnections.getCurrentApi(); @@ -36,6 +37,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { await getConfigurationApi(api) .updateConfiguration({ serverConfiguration: config }); + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY ] + }); + return { isSaved: true }; diff --git a/src/apps/dashboard/routes/playback/streaming.tsx b/src/apps/dashboard/routes/playback/streaming.tsx index 522b266b84..e329277303 100644 --- a/src/apps/dashboard/routes/playback/streaming.tsx +++ b/src/apps/dashboard/routes/playback/streaming.tsx @@ -10,9 +10,10 @@ 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 { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; import Loading from 'components/loading/LoadingComponent'; import { ActionData } from 'types/actionData'; +import { queryClient } from 'utils/query/queryClient'; export const action = async ({ request }: ActionFunctionArgs) => { const api = ServerConnections.getCurrentApi(); @@ -27,6 +28,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { await getConfigurationApi(api) .updateConfiguration({ serverConfiguration: config }); + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY ] + }); + return { isSaved: true }; diff --git a/src/apps/dashboard/routes/playback/trickplay.tsx b/src/apps/dashboard/routes/playback/trickplay.tsx index 442111145d..81ef076961 100644 --- a/src/apps/dashboard/routes/playback/trickplay.tsx +++ b/src/apps/dashboard/routes/playback/trickplay.tsx @@ -1,325 +1,259 @@ -import type { ServerConfiguration } from '@jellyfin/sdk/lib/generated-client/models/server-configuration'; +import React from 'react'; + +import globalize from 'lib/globalize'; +import { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom'; +import { QUERY_KEY, useConfiguration } from 'hooks/useConfiguration'; +import Page from 'components/Page'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import Switch from '@mui/material/Switch'; +import Loading from 'components/loading/LoadingComponent'; +import FormHelperText from '@mui/material/FormHelperText'; +import MenuItem from '@mui/material/MenuItem'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Alert from '@mui/material/Alert'; +import ServerConnections from 'components/ServerConnections'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; import { TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client/models/trickplay-scan-behavior'; import { ProcessPriorityClass } from '@jellyfin/sdk/lib/generated-client/models/process-priority-class'; -import React, { type FC, useCallback, useEffect, useRef } from 'react'; +import { ActionData } from 'types/actionData'; +import { queryClient } from 'utils/query/queryClient'; -import globalize from '../../../../lib/globalize'; -import Page from '../../../../components/Page'; -import SectionTitleContainer from '../../../../elements/SectionTitleContainer'; -import ButtonElement from '../../../../elements/ButtonElement'; -import CheckBoxElement from '../../../../elements/CheckBoxElement'; -import SelectElement from '../../../../elements/SelectElement'; -import InputElement from '../../../../elements/InputElement'; -import loading from '../../../../components/loading/loading'; -import toast from '../../../../components/toast/toast'; -import ServerConnections from '../../../../components/ServerConnections'; +export const action = async ({ request }: ActionFunctionArgs) => { + const api = ServerConnections.getCurrentApi(); + if (!api) throw new Error('No Api instance available'); -function onSaveComplete() { - loading.hide(); - toast(globalize.translate('SettingsSaved')); -} + const formData = await request.formData(); + const data = Object.fromEntries(formData); -const PlaybackTrickplay: FC = () => { - const element = useRef(null); + const { data: config } = await getConfigurationApi(api).getConfiguration(); - const loadConfig = useCallback((config: ServerConfiguration) => { - const page = element.current; - const options = config.TrickplayOptions; + const options = config.TrickplayOptions; + if (!options) throw new Error('Unexpected null TrickplayOptions'); - if (!page) { - console.error('Unexpected null reference'); - return; - } + options.EnableHwAcceleration = data.HwAcceleration?.toString() === 'on'; + options.EnableHwEncoding = data.HwEncoding?.toString() === 'on'; + options.EnableKeyFrameOnlyExtraction = data.KeyFrameOnlyExtraction?.toString() === 'on'; + options.ScanBehavior = data.ScanBehavior.toString() as TrickplayScanBehavior; + options.ProcessPriority = data.ProcessPriority.toString() as ProcessPriorityClass; + options.Interval = parseInt(data.ImageInterval.toString() || '10000', 10); + options.WidthResolutions = data.WidthResolutions.toString().replace(' ', '').split(',').map(Number); + options.TileWidth = parseInt(data.TileWidth.toString() || '10', 10); + options.TileHeight = parseInt(data.TileHeight.toString() || '10', 10); + options.Qscale = parseInt(data.Qscale.toString() || '4', 10); + options.JpegQuality = parseInt(data.JpegQuality.toString() || '90', 10); + options.ProcessThreads = parseInt(data.TrickplayThreads.toString() || '1', 10); - (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options?.EnableHwAcceleration || false; - (page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked = options?.EnableHwEncoding || false; - (page.querySelector('.chkEnableKeyFrameOnlyExtraction') as HTMLInputElement).checked = options?.EnableKeyFrameOnlyExtraction || false; - (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = (options?.ScanBehavior || TrickplayScanBehavior.NonBlocking); - (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = (options?.ProcessPriority || ProcessPriorityClass.Normal); - (page.querySelector('#txtInterval') as HTMLInputElement).value = options?.Interval?.toString() || '10000'; - (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options?.WidthResolutions?.join(',') || ''; - (page.querySelector('#txtTileWidth') as HTMLInputElement).value = options?.TileWidth?.toString() || '10'; - (page.querySelector('#txtTileHeight') as HTMLInputElement).value = options?.TileHeight?.toString() || '10'; - (page.querySelector('#txtQscale') as HTMLInputElement).value = options?.Qscale?.toString() || '4'; - (page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options?.JpegQuality?.toString() || '90'; - (page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options?.ProcessThreads?.toString() || '1'; + await getConfigurationApi(api) + .updateConfiguration({ serverConfiguration: config }); - loading.hide(); - }, []); + void queryClient.invalidateQueries({ + queryKey: [ QUERY_KEY ] + }); - const loadData = useCallback(() => { - loading.show(); - - ServerConnections.currentApiClient()?.getServerConfiguration().then(function (config) { - loadConfig(config); - }).catch(err => { - console.error('[PlaybackTrickplay] failed to fetch server config', err); - }); - }, [loadConfig]); - - useEffect(() => { - const page = element.current; - - if (!page) { - console.error('Unexpected null reference'); - return; - } - - const saveConfig = (config: ServerConfiguration) => { - const apiClient = ServerConnections.currentApiClient(); - - if (!apiClient) { - console.error('[PlaybackTrickplay] No current apiclient instance'); - return; - } - - if (!config.TrickplayOptions) { - throw new Error('Unexpected null TrickplayOptions'); - } - - const options = config.TrickplayOptions; - options.EnableHwAcceleration = (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked; - options.EnableHwEncoding = (page.querySelector('.chkEnableHwEncoding') as HTMLInputElement).checked; - options.EnableKeyFrameOnlyExtraction = (page.querySelector('.chkEnableKeyFrameOnlyExtraction') as HTMLInputElement).checked; - options.ScanBehavior = (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value as TrickplayScanBehavior; - options.ProcessPriority = (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value as ProcessPriorityClass; - options.Interval = Math.max(1, parseInt((page.querySelector('#txtInterval') as HTMLInputElement).value || '10000', 10)); - options.WidthResolutions = (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value.replace(' ', '').split(',').map(Number); - options.TileWidth = Math.max(1, parseInt((page.querySelector('#txtTileWidth') as HTMLInputElement).value || '10', 10)); - options.TileHeight = Math.max(1, parseInt((page.querySelector('#txtTileHeight') as HTMLInputElement).value || '10', 10)); - options.Qscale = Math.min(31, parseInt((page.querySelector('#txtQscale') as HTMLInputElement).value || '4', 10)); - options.JpegQuality = Math.min(100, parseInt((page.querySelector('#txtJpegQuality') as HTMLInputElement).value || '90', 10)); - options.ProcessThreads = parseInt((page.querySelector('#txtProcessThreads') as HTMLInputElement).value || '1', 10); - - apiClient.updateServerConfiguration(config).then(() => { - onSaveComplete(); - }).catch(err => { - console.error('[PlaybackTrickplay] failed to update config', err); - }); - }; - - const onSubmit = (e: Event) => { - const apiClient = ServerConnections.currentApiClient(); - - if (!apiClient) { - console.error('[PlaybackTrickplay] No current apiclient instance'); - return; - } - - loading.show(); - apiClient.getServerConfiguration().then(function (config) { - saveConfig(config); - }).catch(err => { - console.error('[PlaybackTrickplay] failed to fetch server config', err); - }); - - e.preventDefault(); - e.stopPropagation(); - return false; - }; - - (page.querySelector('.trickplayConfigurationForm') as HTMLFormElement).addEventListener('submit', onSubmit); - - loadData(); - }, [loadData]); - - const optionScanBehavior = () => { - let content = ''; - content += ``; - content += ``; - return content; + return { + isSaved: true }; +}; - const optionProcessPriority = () => { - let content = ''; - content += ``; - content += ``; - content += ``; - content += ``; - content += ``; - return content; - }; +export const Component = () => { + const navigation = useNavigation(); + const actionData = useActionData() as ActionData | undefined; + const { data: defaultConfig, isPending } = useConfiguration(); + const isSubmitting = navigation.state === 'submitting'; + + if (!defaultConfig || isPending) { + return ; + } return ( -
-
- -
+ +
+ + + {globalize.translate('Trickplay')} + - -
- + {globalize.translate('SettingsSaved')} + + )} + + + + } + label={globalize.translate('LabelTrickplayAccel')} + /> + + + + + } + label={globalize.translate('LabelTrickplayAccelEncoding')} + /> + {globalize.translate('LabelTrickplayAccelEncodingHelp')} + + + + + } + label={globalize.translate('LabelTrickplayKeyFrameOnlyExtraction')} + /> + {globalize.translate('LabelTrickplayKeyFrameOnlyExtractionHelp')} + + + + {globalize.translate('NonBlockingScan')} + {globalize.translate('BlockingScan')} + + + + {globalize.translate('PriorityHigh')} + {globalize.translate('PriorityAboveNormal')} + {globalize.translate('PriorityNormal')} + {globalize.translate('PriorityBelowNormal')} + {globalize.translate('PriorityIdle')} + + + -
-
- -
-
- {globalize.translate('LabelTrickplayAccelEncodingHelp')} -
-
-
-
- -
-
- {globalize.translate('LabelTrickplayKeyFrameOnlyExtractionHelp')} -
-
-
-
-
- - {optionScanBehavior()} - -
- {globalize.translate('LabelScanBehaviorHelp')} -
-
-
+ -
-
- - {optionProcessPriority()} - -
- {globalize.translate('LabelProcessPriorityHelp')} -
-
-
+ -
-
- -
- {globalize.translate('LabelImageIntervalHelp')} -
-
-
+ -
-
- -
- {globalize.translate('LabelWidthResolutionsHelp')} -
-
-
+ -
-
- -
- {globalize.translate('LabelTileWidthHelp')} -
-
-
- -
-
- -
- {globalize.translate('LabelTileHeightHelp')} -
-
-
- -
-
- -
- {globalize.translate('LabelJpegQualityHelp')} -
-
-
- -
-
- -
- {globalize.translate('LabelQscaleHelp')} -
-
-
- -
-
- -
- {globalize.translate('LabelTrickplayThreadsHelp')} -
-
-
- -
- -
- -
+ size='large' + > + {globalize.translate('Save')} + + + +
); }; -export default PlaybackTrickplay; +Component.displayName = 'TrickplayPage'; diff --git a/src/hooks/useConfiguration.ts b/src/hooks/useConfiguration.ts index 2d5dbd5a2a..f1b9d47c3d 100644 --- a/src/hooks/useConfiguration.ts +++ b/src/hooks/useConfiguration.ts @@ -4,6 +4,8 @@ import { useQuery } from '@tanstack/react-query'; import { useApi } from 'hooks/useApi'; 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'); @@ -19,7 +21,7 @@ export const useConfiguration = () => { const { api } = useApi(); return useQuery({ - queryKey: ['Configuration'], + queryKey: [QUERY_KEY], queryFn: ({ signal }) => fetchConfiguration(api, { signal }), enabled: !!api });