mirror of
https://github.com/jellyfin/jellyfin-web.git
synced 2026-09-03 03:50:02 +03:00
Merge pull request #7223 from viown/use-error-messages
Add error handling to rest of dashboard pages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user