Skip to content

Commit

Permalink
Migrate resume+streaming to React
Browse files Browse the repository at this point in the history
  • Loading branch information
viown committed Feb 6, 2025
1 parent 9124468 commit 88567f3
Show file tree
Hide file tree
Showing 11 changed files with 266 additions and 161 deletions.
2 changes: 2 additions & 0 deletions src/apps/dashboard/routes/_asyncRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'branding', 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 },
Expand Down
14 changes: 0 additions & 14 deletions src/apps/dashboard/routes/_legacyRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/metadatanfo',
view: 'dashboard/metadatanfo.html'
}
}, {
path: 'playback/resume',
pageProps: {
appType: AppType.Dashboard,
controller: 'dashboard/playback',
view: 'dashboard/playback.html'
}
}, {
path: 'plugins/catalog',
pageProps: {
Expand Down Expand Up @@ -142,12 +135,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
controller: 'dashboard/scheduledtasks/scheduledtasks',
view: 'dashboard/scheduledtasks/scheduledtasks.html'
}
}, {
path: 'playback/streaming',
pageProps: {
appType: AppType.Dashboard,
view: 'dashboard/streaming.html',
controller: 'dashboard/streaming'
}
}
];
13 changes: 3 additions & 10 deletions src/apps/dashboard/routes/branding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,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 React, { useCallback, useEffect, useState } from 'react';
import React, { useCallback, useState } from 'react';
import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom';

import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
Expand All @@ -18,6 +18,7 @@ import ServerConnections from 'components/ServerConnections';
import globalize from 'lib/globalize';
import { queryClient } from 'utils/query/queryClient';
import { ActionData } from 'types/actionData';
import { useIsSubmitting } from 'hooks/useIsSubmitting';

const BRANDING_CONFIG_KEY = 'branding';
const BrandingOption = {
Expand Down Expand Up @@ -61,22 +62,14 @@ export const loader = () => {

export const Component = () => {
const actionData = useActionData() as ActionData | undefined;
const [ isSubmitting, setIsSubmitting ] = useState(false);
const { isSubmitting, onSubmit } = useIsSubmitting(actionData);

const {
data: defaultBrandingOptions,
isPending
} = 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,
Expand Down
11 changes: 4 additions & 7 deletions src/apps/dashboard/routes/logs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useConfiguration } from 'hooks/useConfiguration';
import type { ServerConfiguration } from '@jellyfin/sdk/lib/generated-client/models/server-configuration';
import { ActionData } from 'types/actionData';
import LogItemList from 'apps/dashboard/features/logs/components/LogItemList';
import { useIsSubmitting } from 'hooks/useIsSubmitting';

export const action = async ({ request }: ActionFunctionArgs) => {
const api = ServerConnections.getCurrentApi();
Expand All @@ -44,7 +45,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {

const Logs = () => {
const actionData = useActionData() as ActionData | undefined;
const [ isSubmitting, setIsSubmitting ] = useState(false);
const { isSubmitting, onSubmit } = useIsSubmitting(actionData);

const { isPending: isLogEntriesPending, data: logs } = useServerLogs();
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
Expand Down Expand Up @@ -72,10 +73,6 @@ const Logs = () => {
});
}, [configuration]);

const onSubmit = useCallback(() => {
setIsSubmitting(true);
}, []);

if (isLogEntriesPending || isConfigurationPending || loading || !logs) {
return <Loading />;
}
Expand All @@ -93,7 +90,7 @@ const Logs = () => {
{globalize.translate('TabLogs')}
</Typography>

{isSubmitting && actionData?.isSaved && (
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
Expand All @@ -113,7 +110,7 @@ const Logs = () => {
<TextField
fullWidth
type='number'
name={'SlowResponseTime'}
name='SlowResponseTime'
label={globalize.translate('LabelSlowResponseTime')}
value={configuration?.SlowResponseThresholdMs}
disabled={!configuration?.EnableSlowResponseWarning}
Expand Down
151 changes: 151 additions & 0 deletions src/apps/dashboard/routes/playback/resume.tsx
Original file line number Diff line number Diff line change
@@ -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 } from 'react-router-dom';
import { ActionData } from 'types/actionData';
import { useIsSubmitting } from 'hooks/useIsSubmitting';
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 actionData = useActionData() as ActionData | undefined;
const { isSubmitting, onSubmit } = useIsSubmitting(actionData);

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' onSubmit={onSubmit}>
<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;
90 changes: 90 additions & 0 deletions src/apps/dashboard/routes/playback/streaming.tsx
Original file line number Diff line number Diff line change
@@ -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 } 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';
import { useIsSubmitting } from 'hooks/useIsSubmitting';

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 actionData = useActionData() as ActionData | undefined;
const { isSubmitting, onSubmit } = useIsSubmitting(actionData);

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' onSubmit={onSubmit}>
<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;
Loading

0 comments on commit 88567f3

Please sign in to comment.