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

@ -1,42 +0,0 @@
<div id="playbackConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${ButtonResume}">
<div>
<div class="content-primary">
<form class="playbackConfigurationForm">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${ButtonResume}</h2>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtMinResumePct" name="txtMinResumePct" pattern="[0-9]*" required min="0" max="100" label="${LabelMinResumePercentage}"></input>
<div class="fieldDescription">
${LabelMinResumePercentageHelp}
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtMaxResumePct" name="txtMaxResumePct" pattern="[0-9]*" required min="1" max="100" label="${LabelMaxResumePercentage}"></input>
<div class="fieldDescription">
${LabelMaxResumePercentageHelp}
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtMinAudiobookResume" name="txtMinAudiobookResume" pattern="[0-9]*" required min="0" max="100" label="${LabelMinAudiobookResume}"></input>
<div class="fieldDescription">
${LabelMinAudiobookResumeHelp}
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtMaxAudiobookResume" name="txtMaxAudiobookResume" pattern="[0-9]*" required min="1" max="100" label="${LabelMaxAudiobookResume}"></input>
<div class="fieldDescription">
${LabelMaxAudiobookResumeHelp}
</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtMinResumeDuration" name="txtMinResumeDuration" pattern="[0-9]*" required min="0" label="${LabelMinResumeDuration}"></input>
<div class="fieldDescription">
${LabelMinResumeDurationHelp}
</div>
</div>
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
</form>
</div>
</div>
</div>

View file

@ -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);
});
});

View file

@ -1,20 +0,0 @@
<div id="streamingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TabStreaming}">
<div>
<div class="content-primary">
<form class="streamingSettingsForm">
<div class="verticalSection">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${TabStreaming}</h2>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtRemoteClientBitrateLimit" inputmode="decimal" pattern="[0-9]*(\.[0-9]+)?" min="0" step=".25" label="${LabelRemoteClientBitrateLimit}" />
<div class="fieldDescription">${LabelRemoteClientBitrateLimitHelp}</div>
</div>
</div>
<button is="emby-button" type="submit" class="raised button-submit block">
<span>${Save}</span>
</button>
</form>
</div>
</div>
</div>

View file

@ -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);
});
});

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;