1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Migrate resume+streaming to React

This commit is contained in:
viown 2025-02-06 05:22:40 +03:00
parent ceb4f8c786
commit 4fb82c91a9
10 changed files with 255 additions and 170 deletions

View file

@ -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 },

View file

@ -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'
}
}
];

View file

@ -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<HTMLInputElement>, isEnabled: boolean) => {
setBrandingOptions({
...brandingOptions,
@ -98,13 +91,11 @@ export const Component = () => {
return (
<Page
id='brandingPage'
title={globalize.translate('HeaderBranding')}
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Form
method='POST'
onSubmit={onSubmit}
>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('HeaderBranding')}

View file

@ -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';
@ -43,8 +43,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
};
const Logs = () => {
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 <Loading />;
}
@ -87,13 +84,13 @@ const Logs = () => {
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Form method='POST' onSubmit={onSubmit}>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('TabLogs')}
</Typography>
{isSubmitting && actionData?.isSaved && (
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
@ -113,7 +110,7 @@ const Logs = () => {
<TextField
fullWidth
type='number'
name={'SlowResponseTime'}
name='SlowResponseTime'
label={globalize.translate('LabelSlowResponseTime')}
value={configuration?.SlowResponseThresholdMs}
disabled={!configuration?.EnableSlowResponseWarning}

View file

@ -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
};
};
const Resume = () => {
const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const { isPending: isConfigurationPending, data: config } = useConfiguration();
if (isConfigurationPending) {
return <Loading />;
}
return (
<Page
id='playbackConfigurationPage'
title={globalize.translate('ButtonResume')}
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h2'>
{globalize.translate('ButtonResume')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<TextField
label={globalize.translate('LabelMinResumePercentage')}
name='MinResumePercentage'
type='number'
defaultValue={config?.MinResumePct}
inputProps={{
min: 0,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMinResumePercentageHelp')}
/>
<TextField
label={globalize.translate('LabelMaxResumePercentage')}
name='MaxResumePercentage'
type='number'
defaultValue={config?.MaxResumePct}
inputProps={{
min: 1,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
/>
<TextField
label={globalize.translate('LabelMinAudiobookResume')}
name='MinAudiobookResume'
type='number'
defaultValue={config?.MinAudiobookResume}
inputProps={{
min: 0,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
/>
<TextField
label={globalize.translate('LabelMaxAudiobookResume')}
name='MaxAudiobookResume'
type='number'
defaultValue={config?.MaxAudiobookResume}
inputProps={{
min: 1,
max: 100,
required: true
}}
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
/>
<TextField
label={globalize.translate('LabelMinResumeDuration')}
name='MinResumeDuration'
type='number'
defaultValue={config?.MinResumeDurationSeconds}
inputProps={{
min: 0,
required: true
}}
helperText={globalize.translate('LabelMinResumeDurationHelp')}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
</Form>
</Box>
</Page>
);
};
export default Resume;

View file

@ -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
};
};
const Streaming = () => {
const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
if (isConfigurationPending) {
return <Loading />;
}
return (
<Page
id='streamingSettingsPage'
title={globalize.translate('TabStreaming')}
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h2'>
{globalize.translate('TabStreaming')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<TextField
type='number'
inputMode='decimal'
inputProps={{
min: 0,
step: 0.25
}}
name='StreamingBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
helperText={globalize.translate('LabelRemoteClientBitrateLimitHelp')}
defaultValue={defaultConfiguration?.RemoteClientBitrateLimit ? defaultConfiguration?.RemoteClientBitrateLimit / 1e6 : ''}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
</Form>
</Box>
</Page>
);
};
export default Streaming;