1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

Move trickplay settings to react

This commit is contained in:
Nick 2024-03-18 12:28:38 -07:00
parent fc664090cc
commit 08f9ec9f01
6 changed files with 326 additions and 146 deletions

View file

@ -9,5 +9,6 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
{ path: 'users/add', type: AsyncRouteType.Dashboard }, { path: 'users/add', type: AsyncRouteType.Dashboard },
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard }, { path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
{ path: 'users/password', type: AsyncRouteType.Dashboard }, { path: 'users/password', type: AsyncRouteType.Dashboard },
{ path: 'users/profile', type: AsyncRouteType.Dashboard } { path: 'users/profile', type: AsyncRouteType.Dashboard },
{ path: 'playback/trickplay', type: AsyncRouteType.Dashboard }
]; ];

View file

@ -145,11 +145,5 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
view: 'dashboard/streaming.html', view: 'dashboard/streaming.html',
controller: 'dashboard/streaming' controller: 'dashboard/streaming'
} }
}, {
path: 'playback/trickplay',
pageProps: {
view: 'dashboard/trickplay.html',
controller: 'dashboard/trickplay'
}
} }
]; ];

View file

@ -0,0 +1,291 @@
import type { ProcessPriorityClass, ServerConfiguration, TrickplayScanBehavior } from '@jellyfin/sdk/lib/generated-client';
import React, { FunctionComponent, useCallback, useEffect, useRef } from 'react';
import globalize from '../../../../scripts/globalize';
import Page from '../../../../components/Page';
import SectionTitleContainer from '../../../../elements/SectionTitleContainer';
import ButtonElement from '../../../../elements/ButtonElement';
import CheckBoxElement from '../../../../elements/CheckBoxElement';
import SelectElement from '../../../../elements/SelectElement';
import InputElement from '../../../../elements/InputElement';
import LinkTrickplayAcceleration from '../../../../components/dashboard/playback/trickplay/LinkTrickplayAcceleration';
import loading from '../../../../components/loading/loading';
import toast from '../../../../components/toast/toast';
function onSaveComplete() {
loading.hide();
toast(globalize.translate('SettingsSaved'));
}
const PlaybackTrickplay: FunctionComponent = () => {
const element = useRef<HTMLDivElement>(null);
const loadConfig = useCallback((config) => {
const page = element.current;
const options = config.TrickplayOptions;
if (!page) {
console.error('Unexpected null reference');
return;
}
(page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked = options.EnableHwAcceleration;
(page.querySelector('#selectScanBehavior') as HTMLSelectElement).value = options.ScanBehavior;
(page.querySelector('#selectProcessPriority') as HTMLSelectElement).value = options.ProcessPriority;
(page.querySelector('#txtInterval') as HTMLInputElement).value = options.Interval;
(page.querySelector('#txtWidthResolutions') as HTMLInputElement).value = options.WidthResolutions.join(',');
(page.querySelector('#txtTileWidth') as HTMLInputElement).value = options.TileWidth;
(page.querySelector('#txtTileHeight') as HTMLInputElement).value = options.TileHeight;
(page.querySelector('#txtQscale') as HTMLInputElement).value = options.Qscale;
(page.querySelector('#txtJpegQuality') as HTMLInputElement).value = options.JpegQuality;
(page.querySelector('#txtProcessThreads') as HTMLInputElement).value = options.ProcessThreads;
loading.hide();
}, []);
const loadData = useCallback(() => {
loading.show();
window.ApiClient.getServerConfiguration().then(function (config) {
loadConfig(config);
}).catch(err => {
console.error('[playbacktrickplay] failed to fetch server config', err);
});
}, [loadConfig]);
useEffect(() => {
const page = element.current;
if (!page) {
console.error('Unexpected null reference');
return;
}
const saveConfig = (config: ServerConfiguration) => {
if (!config.TrickplayOptions) {
throw new Error('Unexpected null TrickplayOptions');
}
const options = config.TrickplayOptions;
options.EnableHwAcceleration = (page.querySelector('.chkEnableHwAcceleration') as HTMLInputElement).checked;
options.ScanBehavior = (page.querySelector('#selectScanBehavior') as HTMLSelectElement).value as TrickplayScanBehavior;
options.ProcessPriority = (page.querySelector('#selectProcessPriority') as HTMLSelectElement).value as ProcessPriorityClass;
options.Interval = Math.max(1, parseInt((page.querySelector('#txtInterval') as HTMLInputElement).value || '10000', 10));
options.WidthResolutions = (page.querySelector('#txtWidthResolutions') as HTMLInputElement).value.replace(' ', '').split(',').map(Number);
options.TileWidth = Math.max(1, parseInt((page.querySelector('#txtTileWidth') as HTMLInputElement).value || '10', 10));
options.TileHeight = Math.max(1, parseInt((page.querySelector('#txtTileHeight') as HTMLInputElement).value || '10', 10));
options.Qscale = Math.min(31, parseInt((page.querySelector('#txtQscale') as HTMLInputElement).value || '4', 10));
options.JpegQuality = Math.min(100, parseInt((page.querySelector('#txtJpegQuality') as HTMLInputElement).value || '90', 10));
options.ProcessThreads = parseInt((page.querySelector('#txtProcessThreads') as HTMLInputElement).value || '1', 10);
window.ApiClient.updateServerConfiguration(config).then(() => {
onSaveComplete();
}).catch(err => {
console.error('[playbacktrickplay] failed to update config', err);
});
};
const onSubmit = (e: Event) => {
loading.show();
window.ApiClient.getServerConfiguration().then(function (config) {
saveConfig(config);
}).catch(err => {
console.error('[playbacktrickplay] failed to fetch server config', err);
});
e.preventDefault();
e.stopPropagation();
return false;
};
(page.querySelector('.trickplayConfigurationForm') as HTMLFormElement).addEventListener('submit', onSubmit);
loadData();
}, [loadData]);
const optionScanBehavior = () => {
let content = '';
content += `<option value='NonBlocking'>${globalize.translate('NonBlockingScan')}</option>`;
content += `<option value='Blocking'>${globalize.translate('BlockingScan')}</option>`;
return content;
};
const optionProcessPriority = () => {
let content = '';
content += `<option value='High'>${globalize.translate('PriorityHigh')}</option>`;
content += `<option value='AboveNormal'>${globalize.translate('PriorityAboveNormal')}</option>`;
content += `<option value='Normal'>${globalize.translate('PriorityNormal')}</option>`;
content += `<option value='BelowNormal'>${globalize.translate('PriorityBelowNormal')}</option>`;
content += `<option value='Idle'>${globalize.translate('PriorityIdle')}</option>`;
return content;
};
return (
<Page
id='trickplayConfigurationPage'
className='mainAnimatedPage type-interior playbackConfigurationPage'
>
<div ref={element} className='content-primary'>
<div className='verticalSection'>
<SectionTitleContainer
title={globalize.translate('Trickplay')}
isLinkVisible={false}
/>
</div>
<form className='trickplayConfigurationForm'>
<div className='checkboxContainer checkboxContainer-withDescription'>
<CheckBoxElement
className='chkEnableHwAcceleration'
title='LabelTrickplayAccel'
/>
<div className='fieldDescription checkboxFieldDescription'>
<LinkTrickplayAcceleration
title='LabelTrickplayAccelHelp'
href='#/dashboard/playback/transcoding'
className='button-link'
/>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectScanBehavior'>
<SelectElement
id='selectScanBehavior'
label='LabelScanBehavior'
>
{optionScanBehavior()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('LabelScanBehaviorHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='selectContainer fldSelectProcessPriority'>
<SelectElement
id='selectProcessPriority'
label='LabelProcessPriority'
>
{optionProcessPriority()}
</SelectElement>
<div className='fieldDescription'>
{globalize.translate('LabelProcessPriorityHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtInterval'
label='LabelImageInterval'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelImageIntervalHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='text'
id='txtWidthResolutions'
label='LabelWidthResolutions'
options={'required pattern="[0-9,]*"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelWidthResolutionsHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtTileWidth'
label='LabelTileWidth'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTileWidthHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtTileHeight'
label='LabelTileHeight'
options={'required inputMode="numeric" pattern="[0-9]*" min="1"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTileHeightHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtJpegQuality'
label='LabelJpegQuality'
options={'required inputMode="numeric" pattern="[0-9]*" min="1" max="100"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelJpegQualityHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtQscale'
label='LabelQscale'
options={'required inputMode="numeric" pattern="[0-9]*" min="2" max="31"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelQscaleHelp')}
</div>
</div>
</div>
<div className='verticalSection'>
<div className='inputContainer'>
<InputElement
type='number'
id='txtProcessThreads'
label='LabelTrickplayThreads'
options={'required inputMode="numeric" pattern="[0-9]*" min="0"'}
/>
<div className='fieldDescription'>
{globalize.translate('LabelTrickplayThreadsHelp')}
</div>
</div>
</div>
<div>
<ButtonElement
type='submit'
className='raised button-submit block'
title='Save'
/>
</div>
</form>
</div>
</Page>
);
};
export default PlaybackTrickplay;

View file

@ -0,0 +1,33 @@
import React, { FunctionComponent } from 'react';
import globalize from '../../../../scripts/globalize';
type IProps = {
title?: string;
className?: string;
href?: string;
};
const createLinkElement = ({ className, title, href }: IProps) => ({
__html: `<a
is="emby-linkbutton"
rel="noopener noreferrer"
class="${className}"
href="${href}"
>
${title}
</a>`
});
const LinkTrickplayAcceleration: FunctionComponent<IProps> = ({ className, title, href }: IProps) => {
return (
<div
dangerouslySetInnerHTML={createLinkElement({
className: className,
title: globalize.translate(title),
href: href
})}
/>
);
};
export default LinkTrickplayAcceleration;

View file

@ -1,68 +0,0 @@
<div id="trickplayConfigurationPage" data-role="page" class="page type-interior playbackConfigurationPage withTabs">
<div>
<div class="content-primary">
<form class="trickplayConfigurationForm">
<div class="sectionTitleContainer flex align-items-center">
<h2 class="sectionTitle">${Trickplay}</h2>
</div>
<div class="checkboxListContainer">
<div class="checkboxList">
<label>
<input type="checkbox" is="emby-checkbox" id="chkEnableHwAcceleration" />
<span>${LabelTrickplayAccel}</span>
</label>
</div>
<div class="fieldDescription">
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="#/dashboard/playback/transcoding">${LabelTrickplayAccelHelp}</a>
</div>
</div>
<div class="inputContainer">
<select is="emby-select" id="selectScanBehavior" name="Scan Behavior" label="${LabelScanBehavior}">
<option id="optNonBlocking" value="NonBlocking">${NonBlockingScan}</option>
<option id="optBlocking" value="Blocking">${BlockingScan}</option>
</select>
<div class="fieldDescription">${LabelScanBehaviorHelp}</div>
</div>
<div class="inputContainer">
<select is="emby-select" id="selectProcessPriority" name="Process Priority" label="${LabelProcessPriority}">
<option id="optPriorityHigh" value="High">${PriorityHigh}</option>
<option id="optPriorityAboveNormal" value="AboveNormal">${PriorityAboveNormal}</option>
<option id="optPriorityNormal" value="Normal">${PriorityNormal}</option>
<option id="optPriorityBelowNormal" value="BelowNormal">${PriorityBelowNormal}</option>
<option id="optPriorityIdle" value="Idle">${PriorityIdle}</option>
</select>
<div class="fieldDescription">${LabelProcessPriorityHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtInterval" pattern="[0-9]*" min="0" required label="${LabelImageInterval}" />
<div class="fieldDescription">${LabelImageIntervalHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" id="txtWidthResolutions" pattern="[0-9,]*" required label="${LabelWidthResolutions}">
<div class="fieldDescription">${LabelWidthResolutionsHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtTileWidth" pattern="[0-9]*" min="1" required label="${LabelTileWidth}">
<div class="fieldDescription">${LabelTileWidthHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtTileHeight" pattern="[0-9]*" min="1" required label="${LabelTileHeight}">
<div class="fieldDescription">${LabelTileHeightHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtJpegQuality" pattern="[0-9]*" min="1" max="100" required label="${LabelJpegQuality}">
<div class="fieldDescription">${LabelJpegQualityHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtQscale" pattern="[0-9]*" min="2" max="31" required label="${LabelQscale}">
<div class="fieldDescription">${LabelQscaleHelp}</div>
</div>
<div class="inputContainer">
<input is="emby-input" type="number" id="txtProcessThreads" pattern="[0-9]*" required="" label="${LabelTrickplayThreads}">
<div class="fieldDescription"></div>
</div>
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
</form>
</div>
</div>
</div>

View file

@ -1,71 +0,0 @@
import 'jquery';
import loading from '../../components/loading/loading';
import libraryMenu from '../../scripts/libraryMenu';
import globalize from '../../scripts/globalize';
import Dashboard from '../../utils/dashboard';
function loadPage(page, config) {
const trickplayOptions = config.TrickplayOptions;
page.querySelector('#chkEnableHwAcceleration').checked = trickplayOptions.EnableHwAcceleration;
$('#selectScanBehavior', page).val(trickplayOptions.ScanBehavior);
$('#selectProcessPriority', page).val(trickplayOptions.ProcessPriority);
$('#txtInterval', page).val(trickplayOptions.Interval);
$('#txtWidthResolutions', page).val(trickplayOptions.WidthResolutions.join(','));
$('#txtTileWidth', page).val(trickplayOptions.TileWidth);
$('#txtTileHeight', page).val(trickplayOptions.TileHeight);
$('#txtQscale', page).val(trickplayOptions.Qscale);
$('#txtJpegQuality', page).val(trickplayOptions.JpegQuality);
$('#txtProcessThreads', page).val(trickplayOptions.ProcessThreads);
loading.hide();
}
function onSubmit() {
loading.show();
const form = this;
ApiClient.getServerConfiguration().then(function (config) {
const trickplayOptions = config.TrickplayOptions;
trickplayOptions.EnableHwAcceleration = form.querySelector('#chkEnableHwAcceleration').checked;
trickplayOptions.ScanBehavior = $('#selectScanBehavior', form).val();
trickplayOptions.ProcessPriority = $('#selectProcessPriority', form).val();
trickplayOptions.Interval = Math.max(0, parseInt($('#txtInterval', form).val() || '10000', 10));
trickplayOptions.WidthResolutions = $('#txtWidthResolutions', form).val().replace(' ', '').split(',').map(Number);
trickplayOptions.TileWidth = Math.max(1, parseInt($('#txtTileWidth', form).val() || '10', 10));
trickplayOptions.TileHeight = Math.max(1, parseInt($('#txtTileHeight', form).val() || '10', 10));
trickplayOptions.Qscale = Math.min(31, parseInt($('#txtQscale', form).val() || '10', 10));
trickplayOptions.JpegQuality = Math.min(100, parseInt($('#txtJpegQuality', form).val() || '80', 10));
trickplayOptions.ProcessThreads = parseInt($('#txtProcessThreads', form).val() || '0', 10);
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
});
return false;
}
function getTabs() {
return [{
href: '#/dashboard/playback/transcoding',
name: globalize.translate('Transcoding')
}, {
href: '#/dashboard/playback/resume',
name: globalize.translate('ButtonResume')
}, {
href: '#/dashboard/playback/streaming',
name: globalize.translate('TabStreaming')
}, {
href: '#/dashboard/playback/trickplay',
name: globalize.translate('Trickplay')
}];
}
$(document).on('pageinit', '#trickplayConfigurationPage', function () {
$('.trickplayConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
}).on('pageshow', '#trickplayConfigurationPage', function () {
loading.show();
libraryMenu.setTabs('playback', 3, getTabs);
const page = this;
ApiClient.getServerConfiguration().then(function (config) {
loadPage(page, config);
});
});