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:
parent
ceb4f8c786
commit
4fb82c91a9
10 changed files with 255 additions and 170 deletions
|
@ -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>
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -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>
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'devices', type: AppType.Dashboard },
|
{ path: 'devices', type: AppType.Dashboard },
|
||||||
{ path: 'keys', type: AppType.Dashboard },
|
{ path: 'keys', type: AppType.Dashboard },
|
||||||
{ path: 'logs', 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: 'playback/trickplay', type: AppType.Dashboard },
|
||||||
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
{ path: 'plugins/:pluginId', page: 'plugins/plugin', type: AppType.Dashboard },
|
||||||
{ path: 'users', type: AppType.Dashboard },
|
{ path: 'users', type: AppType.Dashboard },
|
||||||
|
|
|
@ -58,13 +58,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'metadatanfo',
|
controller: 'metadatanfo',
|
||||||
view: 'metadatanfo.html'
|
view: 'metadatanfo.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'playback/resume',
|
|
||||||
pageProps: {
|
|
||||||
appType: AppType.Dashboard,
|
|
||||||
controller: 'playback',
|
|
||||||
view: 'playback.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'plugins/catalog',
|
path: 'plugins/catalog',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
@ -128,12 +121,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'scheduledtasks/scheduledtasks',
|
controller: 'scheduledtasks/scheduledtasks',
|
||||||
view: 'scheduledtasks/scheduledtasks.html'
|
view: 'scheduledtasks/scheduledtasks.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'playback/streaming',
|
|
||||||
pageProps: {
|
|
||||||
appType: AppType.Dashboard,
|
|
||||||
view: 'streaming.html',
|
|
||||||
controller: 'streaming'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
|
@ -8,8 +8,8 @@ import Stack from '@mui/material/Stack';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
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 { type ActionFunctionArgs, Form, useActionData, useNavigation } from 'react-router-dom';
|
||||||
|
|
||||||
import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
|
import { getBrandingOptionsQuery, QUERY_KEY, useBrandingOptions } from 'apps/dashboard/features/branding/api/useBrandingOptions';
|
||||||
import Loading from 'components/loading/LoadingComponent';
|
import Loading from 'components/loading/LoadingComponent';
|
||||||
|
@ -60,8 +60,9 @@ export const loader = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
const actionData = useActionData() as ActionData | undefined;
|
const actionData = useActionData() as ActionData | undefined;
|
||||||
const [ isSubmitting, setIsSubmitting ] = useState(false);
|
const isSubmitting = navigation.state === 'submitting';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: defaultBrandingOptions,
|
data: defaultBrandingOptions,
|
||||||
|
@ -69,14 +70,6 @@ export const Component = () => {
|
||||||
} = useBrandingOptions();
|
} = useBrandingOptions();
|
||||||
const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {});
|
const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}, [ actionData ]);
|
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const setSplashscreenEnabled = useCallback((_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => {
|
const setSplashscreenEnabled = useCallback((_: React.ChangeEvent<HTMLInputElement>, isEnabled: boolean) => {
|
||||||
setBrandingOptions({
|
setBrandingOptions({
|
||||||
...brandingOptions,
|
...brandingOptions,
|
||||||
|
@ -98,13 +91,11 @@ export const Component = () => {
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
id='brandingPage'
|
id='brandingPage'
|
||||||
|
title={globalize.translate('HeaderBranding')}
|
||||||
className='mainAnimatedPage type-interior'
|
className='mainAnimatedPage type-interior'
|
||||||
>
|
>
|
||||||
<Box className='content-primary'>
|
<Box className='content-primary'>
|
||||||
<Form
|
<Form method='POST'>
|
||||||
method='POST'
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
>
|
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Typography variant='h1'>
|
<Typography variant='h1'>
|
||||||
{globalize.translate('HeaderBranding')}
|
{globalize.translate('HeaderBranding')}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import Stack from '@mui/material/Stack';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Typography from '@mui/material/Typography';
|
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 ServerConnections from 'components/ServerConnections';
|
||||||
import { useServerLogs } from 'apps/dashboard/features/logs/api/useServerLogs';
|
import { useServerLogs } from 'apps/dashboard/features/logs/api/useServerLogs';
|
||||||
import { useConfiguration } from 'hooks/useConfiguration';
|
import { useConfiguration } from 'hooks/useConfiguration';
|
||||||
|
@ -43,8 +43,9 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const Logs = () => {
|
const Logs = () => {
|
||||||
|
const navigation = useNavigation();
|
||||||
const actionData = useActionData() as ActionData | undefined;
|
const actionData = useActionData() as ActionData | undefined;
|
||||||
const [ isSubmitting, setIsSubmitting ] = useState(false);
|
const isSubmitting = navigation.state === 'submitting';
|
||||||
|
|
||||||
const { isPending: isLogEntriesPending, data: logs } = useServerLogs();
|
const { isPending: isLogEntriesPending, data: logs } = useServerLogs();
|
||||||
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
|
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
|
||||||
|
@ -72,10 +73,6 @@ const Logs = () => {
|
||||||
});
|
});
|
||||||
}, [configuration]);
|
}, [configuration]);
|
||||||
|
|
||||||
const onSubmit = useCallback(() => {
|
|
||||||
setIsSubmitting(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLogEntriesPending || isConfigurationPending || loading || !logs) {
|
if (isLogEntriesPending || isConfigurationPending || loading || !logs) {
|
||||||
return <Loading />;
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
@ -87,13 +84,13 @@ const Logs = () => {
|
||||||
className='mainAnimatedPage type-interior'
|
className='mainAnimatedPage type-interior'
|
||||||
>
|
>
|
||||||
<Box className='content-primary'>
|
<Box className='content-primary'>
|
||||||
<Form method='POST' onSubmit={onSubmit}>
|
<Form method='POST'>
|
||||||
<Stack spacing={3}>
|
<Stack spacing={3}>
|
||||||
<Typography variant='h1'>
|
<Typography variant='h1'>
|
||||||
{globalize.translate('TabLogs')}
|
{globalize.translate('TabLogs')}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
||||||
{isSubmitting && actionData?.isSaved && (
|
{!isSubmitting && actionData?.isSaved && (
|
||||||
<Alert severity='success'>
|
<Alert severity='success'>
|
||||||
{globalize.translate('SettingsSaved')}
|
{globalize.translate('SettingsSaved')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -113,7 +110,7 @@ const Logs = () => {
|
||||||
<TextField
|
<TextField
|
||||||
fullWidth
|
fullWidth
|
||||||
type='number'
|
type='number'
|
||||||
name={'SlowResponseTime'}
|
name='SlowResponseTime'
|
||||||
label={globalize.translate('LabelSlowResponseTime')}
|
label={globalize.translate('LabelSlowResponseTime')}
|
||||||
value={configuration?.SlowResponseThresholdMs}
|
value={configuration?.SlowResponseThresholdMs}
|
||||||
disabled={!configuration?.EnableSlowResponseWarning}
|
disabled={!configuration?.EnableSlowResponseWarning}
|
||||||
|
|
151
src/apps/dashboard/routes/playback/resume.tsx
Normal file
151
src/apps/dashboard/routes/playback/resume.tsx
Normal 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;
|
90
src/apps/dashboard/routes/playback/streaming.tsx
Normal file
90
src/apps/dashboard/routes/playback/streaming.tsx
Normal 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;
|
Loading…
Add table
Add a link
Reference in a new issue