Merge pull request #7223 from viown/use-error-messages

Add error handling to rest of dashboard pages
This commit is contained in:
Bill Thornton
2026-03-16 11:04:01 -04:00
committed by GitHub
10 changed files with 536 additions and 470 deletions
@@ -6,11 +6,14 @@ import { type MRT_RowData, type MRT_TableInstance, type MRT_TableOptions, Materi
import React from 'react';
import Page, { type PageProps } from 'components/Page';
import Alert from '@mui/material/Alert';
interface TablePageProps<T extends MRT_RowData> extends PageProps {
title: string
subtitle?: string
table: MRT_TableInstance<T>
isError?: boolean
errorMessage?: string
}
export const DEFAULT_TABLE_OPTIONS: Partial<MRT_TableOptions<MRT_RowData>> = {
@@ -32,6 +35,8 @@ const TablePage = <T extends MRT_RowData>({
title,
subtitle,
table,
isError,
errorMessage,
children,
...pageProps
}: TablePageProps<T>) => {
@@ -48,22 +53,28 @@ const TablePage = <T extends MRT_RowData>({
height: '100%'
}}
>
<Stack
spacing={2}
sx={{
marginBottom: 1
}}
>
<Typography variant='h1'>
{title}
</Typography>
{subtitle && (
<Typography>
{subtitle}
</Typography>
)}
</Stack>
<MaterialReactTable table={table} />
{isError ? (
<Alert severity='error'>{errorMessage}</Alert>
) : (
<>
<Stack
spacing={2}
sx={{
marginBottom: 1
}}
>
<Typography variant='h1'>
{title}
</Typography>
{subtitle && (
<Typography>
{subtitle}
</Typography>
)}
</Stack>
<MaterialReactTable table={table} />
</>
)}
</Box>
{children}
</Page>
+13 -2
View File
@@ -52,7 +52,12 @@ export const Component = () => {
pageSize: DEFAULT_PAGE_SIZE
});
const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails();
const {
usersById: users,
names: userNames,
isLoading: isUsersLoading,
isError: isUsersError
} = useUsersDetails();
const theme = useTheme();
@@ -64,7 +69,11 @@ export const Component = () => {
hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined
}), [activityView, pagination.pageIndex, pagination.pageSize]);
const { data, isLoading: isLogEntriesLoading } = useLogEntries(activityParams);
const {
data,
isLoading: isLogEntriesLoading,
isError: isLogEntriesError
} = useLogEntries(activityParams);
const logEntries = useMemo(() => (
data?.Items || []
), [ data ]);
@@ -213,6 +222,8 @@ export const Component = () => {
title={globalize.translate('HeaderActivity')}
className='mainAnimatedPage type-interior'
table={table}
isError={isUsersError || isLogEntriesError}
errorMessage={globalize.translate('ActivitiesLoadError')}
/>
);
};
+119 -114
View File
@@ -73,7 +73,8 @@ export const Component = () => {
const {
data: defaultBrandingOptions,
isPending
isPending,
isError
} = useBrandingOptions();
const [ brandingOptions, setBrandingOptions ] = useState(defaultBrandingOptions || {});
@@ -186,129 +187,133 @@ export const Component = () => {
method='POST'
onSubmit={onSubmit}
>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('HeaderBranding')}
</Typography>
{isError ? (
<Alert severity='error'>{globalize.translate('BrandingLoadError')}</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('HeaderBranding')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
{error && (
<Alert severity='error'>
{globalize.translate(error)}
</Alert>
)}
<Stack
direction={{
xs: 'column',
sm: 'row'
}}
spacing={3}
>
<Box sx={{ flex: '1 1 0' }}>
<Image
isLoading={false}
url={
isSplashscreenEnabled ?
splashscreenUrl :
undefined
}
/>
</Box>
{error && (
<Alert severity='error'>
{globalize.translate(error)}
</Alert>
)}
<Stack
spacing={{ xs: 3, sm: 2 }}
sx={{ flex: '1 1 0' }}
direction={{
xs: 'column',
sm: 'row'
}}
spacing={3}
>
<FormControlLabel
control={
<Switch
name={BrandingOption.SplashscreenEnabled}
checked={isSplashscreenEnabled}
onChange={setSplashscreenEnabled}
/>
}
label={globalize.translate('EnableSplashScreen')}
/>
<Typography variant='body2'>
{globalize.translate('CustomSplashScreenSize')}
</Typography>
<Button
component='label'
variant='outlined'
startIcon={<Upload />}
disabled={!isSplashscreenEnabled}
>
<input
type='file'
accept='image/*'
hidden
onChange={onSplashscreenUpload}
<Box sx={{ flex: '1 1 0' }}>
<Image
isLoading={false}
url={
isSplashscreenEnabled ?
splashscreenUrl :
undefined
}
/>
{globalize.translate('UploadCustomImage')}
</Button>
</Box>
<Button
variant='outlined'
color='error'
startIcon={<Delete />}
disabled={!isSplashscreenEnabled}
onClick={onSplashscreenDelete}
<Stack
spacing={{ xs: 3, sm: 2 }}
sx={{ flex: '1 1 0' }}
>
{globalize.translate('DeleteCustomImage')}
</Button>
<FormControlLabel
control={
<Switch
name={BrandingOption.SplashscreenEnabled}
checked={isSplashscreenEnabled}
onChange={setSplashscreenEnabled}
/>
}
label={globalize.translate('EnableSplashScreen')}
/>
<Typography variant='body2'>
{globalize.translate('CustomSplashScreenSize')}
</Typography>
<Button
component='label'
variant='outlined'
startIcon={<Upload />}
disabled={!isSplashscreenEnabled}
>
<input
type='file'
accept='image/*'
hidden
onChange={onSplashscreenUpload}
/>
{globalize.translate('UploadCustomImage')}
</Button>
<Button
variant='outlined'
color='error'
startIcon={<Delete />}
disabled={!isSplashscreenEnabled}
onClick={onSplashscreenDelete}
>
{globalize.translate('DeleteCustomImage')}
</Button>
</Stack>
</Stack>
<TextField
fullWidth
multiline
minRows={5}
maxRows={5}
name={BrandingOption.LoginDisclaimer}
label={globalize.translate('LabelLoginDisclaimer')}
helperText={globalize.translate('LabelLoginDisclaimerHelp')}
value={brandingOptions?.LoginDisclaimer}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<TextField
fullWidth
multiline
minRows={5}
maxRows={20}
name={BrandingOption.CustomCss}
label={globalize.translate('LabelCustomCss')}
helperText={globalize.translate('LabelCustomCssHelp')}
spellCheck={false}
value={brandingOptions?.CustomCss}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
<TextField
fullWidth
multiline
minRows={5}
maxRows={5}
name={BrandingOption.LoginDisclaimer}
label={globalize.translate('LabelLoginDisclaimer')}
helperText={globalize.translate('LabelLoginDisclaimerHelp')}
value={brandingOptions?.LoginDisclaimer}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<TextField
fullWidth
multiline
minRows={5}
maxRows={20}
name={BrandingOption.CustomCss}
label={globalize.translate('LabelCustomCss')}
helperText={globalize.translate('LabelCustomCssHelp')}
spellCheck={false}
value={brandingOptions?.CustomCss}
onChange={setBrandingOption}
slotProps={{
input: {
className: 'textarea-mono'
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
)}
</Form>
</Box>
</Page>
+14 -2
View File
@@ -37,11 +37,21 @@ const getUserCell = (users: UsersRecords) => function UserCell({ renderedCellVal
export const Component = () => {
const { api } = useApi();
const { data, isLoading: isDevicesLoading, isRefetching } = useDevices({});
const {
data,
isLoading: isDevicesLoading,
isError: isDevicesError,
isRefetching
} = useDevices({});
const devices = useMemo(() => (
data?.Items || []
), [ data ]);
const { usersById: users, names: userNames, isLoading: isUsersLoading } = useUsersDetails();
const {
usersById: users,
names: userNames,
isLoading: isUsersLoading,
isError: isUsersError
} = useUsersDetails();
const theme = useTheme();
const [ isDeleteConfirmOpen, setIsDeleteConfirmOpen ] = useState(false);
@@ -257,6 +267,8 @@ export const Component = () => {
title={globalize.translate('HeaderDevices')}
className='mainAnimatedPage type-interior'
table={mrTable}
isError={isDevicesError || isUsersError}
errorMessage={globalize.translate('DevicesLoadError')}
>
<ConfirmDialog
open={isDeleteConfirmOpen}
+3 -1
View File
@@ -23,7 +23,7 @@ export const Component = () => {
const [ isCreateApiKeyPromptOpen, setIsCreateApiKeyPromptOpen ] = useState(false);
const [ isConfirmDeleteOpen, setIsConfirmDeleteOpen ] = useState(false);
const [ apiKeyToDelete, setApiKeyToDelete ] = useState('');
const { data, isLoading } = useApiKeys();
const { data, isLoading, isError } = useApiKeys();
const keys = useMemo(() => (
data?.Items || []
), [ data ]);
@@ -170,6 +170,8 @@ export const Component = () => {
subtitle={globalize.translate('HeaderApiKeysHelp')}
className='mainAnimatedPage type-interior'
table={table}
isError={isError}
errorMessage={globalize.translate('ApiKeysLoadError')}
/>
</>
);
+46 -41
View File
@@ -17,9 +17,10 @@ import getCollectionTypeOptions from 'apps/dashboard/features/libraries/utils/co
import { queryClient } from 'utils/query/queryClient';
import RefreshIcon from '@mui/icons-material/Refresh';
import Add from '@mui/icons-material/Add';
import Alert from '@mui/material/Alert';
export const Component = () => {
const { data: virtualFolders, isPending: isVirtualFoldersPending } = useVirtualFolders();
const { data: virtualFolders, isPending: isVirtualFoldersPending, isError: isVirtualFoldersError } = useVirtualFolders();
const startTask = useStartTask();
const { data: tasks, isPending: isLiveTasksPending } = useLiveTasks({ isHidden: false });
@@ -59,47 +60,51 @@ export const Component = () => {
className='mainAnimatedPage type-interior'
>
<Box className='content-primary'>
<Stack spacing={3} mt={2}>
<Stack direction='row' alignItems={'center'} spacing={1.5}>
<Button
startIcon={<Add />}
onClick={showMediaLibraryCreator}
>
{globalize.translate('ButtonAddMediaLibrary')}
</Button>
<Button
onClick={onScanLibraries}
startIcon={<RefreshIcon />}
loading={librariesTask && librariesTask.State !== TaskState.Idle}
loadingPosition='start'
variant='outlined'
>
{globalize.translate('ButtonScanAllLibraries')}
</Button>
{(librariesTask && librariesTask.State == TaskState.Running) && (
<TaskProgress task={librariesTask} />
)}
</Stack>
{isVirtualFoldersError ? (
<Alert severity='error'>{globalize.translate('LibrariesLoadError')}</Alert>
) : (
<Stack spacing={3} mt={2}>
<Stack direction='row' alignItems={'center'} spacing={1.5}>
<Button
startIcon={<Add />}
onClick={showMediaLibraryCreator}
>
{globalize.translate('ButtonAddMediaLibrary')}
</Button>
<Button
onClick={onScanLibraries}
startIcon={<RefreshIcon />}
loading={librariesTask && librariesTask.State !== TaskState.Idle}
loadingPosition='start'
variant='outlined'
>
{globalize.translate('ButtonScanAllLibraries')}
</Button>
{(librariesTask && librariesTask.State == TaskState.Running) && (
<TaskProgress task={librariesTask} />
)}
</Stack>
<Box>
<Grid container spacing={2}>
{virtualFolders?.map(virtualFolder => (
<Grid
key={virtualFolder?.ItemId}
item
xs={12}
sm={6}
md={3}
lg={2.4}
>
<LibraryCard
virtualFolder={virtualFolder}
/>
</Grid>
))}
</Grid>
</Box>
</Stack>
<Box>
<Grid container spacing={2}>
{virtualFolders?.map(virtualFolder => (
<Grid
key={virtualFolder?.ItemId}
item
xs={12}
sm={6}
md={3}
lg={2.4}
>
<LibraryCard
virtualFolder={virtualFolder}
/>
</Grid>
))}
</Grid>
</Box>
</Stack>
)}
</Box>
</Page>
);
+90 -86
View File
@@ -51,7 +51,7 @@ export const Component = () => {
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const { isPending: isConfigurationPending, data: config } = useConfiguration();
const { isPending: isConfigurationPending, isError: isConfigurationError, data: config } = useConfiguration();
if (isConfigurationPending) {
return <Loading />;
@@ -65,98 +65,102 @@ export const Component = () => {
>
<Box className='content-primary'>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('ButtonResume')}
</Typography>
{isConfigurationError ? (
<Alert severity='error'>{globalize.translate('ResumeLoadError')}</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('ButtonResume')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<TextField
label={globalize.translate('LabelMinResumePercentage')}
name='MinResumePercentage'
type='number'
defaultValue={config?.MinResumePct}
helperText={globalize.translate('LabelMinResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMinResumePercentage')}
name='MinResumePercentage'
type='number'
defaultValue={config.MinResumePct}
helperText={globalize.translate('LabelMinResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMaxResumePercentage')}
name='MaxResumePercentage'
type='number'
defaultValue={config?.MaxResumePct}
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMaxResumePercentage')}
name='MaxResumePercentage'
type='number'
defaultValue={config.MaxResumePct}
helperText={globalize.translate('LabelMaxResumePercentageHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMinAudiobookResume')}
name='MinAudiobookResume'
type='number'
defaultValue={config?.MinAudiobookResume}
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMinAudiobookResume')}
name='MinAudiobookResume'
type='number'
defaultValue={config.MinAudiobookResume}
helperText={globalize.translate('LabelMinAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 0,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMaxAudiobookResume')}
name='MaxAudiobookResume'
type='number'
defaultValue={config?.MaxAudiobookResume}
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMaxAudiobookResume')}
name='MaxAudiobookResume'
type='number'
defaultValue={config.MaxAudiobookResume}
helperText={globalize.translate('LabelMaxAudiobookResumeHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMinResumeDuration')}
name='MinResumeDuration'
type='number'
defaultValue={config?.MinResumeDurationSeconds}
helperText={globalize.translate('LabelMinResumeDurationHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelMinResumeDuration')}
name='MinResumeDuration'
type='number'
defaultValue={config.MinResumeDurationSeconds}
helperText={globalize.translate('LabelMinResumeDurationHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
)}
</Form>
</Box>
</Page>
@@ -42,7 +42,7 @@ export const Component = () => {
const actionData = useActionData() as ActionData | undefined;
const isSubmitting = navigation.state === 'submitting';
const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration();
const { isPending: isConfigurationPending, isError: isConfigurationError, data: defaultConfiguration } = useConfiguration();
if (isConfigurationPending) {
return <Loading />;
@@ -56,39 +56,43 @@ export const Component = () => {
>
<Box className='content-primary'>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('TabStreaming')}
</Typography>
{isConfigurationError ? (
<Alert severity='error'>{globalize.translate('StreamingLoadError')}</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('TabStreaming')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<TextField
type='number'
inputMode='decimal'
name='StreamingBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
helperText={globalize.translate('LabelRemoteClientBitrateLimitHelp')}
defaultValue={defaultConfiguration?.RemoteClientBitrateLimit ? defaultConfiguration?.RemoteClientBitrateLimit / 1e6 : ''}
slotProps={{
htmlInput: {
min: 0,
max: 2147.25,
step: 0.25
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
<TextField
type='number'
inputMode='decimal'
name='StreamingBitrateLimit'
label={globalize.translate('LabelRemoteClientBitrateLimit')}
helperText={globalize.translate('LabelRemoteClientBitrateLimitHelp')}
defaultValue={defaultConfiguration.RemoteClientBitrateLimit ? defaultConfiguration.RemoteClientBitrateLimit / 1e6 : ''}
slotProps={{
htmlInput: {
min: 0,
step: 0.25,
max: 2147.25
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
)}
</Form>
</Box>
</Page>
+179 -175
View File
@@ -63,7 +63,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
export const Component = () => {
const navigation = useNavigation();
const actionData = useActionData() as ActionData | undefined;
const { data: defaultConfig, isPending } = useConfiguration();
const { data: defaultConfig, isPending, isError } = useConfiguration();
const isSubmitting = navigation.state === 'submitting';
if (!defaultConfig || isPending) {
@@ -78,192 +78,196 @@ export const Component = () => {
>
<Box className='content-primary'>
<Form method='POST'>
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('Trickplay')}
</Typography>
{isError ? (
<Alert severity='error'>{globalize.translate('TrickplayLoadError')}</Alert>
) : (
<Stack spacing={3}>
<Typography variant='h1'>
{globalize.translate('Trickplay')}
</Typography>
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
{!isSubmitting && actionData?.isSaved && (
<Alert severity='success'>
{globalize.translate('SettingsSaved')}
</Alert>
)}
<FormControl>
<FormControlLabel
control={
<Checkbox
name='HwAcceleration'
defaultChecked={defaultConfig.TrickplayOptions?.EnableHwAcceleration}
/>
}
label={globalize.translate('LabelTrickplayAccel')}
<FormControl>
<FormControlLabel
control={
<Checkbox
name='HwAcceleration'
defaultChecked={defaultConfig.TrickplayOptions?.EnableHwAcceleration}
/>
}
label={globalize.translate('LabelTrickplayAccel')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='HwEncoding'
defaultChecked={defaultConfig.TrickplayOptions?.EnableHwEncoding}
/>
}
label={globalize.translate('LabelTrickplayAccelEncoding')}
/>
<FormHelperText>{globalize.translate('LabelTrickplayAccelEncodingHelp')}</FormHelperText>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='KeyFrameOnlyExtraction'
defaultChecked={defaultConfig.TrickplayOptions?.EnableKeyFrameOnlyExtraction}
/>
}
label={globalize.translate('LabelTrickplayKeyFrameOnlyExtraction')}
/>
<FormHelperText>{globalize.translate('LabelTrickplayKeyFrameOnlyExtractionHelp')}</FormHelperText>
</FormControl>
<TextField
name='ScanBehavior'
select
defaultValue={defaultConfig.TrickplayOptions?.ScanBehavior}
label={globalize.translate('LabelScanBehavior')}
helperText={globalize.translate('LabelScanBehaviorHelp')}
>
<MenuItem value={TrickplayScanBehavior.NonBlocking}>{globalize.translate('NonBlockingScan')}</MenuItem>
<MenuItem value={TrickplayScanBehavior.Blocking}>{globalize.translate('BlockingScan')}</MenuItem>
</TextField>
<TextField
name='ProcessPriority'
select
defaultValue={defaultConfig.TrickplayOptions?.ProcessPriority}
label={globalize.translate('LabelProcessPriority')}
helperText={globalize.translate('LabelProcessPriorityHelp')}
>
<MenuItem value={ProcessPriorityClass.High}>{globalize.translate('PriorityHigh')}</MenuItem>
<MenuItem value={ProcessPriorityClass.AboveNormal}>{globalize.translate('PriorityAboveNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.Normal}>{globalize.translate('PriorityNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.BelowNormal}>{globalize.translate('PriorityBelowNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.Idle}>{globalize.translate('PriorityIdle')}</MenuItem>
</TextField>
<TextField
label={globalize.translate('LabelImageInterval')}
name='ImageInterval'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Interval}
helperText={globalize.translate('LabelImageIntervalHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='HwEncoding'
defaultChecked={defaultConfig.TrickplayOptions?.EnableHwEncoding}
/>
}
label={globalize.translate('LabelTrickplayAccelEncoding')}
<TextField
label={globalize.translate('LabelWidthResolutions')}
name='WidthResolutions'
defaultValue={defaultConfig.TrickplayOptions?.WidthResolutions}
helperText={globalize.translate('LabelWidthResolutionsHelp')}
slotProps={{
htmlInput: {
required: true,
pattern: '[0-9,]*'
}
}}
/>
<FormHelperText>{globalize.translate('LabelTrickplayAccelEncodingHelp')}</FormHelperText>
</FormControl>
<FormControl>
<FormControlLabel
control={
<Checkbox
name='KeyFrameOnlyExtraction'
defaultChecked={defaultConfig.TrickplayOptions?.EnableKeyFrameOnlyExtraction}
/>
}
label={globalize.translate('LabelTrickplayKeyFrameOnlyExtraction')}
<TextField
label={globalize.translate('LabelTileWidth')}
name='TileWidth'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileWidth}
helperText={globalize.translate('LabelTileWidthHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<FormHelperText>{globalize.translate('LabelTrickplayKeyFrameOnlyExtractionHelp')}</FormHelperText>
</FormControl>
<TextField
name='ScanBehavior'
select
defaultValue={defaultConfig.TrickplayOptions?.ScanBehavior}
label={globalize.translate('LabelScanBehavior')}
helperText={globalize.translate('LabelScanBehaviorHelp')}
>
<MenuItem value={TrickplayScanBehavior.NonBlocking}>{globalize.translate('NonBlockingScan')}</MenuItem>
<MenuItem value={TrickplayScanBehavior.Blocking}>{globalize.translate('BlockingScan')}</MenuItem>
</TextField>
<TextField
label={globalize.translate('LabelTileHeight')}
name='TileHeight'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileHeight}
helperText={globalize.translate('LabelTileHeightHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
name='ProcessPriority'
select
defaultValue={defaultConfig.TrickplayOptions?.ProcessPriority}
label={globalize.translate('LabelProcessPriority')}
helperText={globalize.translate('LabelProcessPriorityHelp')}
>
<MenuItem value={ProcessPriorityClass.High}>{globalize.translate('PriorityHigh')}</MenuItem>
<MenuItem value={ProcessPriorityClass.AboveNormal}>{globalize.translate('PriorityAboveNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.Normal}>{globalize.translate('PriorityNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.BelowNormal}>{globalize.translate('PriorityBelowNormal')}</MenuItem>
<MenuItem value={ProcessPriorityClass.Idle}>{globalize.translate('PriorityIdle')}</MenuItem>
</TextField>
<TextField
label={globalize.translate('LabelJpegQuality')}
name='JpegQuality'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.JpegQuality}
helperText={globalize.translate('LabelJpegQualityHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelImageInterval')}
name='ImageInterval'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Interval}
helperText={globalize.translate('LabelImageIntervalHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelQscale')}
name='Qscale'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Qscale}
helperText={globalize.translate('LabelQscaleHelp')}
slotProps={{
htmlInput: {
min: 2,
max: 31,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelWidthResolutions')}
name='WidthResolutions'
defaultValue={defaultConfig.TrickplayOptions?.WidthResolutions}
helperText={globalize.translate('LabelWidthResolutionsHelp')}
slotProps={{
htmlInput: {
required: true,
pattern: '[0-9,]*'
}
}}
/>
<TextField
label={globalize.translate('LabelTrickplayThreads')}
name='TrickplayThreads'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.ProcessThreads}
helperText={globalize.translate('LabelTrickplayThreadsHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelTileWidth')}
name='TileWidth'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileWidth}
helperText={globalize.translate('LabelTileWidthHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelTileHeight')}
name='TileHeight'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.TileHeight}
helperText={globalize.translate('LabelTileHeightHelp')}
slotProps={{
htmlInput: {
min: 1,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelJpegQuality')}
name='JpegQuality'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.JpegQuality}
helperText={globalize.translate('LabelJpegQualityHelp')}
slotProps={{
htmlInput: {
min: 1,
max: 100,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelQscale')}
name='Qscale'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.Qscale}
helperText={globalize.translate('LabelQscaleHelp')}
slotProps={{
htmlInput: {
min: 2,
max: 31,
required: true
}
}}
/>
<TextField
label={globalize.translate('LabelTrickplayThreads')}
name='TrickplayThreads'
type='number'
inputMode='numeric'
defaultValue={defaultConfig.TrickplayOptions?.ProcessThreads}
helperText={globalize.translate('LabelTrickplayThreadsHelp')}
slotProps={{
htmlInput: {
min: 0,
required: true
}
}}
/>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
<Button
type='submit'
size='large'
>
{globalize.translate('Save')}
</Button>
</Stack>
)}
</Form>
</Box>
</Page>
+9 -1
View File
@@ -2,6 +2,7 @@
"Absolute": "Absolute",
"AccessRestrictedTryAgainLater": "Access is currently restricted. Please try again later.",
"Actor": "Actor",
"ActivitiesLoadError": "Failed to load activities",
"Add": "Add",
"AddedOnValue": "Added {0}",
"AddToCollection": "Add to collection",
@@ -57,6 +58,7 @@
"AnyLanguage": "Any Language",
"Anytime": "Anytime",
"ApiKeysCaption": "List of the currently enabled API keys",
"ApiKeysLoadError": "Failed to load API keys",
"AroundTime": "Around {0}",
"Arranger": "Arranger",
"Art": "Clearart",
@@ -88,6 +90,7 @@
"BoxRear": "Box (rear)",
"BoxSet": "Box Set",
"Browse": "Browse",
"BrandingLoadError": "Failed to load branding settings",
"BurnSubtitlesHelp": "Determine if the server should burn in subtitles. Avoiding this will greatly improve performance. Select Auto to burn image based formats (VobSub, PGS, SUB, IDX, etc.) and certain ASS or SSA subtitles.",
"ButtonActivate": "Activate",
"ButtonAddImage": "Add Image",
@@ -237,6 +240,7 @@
"Desktop": "Desktop (Legacy)",
"DetectingDevices": "Detecting devices",
"DeviceAccessHelp": "This only applies to devices that can be uniquely identified and will not prevent browser access. Filtering user device access will prevent them from using new devices until they've been approved here.",
"DevicesLoadError": "Failed to load devices",
"Digital": "Digital",
"Director": "Director",
"Directors": "Directors",
@@ -1047,6 +1051,7 @@
"LearnHowYouCanContribute": "Learn how you can contribute.",
"LeaveBlankToNotSetAPassword": "You can leave this field blank to set no password.",
"Letterer": "Letterer",
"LibrariesLoadError": "Failed to load libraries",
"LibraryAccessHelp": "Select the libraries to share with this user. Administrators will be able to edit all folders using the metadata manager.",
"LibraryNameInvalid": "Library name cannot be empty.",
"LibraryScanFanoutConcurrency": "Parallel library scan tasks limit",
@@ -1410,7 +1415,7 @@
"PluginLoadConfigError": "An error occurred while getting the plugin configuration pages.",
"PluginLoadRepoError": "An error occurred while getting the plugin details from the repository.",
"PluginUninstallError": "An error occurred while uninstalling the plugin.",
"PluginsLoadError": "An error occurred while loading plugins.",
"PluginsLoadError": "An error occurred while loading plugins",
"Poster": "Poster",
"PosterCard": "Poster Card",
"PreferEmbeddedEpisodeInfosOverFileNames": "Prefer embedded episode information over filenames",
@@ -1499,6 +1504,7 @@
"ResolutionMatchSource": "Match Source",
"Restart": "Restart",
"ResumeAt": "Resume from {0}",
"ResumeLoadError": "Failed to load resume settings",
"Rewind": "Rewind",
"Runtime": "Runtime",
"Saturday": "Saturday",
@@ -1581,6 +1587,7 @@
"StorageType.Removable": "Removable",
"StorageType.Unknown": "Unknown",
"StreamCountExceedsLimit": "The number of streams exceeds the limit",
"StreamingLoadError": "Failed to load streaming page settings",
"Studio": "Studio",
"Studios": "Studios",
"Subtitle": "Subtitle",
@@ -1838,6 +1845,7 @@
"HearingImpairedShort": "HI/SDH",
"LabelIsHearingImpaired": "For hearing impaired (SDH)",
"Trickplay": "Trickplay",
"TrickplayLoadError": "Failed to load trickplay settings",
"LabelTrickplayAccel": "Enable hardware decoding",
"LabelTrickplayAccelEncoding": "Enable hardware accelerated MJPEG encoding",
"LabelTrickplayAccelEncodingHelp": "Currently only available on QSV, VA-API, VideoToolbox and RKMPP, this option has no effect on other hardware acceleration methods.",