mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #6474 from thornbill/dashboard-controllers
Move dashboard controllers to app directory
This commit is contained in:
commit
d5db15367b
45 changed files with 246 additions and 222 deletions
126
src/apps/dashboard/controllers/dashboard.html
Normal file
126
src/apps/dashboard/controllers/dashboard.html
Normal file
|
@ -0,0 +1,126 @@
|
|||
<div id="dashboardPage" data-role="page" class="page type-interior dashboardHomePage fullWidthContent" data-title="${TabDashboard}">
|
||||
<div class="content-primary">
|
||||
<div class="dashboardSections" style="padding-top:.5em;">
|
||||
<div class="dashboardColumn dashboardColumn-2-60 dashboardColumn-3-46">
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${TabServer}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
|
||||
<div class="serverInfo paperList">
|
||||
<div>${LabelServerName}</div>
|
||||
<div id="serverName"></div>
|
||||
<div>${LabelServerVersion}</div>
|
||||
<div id="versionNumber"></div>
|
||||
<div>${LabelWebVersion}</div>
|
||||
<div id="webVersion"></div>
|
||||
<div>${LabelBuildVersion}</div>
|
||||
<div id="buildVersion"></div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardActionsContainer">
|
||||
<button is="emby-button" type="button" class="raised btnRefresh">
|
||||
<span>${ButtonScanAllLibraries}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" id="btnRestartServer" class="raised" onclick="DashboardPage.restart(this);">
|
||||
<span>${Restart}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" id="btnShutdown" class="raised" onclick="DashboardPage.shutdown(this);">
|
||||
<span>${ButtonShutdown}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 2em;" class="runningTasksContainer hide">
|
||||
<h3>${HeaderRunningTasks}</h3>
|
||||
<div id="divRunningTasks" class="paperList" style="padding: 1em;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboard/devices" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderActiveDevices}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="activeDevices itemsContainer vertical-wrap">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardColumn dashboardColumn-2-40 dashboardColumn-3-27">
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboard/activity?useractivity=true" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderActivity}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="paperList userActivityItems" data-activitylimit="7" data-useractivity="true">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboardColumn dashboardColumn-3-27">
|
||||
|
||||
<div class="dashboardSection activeRecordingsSection hide">
|
||||
<h3>${HeaderActiveRecordings}</h3>
|
||||
<div class="activeRecordingItems vertical-wrap" is="emby-itemscontainer">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardSection serverActivitySection hide activityContainer">
|
||||
<a is="emby-linkbutton" href="#/dashboard/activity?useractivity=false" class="button-flat sectionTitleTextButton">
|
||||
<h3>${Alerts}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="paperList serverActivityItems" data-activitylimit="4" data-useractivity="false">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardSection">
|
||||
<a is="emby-linkbutton" href="#/dashboard/settings" class="button-flat sectionTitleTextButton">
|
||||
<h3>${HeaderPaths}</h3>
|
||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||
</a>
|
||||
<div class="paperList">
|
||||
<div class="listItem listItem-border">
|
||||
<div class="listItemBody two-line">
|
||||
<div class="listItemBodyText secondary" style="margin:0;">${LabelCache}</div>
|
||||
<div class="listItemBodyText" id="cachePath" dir="ltr" style="text-align: left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listItem listItem-border">
|
||||
<div class="listItemBody two-line">
|
||||
<div class="listItemBodyText secondary" style="margin:0;">${LabelLogs}</div>
|
||||
<div class="listItemBodyText" id="logPath" dir="ltr" style="text-align: left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listItem listItem-border">
|
||||
<div class="listItemBody two-line">
|
||||
<div class="listItemBodyText secondary" style="margin:0;">${LabelMetadata}</div>
|
||||
<div class="listItemBodyText" id="metadataPath" dir="ltr" style="text-align: left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listItem listItem-border">
|
||||
<div class="listItemBody two-line">
|
||||
<div class="listItemBodyText secondary" style="margin:0;">${LabelTranscodes}</div>
|
||||
<div class="listItemBodyText" id="transcodePath" dir="ltr" style="text-align: left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="listItem listItem-border">
|
||||
<div class="listItemBody two-line">
|
||||
<div class="listItemBodyText secondary" style="margin:0;">${LabelWeb}</div>
|
||||
<div class="listItemBodyText" id="webPath" dir="ltr" style="text-align: left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dashboardFooter">
|
||||
<div style="height:1px;" class="ui-bar-inherit"></div>
|
||||
<div style="margin-top:1em;">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org" target="_blank">Jellyfin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
857
src/apps/dashboard/controllers/dashboard.js
Normal file
857
src/apps/dashboard/controllers/dashboard.js
Normal file
|
@ -0,0 +1,857 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
|
||||
import datetime from 'scripts/datetime';
|
||||
import Events from 'utils/events.ts';
|
||||
import itemHelper from 'components/itemHelper';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { getLocaleWithSuffix } from 'utils/dateFnsLocale.ts';
|
||||
import loading from 'components/loading/loading';
|
||||
import playMethodHelper from 'components/playback/playmethodhelper';
|
||||
import cardBuilder from 'components/cardbuilder/cardBuilder';
|
||||
import imageLoader from 'components/images/imageLoader';
|
||||
import ActivityLog from 'components/activitylog';
|
||||
import imageHelper from 'utils/image';
|
||||
import indicators from 'components/indicators/indicators';
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import ServerConnections from 'components/ServerConnections';
|
||||
import alert from 'components/alert';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
import { getSystemInfoQuery } from 'hooks/useSystemInfo';
|
||||
import { toApi } from 'utils/jellyfin-apiclient/compat';
|
||||
import { queryClient } from 'utils/query/queryClient';
|
||||
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
|
||||
import 'components/listview/listview.scss';
|
||||
import 'styles/flexstyles.scss';
|
||||
import './dashboard.scss';
|
||||
|
||||
function showPlaybackInfo(btn, session) {
|
||||
let title;
|
||||
const text = [];
|
||||
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
|
||||
|
||||
if (displayPlayMethod === 'Remux') {
|
||||
title = globalize.translate('Remuxing');
|
||||
text.push(globalize.translate('RemuxHelp1'));
|
||||
text.push('<br/>');
|
||||
text.push(globalize.translate('RemuxHelp2'));
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
title = globalize.translate('DirectStreaming');
|
||||
text.push(globalize.translate('DirectStreamHelp1'));
|
||||
text.push('<br/>');
|
||||
text.push(globalize.translate('DirectStreamHelp2'));
|
||||
} else if (displayPlayMethod === 'DirectPlay') {
|
||||
title = globalize.translate('DirectPlaying');
|
||||
text.push(globalize.translate('DirectPlayHelp'));
|
||||
} else if (displayPlayMethod === 'Transcode') {
|
||||
title = globalize.translate('Transcoding');
|
||||
text.push(globalize.translate('MediaIsBeingConverted'));
|
||||
text.push(DashboardPage.getSessionNowPlayingStreamInfo(session));
|
||||
|
||||
if (session.TranscodingInfo?.TranscodeReasons?.length) {
|
||||
text.push('<br/>');
|
||||
text.push(globalize.translate('LabelReasonForTranscoding'));
|
||||
session.TranscodingInfo.TranscodeReasons.forEach(function (transcodeReason) {
|
||||
text.push(globalize.translate(transcodeReason));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
alert({
|
||||
text: text.join('<br/>'),
|
||||
title: title
|
||||
});
|
||||
}
|
||||
|
||||
function showSendMessageForm(btn, session) {
|
||||
import('components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
title: globalize.translate('HeaderSendMessage'),
|
||||
label: globalize.translate('LabelMessageText'),
|
||||
confirmText: globalize.translate('ButtonSend')
|
||||
}).then(function (text) {
|
||||
if (text) {
|
||||
ServerConnections.getApiClient(session.ServerId).sendMessageCommand(session.Id, {
|
||||
Text: text,
|
||||
TimeoutMs: 5e3
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showOptionsMenu(btn, session) {
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
const menuItems = [];
|
||||
|
||||
if (session.ServerId && session.DeviceId !== ServerConnections.deviceId()) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('SendMessage'),
|
||||
id: 'sendmessage'
|
||||
});
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo?.TranscodeReasons?.length) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('ViewPlaybackInfo'),
|
||||
id: 'transcodinginfo'
|
||||
});
|
||||
}
|
||||
|
||||
return actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: btn
|
||||
}).then(function (id) {
|
||||
switch (id) {
|
||||
case 'sendmessage':
|
||||
showSendMessageForm(btn, session);
|
||||
break;
|
||||
case 'transcodinginfo':
|
||||
showPlaybackInfo(btn, session);
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onActiveDevicesClick(evt) {
|
||||
const btn = dom.parentWithClass(evt.target, 'sessionCardButton');
|
||||
|
||||
if (btn) {
|
||||
const card = dom.parentWithClass(btn, 'card');
|
||||
|
||||
if (card) {
|
||||
const sessionId = card.id;
|
||||
const session = (DashboardPage.sessionsList || []).filter(function (dashboardSession) {
|
||||
return 'session' + dashboardSession.Id === sessionId;
|
||||
})[0];
|
||||
|
||||
if (session) {
|
||||
if (btn.classList.contains('btnCardOptions')) {
|
||||
showOptionsMenu(btn, session);
|
||||
} else if (btn.classList.contains('btnSessionInfo')) {
|
||||
showPlaybackInfo(btn, session);
|
||||
} else if (btn.classList.contains('btnSessionSendMessage')) {
|
||||
showSendMessageForm(btn, session);
|
||||
} else if (btn.classList.contains('btnSessionStop')) {
|
||||
ServerConnections.getApiClient(session.ServerId).sendPlayStateCommand(session.Id, 'Stop');
|
||||
} else if (btn.classList.contains('btnSessionPlayPause') && session.PlayState) {
|
||||
ServerConnections.getApiClient(session.ServerId).sendPlayStateCommand(session.Id, 'PlayPause');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterSessions(sessions) {
|
||||
const list = [];
|
||||
const minActiveDate = new Date().getTime() - 9e5;
|
||||
|
||||
for (let i = 0, length = sessions.length; i < length; i++) {
|
||||
const session = sessions[i];
|
||||
|
||||
if (!session.NowPlayingItem && !session.UserId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (datetime.parseISO8601Date(session.LastActivityDate, true).getTime() >= minActiveDate) {
|
||||
list.push(session);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function refreshActiveRecordings(view, apiClient) {
|
||||
apiClient.getLiveTvRecordings({
|
||||
UserId: Dashboard.getCurrentUserId(),
|
||||
IsInProgress: true,
|
||||
Fields: 'CanDelete,PrimaryImageAspectRatio',
|
||||
EnableTotalRecordCount: false,
|
||||
EnableImageTypes: 'Primary,Thumb,Backdrop'
|
||||
}).then(function (result) {
|
||||
const itemsContainer = view.querySelector('.activeRecordingItems');
|
||||
|
||||
if (!result.Items.length) {
|
||||
view.querySelector('.activeRecordingsSection').classList.add('hide');
|
||||
itemsContainer.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
view.querySelector('.activeRecordingsSection').classList.remove('hide');
|
||||
itemsContainer.innerHTML = cardBuilder.getCardsHtml({
|
||||
items: result.Items,
|
||||
shape: 'auto',
|
||||
defaultShape: 'backdrop',
|
||||
showTitle: true,
|
||||
showParentTitle: true,
|
||||
coverImage: true,
|
||||
cardLayout: false,
|
||||
centerText: true,
|
||||
preferThumb: 'auto',
|
||||
overlayText: false,
|
||||
overlayMoreButton: true,
|
||||
action: 'none',
|
||||
centerPlayButton: true
|
||||
});
|
||||
imageLoader.lazyChildren(itemsContainer);
|
||||
});
|
||||
}
|
||||
|
||||
function reloadSystemInfo(view, apiClient) {
|
||||
view.querySelector('#buildVersion').innerText = __JF_BUILD_VERSION__;
|
||||
|
||||
let webVersion = __PACKAGE_JSON_VERSION__;
|
||||
if (__COMMIT_SHA__) {
|
||||
webVersion += ` (${__COMMIT_SHA__})`;
|
||||
}
|
||||
view.querySelector('#webVersion').innerText = webVersion;
|
||||
|
||||
queryClient
|
||||
.fetchQuery(getSystemInfoQuery(toApi(apiClient)))
|
||||
.then(systemInfo => {
|
||||
view.querySelector('#serverName').innerText = systemInfo.ServerName;
|
||||
view.querySelector('#versionNumber').innerText = systemInfo.Version;
|
||||
|
||||
view.querySelector('#cachePath').innerText = systemInfo.CachePath;
|
||||
view.querySelector('#logPath').innerText = systemInfo.LogPath;
|
||||
view.querySelector('#transcodePath').innerText = systemInfo.TranscodingTempPath;
|
||||
view.querySelector('#metadataPath').innerText = systemInfo.InternalMetadataPath;
|
||||
view.querySelector('#webPath').innerText = systemInfo.WebPath;
|
||||
});
|
||||
}
|
||||
|
||||
function renderInfo(view, sessions) {
|
||||
sessions = filterSessions(sessions);
|
||||
renderActiveConnections(view, sessions);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function pollForInfo(view, apiClient) {
|
||||
apiClient.getSessions({
|
||||
ActiveWithinSeconds: 960
|
||||
}).then(function (sessions) {
|
||||
renderInfo(view, sessions);
|
||||
});
|
||||
apiClient.getScheduledTasks().then(function (tasks) {
|
||||
renderRunningTasks(view, tasks);
|
||||
});
|
||||
}
|
||||
|
||||
function renderActiveConnections(view, sessions) {
|
||||
let html = '';
|
||||
DashboardPage.sessionsList = sessions;
|
||||
const parentElement = view.querySelector('.activeDevices');
|
||||
const cardElem = parentElement.querySelector('.card');
|
||||
|
||||
if (cardElem) {
|
||||
cardElem.classList.add('deadSession');
|
||||
}
|
||||
|
||||
for (let i = 0, length = sessions.length; i < length; i++) {
|
||||
const session = sessions[i];
|
||||
const rowId = 'session' + session.Id;
|
||||
const elem = view.querySelector('#' + rowId);
|
||||
|
||||
if (elem) {
|
||||
DashboardPage.updateSession(elem, session);
|
||||
} else {
|
||||
const nowPlayingItem = session.NowPlayingItem;
|
||||
const className = 'scalableCard card activeSession backdropCard backdropCard-scalable';
|
||||
const imgUrl = DashboardPage.getNowPlayingImageUrl(nowPlayingItem);
|
||||
|
||||
html += '<div class="' + className + '" id="' + rowId + '">';
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += `<div class="cardContent ${getDefaultBackgroundClass()}">`;
|
||||
|
||||
if (imgUrl) {
|
||||
html += '<div class="sessionNowPlayingContent sessionNowPlayingContent-withbackground"';
|
||||
html += ' data-src="' + imgUrl + '" style="display:inline-block;background-image:url(\'' + imgUrl + "');\"></div>";
|
||||
} else {
|
||||
html += '<div class="sessionNowPlayingContent"></div>';
|
||||
}
|
||||
|
||||
html += `<div class="sessionNowPlayingInnerContent ${imgUrl ? 'darkenContent' : ''}">`;
|
||||
html += '<div class="sessionAppInfo">';
|
||||
const clientImage = DashboardPage.getClientImage(session);
|
||||
|
||||
if (clientImage) {
|
||||
html += clientImage;
|
||||
}
|
||||
|
||||
html += '<div class="sessionAppName" style="display:inline-block; text-align:left;" dir="ltr" >';
|
||||
html += '<div class="sessionDeviceName">' + escapeHtml(session.DeviceName) + '</div>';
|
||||
html += '<div class="sessionAppSecondaryText">' + escapeHtml(DashboardPage.getAppSecondaryText(session)) + '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="sessionNowPlayingDetails">';
|
||||
const nowPlayingName = DashboardPage.getNowPlayingName(session);
|
||||
html += '<div class="sessionNowPlayingInfo" data-imgsrc="' + nowPlayingName.image + '">';
|
||||
html += '<span class="sessionNowPlayingName">' + nowPlayingName.html + '</span>';
|
||||
html += '</div>';
|
||||
html += '<div class="sessionNowPlayingTime">' + escapeHtml(DashboardPage.getSessionNowPlayingTime(session)) + '</div>';
|
||||
html += '</div>';
|
||||
|
||||
let percent = 100 * session?.PlayState?.PositionTicks / nowPlayingItem?.RunTimeTicks;
|
||||
html += indicators.getProgressHtml(percent || 0, {
|
||||
containerClass: 'playbackProgress'
|
||||
});
|
||||
|
||||
percent = session?.TranscodingInfo?.CompletionPercentage?.toFixed(1);
|
||||
html += indicators.getProgressHtml(percent || 0, {
|
||||
containerClass: 'transcodingProgress'
|
||||
});
|
||||
|
||||
html += indicators.getProgressHtml(100, {
|
||||
containerClass: 'backgroundProgress'
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="sessionCardFooter cardFooter">';
|
||||
html += '<div class="sessionCardButtons flex align-items-center justify-content-center">';
|
||||
|
||||
let btnCssClass = session.ServerId && session.NowPlayingItem && session.SupportsRemoteControl ? '' : ' hide';
|
||||
const playIcon = session.PlayState.IsPaused ? 'play_arrow' : 'pause';
|
||||
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionPlayPause paper-icon-button-light ' + btnCssClass + '"><span class="material-icons ' + playIcon + '" aria-hidden="true"></span></button>';
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionStop paper-icon-button-light ' + btnCssClass + '"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionInfo paper-icon-button-light ' + btnCssClass + '" title="' + globalize.translate('ViewPlaybackInfo') + '"><span class="material-icons info" aria-hidden="true"></span></button>';
|
||||
|
||||
btnCssClass = session.ServerId && session.SupportedCommands.indexOf('DisplayMessage') !== -1 && session.DeviceId !== ServerConnections.deviceId() ? '' : ' hide';
|
||||
html += '<button is="paper-icon-button-light" class="sessionCardButton btnSessionSendMessage paper-icon-button-light ' + btnCssClass + '" title="' + globalize.translate('SendMessage') + '"><span class="material-icons message" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
|
||||
html += '<div class="flex align-items-center justify-content-center">';
|
||||
const userImage = DashboardPage.getUserImage(session);
|
||||
html += userImage ? '<div class="activitylogUserPhoto" style="background-image:url(\'' + userImage + "');\"></div>" : '<div style="height:1.71em;"></div>';
|
||||
html += '<div class="sessionUserName">';
|
||||
html += DashboardPage.getUsersHtml(session);
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
parentElement.insertAdjacentHTML('beforeend', html);
|
||||
const deadSessionElem = parentElement.querySelector('.deadSession');
|
||||
|
||||
if (deadSessionElem) {
|
||||
deadSessionElem.parentNode.removeChild(deadSessionElem);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRunningTasks(view, tasks) {
|
||||
let html = '';
|
||||
tasks = tasks.filter(function (task) {
|
||||
if (task.State != 'Idle') {
|
||||
return !task.IsHidden;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (tasks.length) {
|
||||
view.querySelector('.runningTasksContainer').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.runningTasksContainer').classList.add('hide');
|
||||
}
|
||||
|
||||
for (let i = 0, length = tasks.length; i < length; i++) {
|
||||
const task = tasks[i];
|
||||
html += '<p>';
|
||||
html += task.Name + '<br/>';
|
||||
|
||||
if (task.State === 'Running') {
|
||||
const progress = (task.CurrentProgressPercentage || 0).toFixed(1);
|
||||
html += '<progress max="100" value="' + progress + '" title="' + progress + '%">';
|
||||
html += progress + '%';
|
||||
html += '</progress>';
|
||||
html += "<span style='color:#00a4dc;margin-left:5px;margin-right:5px;'>" + progress + '%</span>';
|
||||
html += '<button type="button" is="paper-icon-button-light" title="' + globalize.translate('ButtonStop') + '" onclick="DashboardPage.stopTask(this, \'' + task.Id + '\');" class="autoSize"><span class="material-icons cancel" aria-hidden="true"></span></button>';
|
||||
} else if (task.State === 'Cancelling') {
|
||||
html += '<span style="color:#cc0000;">' + globalize.translate('LabelStopping') + '</span>';
|
||||
}
|
||||
|
||||
html += '</p>';
|
||||
}
|
||||
|
||||
view.querySelector('#divRunningTasks').innerHTML = html;
|
||||
}
|
||||
|
||||
window.DashboardPage = {
|
||||
startInterval: function (apiClient) {
|
||||
apiClient.sendMessage('SessionsStart', '0,1500');
|
||||
apiClient.sendMessage('ScheduledTasksInfoStart', '0,1000');
|
||||
},
|
||||
stopInterval: function (apiClient) {
|
||||
apiClient.sendMessage('SessionsStop');
|
||||
apiClient.sendMessage('ScheduledTasksInfoStop');
|
||||
},
|
||||
getSessionNowPlayingStreamInfo: function (session) {
|
||||
let html = '';
|
||||
let showTranscodingInfo = false;
|
||||
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
|
||||
|
||||
if (displayPlayMethod === 'DirectPlay') {
|
||||
html += globalize.translate('DirectPlaying');
|
||||
} else if (displayPlayMethod === 'Remux') {
|
||||
html += globalize.translate('Remuxing');
|
||||
} else if (displayPlayMethod === 'DirectStream') {
|
||||
html += globalize.translate('DirectStreaming');
|
||||
} else if (displayPlayMethod === 'Transcode') {
|
||||
if (session.TranscodingInfo?.Framerate) {
|
||||
html += `${globalize.translate('Framerate')}: ${session.TranscodingInfo.Framerate}fps`;
|
||||
}
|
||||
|
||||
showTranscodingInfo = true;
|
||||
}
|
||||
|
||||
if (showTranscodingInfo) {
|
||||
const line = [];
|
||||
|
||||
if (session.TranscodingInfo) {
|
||||
if (session.TranscodingInfo.Bitrate) {
|
||||
if (session.TranscodingInfo.Bitrate > 1e6) {
|
||||
line.push((session.TranscodingInfo.Bitrate / 1e6).toFixed(1) + ' Mbps');
|
||||
} else {
|
||||
line.push(Math.floor(session.TranscodingInfo.Bitrate / 1e3) + ' Kbps');
|
||||
}
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo.Container) {
|
||||
line.push(session.TranscodingInfo.Container.toUpperCase());
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo.VideoCodec) {
|
||||
line.push(session.TranscodingInfo.VideoCodec.toUpperCase());
|
||||
}
|
||||
|
||||
if (session.TranscodingInfo.AudioCodec && session.TranscodingInfo.AudioCodec != session.TranscodingInfo.Container) {
|
||||
line.push(session.TranscodingInfo.AudioCodec.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
if (line.length) {
|
||||
html += '<br/><br/>' + line.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
getSessionNowPlayingTime: function (session) {
|
||||
const nowPlayingItem = session.NowPlayingItem;
|
||||
let html = '';
|
||||
|
||||
if (nowPlayingItem) {
|
||||
if (session.PlayState.PositionTicks) {
|
||||
html += datetime.getDisplayRunningTime(session.PlayState.PositionTicks);
|
||||
} else {
|
||||
html += '0:00';
|
||||
}
|
||||
|
||||
html += ' / ';
|
||||
|
||||
if (nowPlayingItem.RunTimeTicks) {
|
||||
html += datetime.getDisplayRunningTime(nowPlayingItem.RunTimeTicks);
|
||||
} else {
|
||||
html += '0:00';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
},
|
||||
getAppSecondaryText: function (session) {
|
||||
return session.Client + ' ' + session.ApplicationVersion;
|
||||
},
|
||||
getNowPlayingName: function (session) {
|
||||
let imgUrl = '';
|
||||
const nowPlayingItem = session.NowPlayingItem;
|
||||
// FIXME: It seems that, sometimes, server sends date in the future, so date-fns displays messages like 'in less than a minute'. We should fix
|
||||
// how dates are returned by the server when the session is active and show something like 'Active now', instead of past/future sentences
|
||||
if (!nowPlayingItem) {
|
||||
return {
|
||||
html: globalize.translate('LastSeen', formatDistanceToNow(Date.parse(session.LastActivityDate), getLocaleWithSuffix())),
|
||||
image: imgUrl
|
||||
};
|
||||
}
|
||||
|
||||
let topText = escapeHtml(itemHelper.getDisplayName(nowPlayingItem));
|
||||
let bottomText = '';
|
||||
|
||||
if (nowPlayingItem.Artists?.length) {
|
||||
bottomText = topText;
|
||||
topText = escapeHtml(nowPlayingItem.Artists[0]);
|
||||
} else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) {
|
||||
bottomText = topText;
|
||||
topText = escapeHtml(nowPlayingItem.SeriesName || nowPlayingItem.Album);
|
||||
} else if (nowPlayingItem.ProductionYear) {
|
||||
bottomText = nowPlayingItem.ProductionYear;
|
||||
}
|
||||
|
||||
if (nowPlayingItem.ImageTags?.Logo) {
|
||||
imgUrl = ApiClient.getScaledImageUrl(nowPlayingItem.Id, {
|
||||
tag: nowPlayingItem.ImageTags.Logo,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
} else if (nowPlayingItem.ParentLogoImageTag) {
|
||||
imgUrl = ApiClient.getScaledImageUrl(nowPlayingItem.ParentLogoItemId, {
|
||||
tag: nowPlayingItem.ParentLogoImageTag,
|
||||
maxHeight: 24,
|
||||
maxWidth: 130,
|
||||
type: 'Logo'
|
||||
});
|
||||
}
|
||||
|
||||
if (imgUrl) {
|
||||
topText = '<img src="' + imgUrl + '" style="max-height:24px;max-width:130px;" />';
|
||||
}
|
||||
|
||||
return {
|
||||
html: bottomText ? topText + '<br/>' + bottomText : topText,
|
||||
image: imgUrl
|
||||
};
|
||||
},
|
||||
getUsersHtml: function (session) {
|
||||
const html = [];
|
||||
|
||||
if (session.UserId) {
|
||||
html.push(escapeHtml(session.UserName));
|
||||
}
|
||||
|
||||
for (let i = 0, length = session.AdditionalUsers.length; i < length; i++) {
|
||||
html.push(escapeHtml(session.AdditionalUsers[i].UserName));
|
||||
}
|
||||
|
||||
return html.join(', ');
|
||||
},
|
||||
getUserImage: function (session) {
|
||||
if (session.UserId && session.UserPrimaryImageTag) {
|
||||
return ApiClient.getUserImageUrl(session.UserId, {
|
||||
tag: session.UserPrimaryImageTag,
|
||||
type: 'Primary'
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
updateSession: function (row, session) {
|
||||
row.classList.remove('deadSession');
|
||||
const nowPlayingItem = session.NowPlayingItem;
|
||||
|
||||
if (nowPlayingItem) {
|
||||
row.classList.add('playingSession');
|
||||
row.querySelector('.btnSessionInfo').classList.remove('hide');
|
||||
} else {
|
||||
row.classList.remove('playingSession');
|
||||
row.querySelector('.btnSessionInfo').classList.add('hide');
|
||||
}
|
||||
|
||||
if (session.ServerId && session.SupportedCommands.indexOf('DisplayMessage') !== -1) {
|
||||
row.querySelector('.btnSessionSendMessage').classList.remove('hide');
|
||||
} else {
|
||||
row.querySelector('.btnSessionSendMessage').classList.add('hide');
|
||||
}
|
||||
|
||||
const btnSessionPlayPause = row.querySelector('.btnSessionPlayPause');
|
||||
|
||||
if (session.ServerId && nowPlayingItem && session.SupportsRemoteControl) {
|
||||
btnSessionPlayPause.classList.remove('hide');
|
||||
row.querySelector('.btnSessionStop').classList.remove('hide');
|
||||
} else {
|
||||
btnSessionPlayPause.classList.add('hide');
|
||||
row.querySelector('.btnSessionStop').classList.add('hide');
|
||||
}
|
||||
|
||||
const btnSessionPlayPauseIcon = btnSessionPlayPause.querySelector('.material-icons');
|
||||
btnSessionPlayPauseIcon.classList.remove('play_arrow', 'pause');
|
||||
btnSessionPlayPauseIcon.classList.add(session.PlayState?.IsPaused ? 'play_arrow' : 'pause');
|
||||
|
||||
row.querySelector('.sessionNowPlayingTime').innerText = DashboardPage.getSessionNowPlayingTime(session);
|
||||
row.querySelector('.sessionUserName').innerHTML = DashboardPage.getUsersHtml(session);
|
||||
row.querySelector('.sessionAppSecondaryText').innerText = DashboardPage.getAppSecondaryText(session);
|
||||
const nowPlayingName = DashboardPage.getNowPlayingName(session);
|
||||
const nowPlayingInfoElem = row.querySelector('.sessionNowPlayingInfo');
|
||||
|
||||
if (!(nowPlayingName.image && nowPlayingName.image == nowPlayingInfoElem.getAttribute('data-imgsrc'))) {
|
||||
nowPlayingInfoElem.innerHTML = nowPlayingName.html;
|
||||
nowPlayingInfoElem.setAttribute('data-imgsrc', nowPlayingName.image || '');
|
||||
}
|
||||
|
||||
const playbackProgressElem = row.querySelector('.playbackProgress');
|
||||
const transcodingProgress = row.querySelector('.transcodingProgress');
|
||||
|
||||
let percent = 100 * session?.PlayState?.PositionTicks / nowPlayingItem?.RunTimeTicks;
|
||||
playbackProgressElem.outerHTML = indicators.getProgressHtml(percent || 0, {
|
||||
containerClass: 'playbackProgress'
|
||||
});
|
||||
|
||||
percent = session?.TranscodingInfo?.CompletionPercentage?.toFixed(1);
|
||||
transcodingProgress.outerHTML = indicators.getProgressHtml(percent || 0, {
|
||||
containerClass: 'transcodingProgress'
|
||||
});
|
||||
|
||||
const imgUrl = DashboardPage.getNowPlayingImageUrl(nowPlayingItem) || '';
|
||||
const imgElem = row.querySelector('.sessionNowPlayingContent');
|
||||
|
||||
if (imgUrl != imgElem.getAttribute('data-src')) {
|
||||
imgElem.style.backgroundImage = imgUrl ? "url('" + imgUrl + "')" : '';
|
||||
imgElem.setAttribute('data-src', imgUrl);
|
||||
|
||||
if (imgUrl) {
|
||||
imgElem.classList.add('sessionNowPlayingContent-withbackground');
|
||||
row.querySelector('.sessionNowPlayingInnerContent').classList.add('darkenContent');
|
||||
} else {
|
||||
imgElem.classList.remove('sessionNowPlayingContent-withbackground');
|
||||
row.querySelector('.sessionNowPlayingInnerContent').classList.remove('darkenContent');
|
||||
}
|
||||
}
|
||||
},
|
||||
getClientImage: function (connection) {
|
||||
const iconUrl = imageHelper.getDeviceIcon(connection);
|
||||
return "<img src='" + iconUrl + "' />";
|
||||
},
|
||||
getNowPlayingImageUrl: function (item) {
|
||||
/* Screen width is multiplied by 0.2, as the there is currently no way to get the width of
|
||||
elements that aren't created yet. */
|
||||
if (item?.BackdropImageTags?.length) {
|
||||
return ApiClient.getScaledImageUrl(item.Id, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Backdrop',
|
||||
tag: item.BackdropImageTags[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.ParentBackdropImageTags?.length) {
|
||||
return ApiClient.getScaledImageUrl(item.ParentBackdropItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Backdrop',
|
||||
tag: item.ParentBackdropImageTags[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.BackdropImageTag) {
|
||||
return ApiClient.getScaledImageUrl(item.BackdropItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Backdrop',
|
||||
tag: item.BackdropImageTag
|
||||
});
|
||||
}
|
||||
|
||||
const imageTags = item?.ImageTags || {};
|
||||
|
||||
if (item && imageTags.Thumb) {
|
||||
return ApiClient.getScaledImageUrl(item.Id, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Thumb',
|
||||
tag: imageTags.Thumb
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.ParentThumbImageTag) {
|
||||
return ApiClient.getScaledImageUrl(item.ParentThumbItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Thumb',
|
||||
tag: item.ParentThumbImageTag
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.ThumbImageTag) {
|
||||
return ApiClient.getScaledImageUrl(item.ThumbItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Thumb',
|
||||
tag: item.ThumbImageTag
|
||||
});
|
||||
}
|
||||
|
||||
if (item && imageTags.Primary) {
|
||||
return ApiClient.getScaledImageUrl(item.Id, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Primary',
|
||||
tag: imageTags.Primary
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.PrimaryImageTag) {
|
||||
return ApiClient.getScaledImageUrl(item.PrimaryImageItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Primary',
|
||||
tag: item.PrimaryImageTag
|
||||
});
|
||||
}
|
||||
|
||||
if (item?.AlbumPrimaryImageTag) {
|
||||
return ApiClient.getScaledImageUrl(item.AlbumId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.20),
|
||||
type: 'Primary',
|
||||
tag: item.AlbumPrimaryImageTag
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
systemUpdateTaskKey: 'SystemUpdateTask',
|
||||
stopTask: function (btn, id) {
|
||||
const page = dom.parentWithClass(btn, 'page');
|
||||
ApiClient.stopScheduledTask(id).then(function () {
|
||||
pollForInfo(page, ApiClient);
|
||||
});
|
||||
},
|
||||
restart: function (btn) {
|
||||
confirm({
|
||||
title: globalize.translate('Restart'),
|
||||
text: globalize.translate('MessageConfirmRestart'),
|
||||
confirmText: globalize.translate('Restart'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const page = dom.parentWithClass(btn, 'page');
|
||||
page.querySelector('#btnRestartServer').disabled = true;
|
||||
page.querySelector('#btnShutdown').disabled = true;
|
||||
ApiClient.restartServer();
|
||||
});
|
||||
},
|
||||
shutdown: function (btn) {
|
||||
confirm({
|
||||
title: globalize.translate('ButtonShutdown'),
|
||||
text: globalize.translate('MessageConfirmShutdown'),
|
||||
confirmText: globalize.translate('ButtonShutdown'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const page = dom.parentWithClass(btn, 'page');
|
||||
page.querySelector('#btnRestartServer').disabled = true;
|
||||
page.querySelector('#btnShutdown').disabled = true;
|
||||
ApiClient.shutdownServer();
|
||||
});
|
||||
}
|
||||
};
|
||||
export default function (view) {
|
||||
function onRestartRequired(evt, apiClient) {
|
||||
console.debug('onRestartRequired not implemented', evt, apiClient);
|
||||
}
|
||||
|
||||
function onServerShuttingDown(evt, apiClient) {
|
||||
console.debug('onServerShuttingDown not implemented', evt, apiClient);
|
||||
}
|
||||
|
||||
function onServerRestarting(evt, apiClient) {
|
||||
console.debug('onServerRestarting not implemented', evt, apiClient);
|
||||
}
|
||||
|
||||
function onPackageInstall(_, apiClient) {
|
||||
if (apiClient.serverId() === serverId) {
|
||||
pollForInfo(view, apiClient);
|
||||
reloadSystemInfo(view, apiClient);
|
||||
}
|
||||
}
|
||||
|
||||
function onSessionsUpdate(evt, apiClient, info) {
|
||||
if (apiClient.serverId() === serverId) {
|
||||
renderInfo(view, info);
|
||||
}
|
||||
}
|
||||
|
||||
function onScheduledTasksUpdate(evt, apiClient, info) {
|
||||
if (apiClient.serverId() === serverId) {
|
||||
renderRunningTasks(view, info);
|
||||
}
|
||||
}
|
||||
|
||||
const serverId = ApiClient.serverId();
|
||||
view.querySelector('.activeDevices').addEventListener('click', onActiveDevicesClick);
|
||||
view.addEventListener('viewshow', function () {
|
||||
const page = this;
|
||||
const apiClient = ApiClient;
|
||||
|
||||
if (apiClient) {
|
||||
loading.show();
|
||||
pollForInfo(page, apiClient);
|
||||
DashboardPage.startInterval(apiClient);
|
||||
Events.on(serverNotifications, 'RestartRequired', onRestartRequired);
|
||||
Events.on(serverNotifications, 'ServerShuttingDown', onServerShuttingDown);
|
||||
Events.on(serverNotifications, 'ServerRestarting', onServerRestarting);
|
||||
Events.on(serverNotifications, 'PackageInstalling', onPackageInstall);
|
||||
Events.on(serverNotifications, 'PackageInstallationCompleted', onPackageInstall);
|
||||
Events.on(serverNotifications, 'Sessions', onSessionsUpdate);
|
||||
Events.on(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
DashboardPage.lastAppUpdateCheck = null;
|
||||
reloadSystemInfo(page, ApiClient);
|
||||
|
||||
if (!page.userActivityLog) {
|
||||
page.userActivityLog = new ActivityLog({
|
||||
serverId: ApiClient.serverId(),
|
||||
element: page.querySelector('.userActivityItems')
|
||||
});
|
||||
}
|
||||
|
||||
if (!page.serverActivityLog) {
|
||||
page.serverActivityLog = new ActivityLog({
|
||||
serverId: ApiClient.serverId(),
|
||||
element: page.querySelector('.serverActivityItems')
|
||||
});
|
||||
}
|
||||
|
||||
refreshActiveRecordings(view, apiClient);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
view.addEventListener('viewbeforehide', function () {
|
||||
const apiClient = ApiClient;
|
||||
const page = this;
|
||||
|
||||
Events.off(serverNotifications, 'RestartRequired', onRestartRequired);
|
||||
Events.off(serverNotifications, 'ServerShuttingDown', onServerShuttingDown);
|
||||
Events.off(serverNotifications, 'ServerRestarting', onServerRestarting);
|
||||
Events.off(serverNotifications, 'PackageInstalling', onPackageInstall);
|
||||
Events.off(serverNotifications, 'PackageInstallationCompleted', onPackageInstall);
|
||||
Events.off(serverNotifications, 'Sessions', onSessionsUpdate);
|
||||
Events.off(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
|
||||
if (apiClient) {
|
||||
DashboardPage.stopInterval(apiClient);
|
||||
}
|
||||
|
||||
taskButton({
|
||||
mode: 'off',
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
view.addEventListener('viewdestroy', function () {
|
||||
const page = this;
|
||||
const userActivityLog = page.userActivityLog;
|
||||
|
||||
if (userActivityLog) {
|
||||
userActivityLog.destroy();
|
||||
}
|
||||
|
||||
const serverActivityLog = page.serverActivityLog;
|
||||
|
||||
if (serverActivityLog) {
|
||||
serverActivityLog.destroy();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
17
src/apps/dashboard/controllers/dashboard.scss
Normal file
17
src/apps/dashboard/controllers/dashboard.scss
Normal file
|
@ -0,0 +1,17 @@
|
|||
.serverInfo {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1em;
|
||||
padding: 1em;
|
||||
|
||||
> *:nth-child(odd) {
|
||||
flex: 1 0 20%;
|
||||
min-width: 7.5em;
|
||||
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> *:nth-child(even) {
|
||||
flex: 1 0 70%;
|
||||
}
|
||||
}
|
23
src/apps/dashboard/controllers/devices/device.html
Normal file
23
src/apps/dashboard/controllers/devices/device.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<div id="devicePage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="deviceForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle reportedName"></h2>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtCustomName" label="${LabelDisplayName}" />
|
||||
<div class="fieldDescription">${LabelCustomDeviceDisplayNameHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
54
src/apps/dashboard/controllers/devices/device.js
Normal file
54
src/apps/dashboard/controllers/devices/device.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url.ts';
|
||||
|
||||
function load(page, device, deviceOptions) {
|
||||
page.querySelector('#txtCustomName', page).value = deviceOptions?.CustomName || '';
|
||||
page.querySelector('.reportedName', page).innerText = device.Name || '';
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
const page = this;
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
const device = ApiClient.getJSON(ApiClient.getUrl('Devices/Info', {
|
||||
Id: id
|
||||
}));
|
||||
const deviceOptions = ApiClient.getJSON(ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
})).catch(() => undefined);
|
||||
Promise.all([device, deviceOptions]).then(function (responses) {
|
||||
load(page, responses[0], responses[1]);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function save(page) {
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.ajax({
|
||||
url: ApiClient.getUrl('Devices/Options', {
|
||||
Id: id
|
||||
}),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
CustomName: page.querySelector('#txtCustomName').value
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
}
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
save(dom.parentWithClass(form, 'page'));
|
||||
e.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
view.querySelector('form').addEventListener('submit', onSubmit);
|
||||
view.addEventListener('viewshow', loadData);
|
||||
}
|
||||
|
21
src/apps/dashboard/controllers/devices/devices.html
Normal file
21
src/apps/dashboard/controllers/devices/devices.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div id="devicesPage" data-role="page" class="page type-interior devicesPage noSecondaryNavPage" data-title="${HeaderDevices}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection">
|
||||
<div class="sectionTitleContainer sectionTitleContainer-cards flex align-items-center">
|
||||
<h2 class="sectionTitle sectionTitle-cards">${HeaderDevices}</h2>
|
||||
<button
|
||||
id="deviceDeleteAll"
|
||||
is="emby-button"
|
||||
type="button"
|
||||
class="raised button-alt"
|
||||
style="margin-left: 1.25em !important; padding-bottom: 0.4em !important; padding-top: 0.4em !important;"
|
||||
>
|
||||
${DeleteAll}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div is="emby-itemscontainer" class="devicesList vertical-wrap" data-multiselect="false"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
171
src/apps/dashboard/controllers/devices/devices.js
Normal file
171
src/apps/dashboard/controllers/devices/devices.js
Normal file
|
@ -0,0 +1,171 @@
|
|||
import { formatDistanceToNow } from 'date-fns';
|
||||
import escapeHtml from 'escape-html';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import imageHelper from 'utils/image';
|
||||
import { getLocaleWithSuffix } from 'utils/dateFnsLocale.ts';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
// Local cache of loaded
|
||||
let deviceIds = [];
|
||||
|
||||
function canDelete(deviceId) {
|
||||
return deviceId !== ApiClient.deviceId();
|
||||
}
|
||||
|
||||
function deleteAllDevices(page) {
|
||||
const msg = globalize.translate('DeleteDevicesConfirmation');
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevices'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await Promise.all(
|
||||
deviceIds.filter(canDelete).map((id) => ApiClient.deleteDevice(id))
|
||||
);
|
||||
loadData(page);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteDevice(page, id) {
|
||||
const msg = globalize.translate('DeleteDeviceConfirmation');
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderDeleteDevice'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(async () => {
|
||||
loading.show();
|
||||
await ApiClient.deleteDevice(id);
|
||||
loadData(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showDeviceMenu(view, btn, deviceId) {
|
||||
const menuItems = [{
|
||||
name: globalize.translate('Edit'),
|
||||
id: 'open',
|
||||
icon: 'mode_edit'
|
||||
}];
|
||||
|
||||
if (canDelete(deviceId)) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: btn,
|
||||
callback: function (id) {
|
||||
switch (id) {
|
||||
case 'open':
|
||||
Dashboard.navigate('dashboard/devices/edit?id=' + deviceId);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteDevice(view, deviceId);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function load(page, devices) {
|
||||
const localeWithSuffix = getLocaleWithSuffix();
|
||||
|
||||
let html = '';
|
||||
html += devices.map(function (device) {
|
||||
let deviceHtml = '';
|
||||
deviceHtml += "<div data-id='" + escapeHtml(device.Id) + "' class='card backdropCard'>";
|
||||
deviceHtml += '<div class="cardBox visualCardBox">';
|
||||
deviceHtml += '<div class="cardScalable">';
|
||||
deviceHtml += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
deviceHtml += `<a is="emby-linkbutton" href="#/dashboard/devices/edit?id=${escapeHtml(device.Id)}" class="cardContent cardImageContainer ${getDefaultBackgroundClass()}">`;
|
||||
// audit note: getDeviceIcon returns static text
|
||||
const iconUrl = imageHelper.getDeviceIcon(device);
|
||||
|
||||
if (iconUrl) {
|
||||
deviceHtml += '<div class="cardImage" style="background-image:url(\'' + iconUrl + "');background-size:contain;background-position:center center;background-origin:content-box;padding:1em;\">";
|
||||
deviceHtml += '</div>';
|
||||
} else {
|
||||
deviceHtml += '<span class="cardImageIcon material-icons tablet_android" aria-hidden="true"></span>';
|
||||
}
|
||||
|
||||
deviceHtml += '</a>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '<div class="cardFooter">';
|
||||
|
||||
if (canDelete(device.Id)) {
|
||||
if (globalize.getIsRTL()) {
|
||||
deviceHtml += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||
} else {
|
||||
deviceHtml += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
}
|
||||
deviceHtml += '<button type="button" is="paper-icon-button-light" data-id="' + escapeHtml(device.Id) + '" title="' + globalize.translate('Menu') + '" class="btnDeviceMenu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
deviceHtml += '</div>';
|
||||
}
|
||||
|
||||
deviceHtml += "<div class='cardText'>";
|
||||
deviceHtml += escapeHtml(device.CustomName || device.Name);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
deviceHtml += escapeHtml(device.AppName + ' ' + device.AppVersion);
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += "<div class='cardText cardText-secondary'>";
|
||||
|
||||
if (device.LastUserName) {
|
||||
deviceHtml += escapeHtml(device.LastUserName);
|
||||
deviceHtml += ', ' + formatDistanceToNow(Date.parse(device.DateLastActivity), localeWithSuffix);
|
||||
}
|
||||
|
||||
deviceHtml += ' ';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
deviceHtml += '</div>';
|
||||
return deviceHtml;
|
||||
}).join('');
|
||||
page.querySelector('.devicesList').innerHTML = html;
|
||||
}
|
||||
|
||||
function loadData(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Devices')).then(function (result) {
|
||||
load(page, result.Items);
|
||||
deviceIds = result.Items.map((device) => device.Id);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
view.querySelector('.devicesList').addEventListener('click', function (e) {
|
||||
const btnDeviceMenu = dom.parentWithClass(e.target, 'btnDeviceMenu');
|
||||
|
||||
if (btnDeviceMenu) {
|
||||
showDeviceMenu(view, btnDeviceMenu, btnDeviceMenu.getAttribute('data-id'));
|
||||
}
|
||||
});
|
||||
view.addEventListener('viewshow', function () {
|
||||
loadData(this);
|
||||
});
|
||||
|
||||
view.querySelector('#deviceDeleteAll').addEventListener('click', function() {
|
||||
deleteAllDevices(view);
|
||||
});
|
||||
}
|
||||
|
407
src/apps/dashboard/controllers/encodingsettings.html
Normal file
407
src/apps/dashboard/controllers/encodingsettings.html
Normal file
|
@ -0,0 +1,407 @@
|
|||
<div id="encodingSettingsPage" data-role="page" class="page type-interior playbackConfigurationPage" data-title="${TitlePlayback}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="encodingSettingsForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${Transcoding}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectVideoDecoder" label="${LabelHardwareAccelerationType}">
|
||||
<option value="none">${None}</option>
|
||||
<option value="amf">AMD AMF</option>
|
||||
<option value="nvenc">Nvidia NVENC</option>
|
||||
<option value="qsv">Intel QuickSync (QSV)</option>
|
||||
<option value="vaapi">Video Acceleration API (VAAPI)</option>
|
||||
<option value="rkmpp">Rockchip MPP (RKMPP)</option>
|
||||
<option value="videotoolbox">Apple VideoToolBox</option>
|
||||
<option value="v4l2m2m">Video4Linux2 (V4L2)</option>
|
||||
</select>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration" target="_blank">${LabelHardwareAccelerationTypeHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer hide fldVaapiDevice">
|
||||
<input is="emby-input" type="text" id="txtVaapiDevice" label="${LabelVaapiDevice}" />
|
||||
<div class="fieldDescription">${LabelVaapiDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer hide fldQsvDevice">
|
||||
<input is="emby-input" type="text" id="txtQsvDevice" label="${LabelQsvDevice}" />
|
||||
<div class="fieldDescription">${LabelQsvDeviceHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="hardwareAccelerationOptions hide">
|
||||
<div class="checkboxListContainer decodingCodecsList">
|
||||
<h3 class="checkboxListLabel">${LabelEnableHardwareDecodingFor}</h3>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="h264" data-types="amf,nvenc,qsv,vaapi,rkmpp,videotoolbox,v4l2m2m" />
|
||||
<span>H264</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="hevc" data-types="amf,nvenc,qsv,vaapi,rkmpp,videotoolbox" />
|
||||
<span>HEVC</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg1video" data-types="rkmpp" />
|
||||
<span>MPEG1</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg2video" data-types="amf,nvenc,qsv,vaapi,rkmpp" />
|
||||
<span>MPEG2</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="mpeg4" data-types="nvenc,rkmpp" />
|
||||
<span>MPEG4</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vc1" data-types="amf,nvenc,qsv,vaapi" />
|
||||
<span>VC1</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp8" data-types="nvenc,qsv,vaapi,rkmpp,videotoolbox" />
|
||||
<span>VP8</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="vp9" data-types="amf,nvenc,qsv,vaapi,rkmpp,videotoolbox" />
|
||||
<span>VP9</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDecodeCodec" data-codec="av1" data-types="amf,nvenc,qsv,vaapi,rkmpp" />
|
||||
<span>AV1</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fld10bitHevcVp9HwDecoding">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Hevc" />
|
||||
<span>HEVC 10bit</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10Vp9" />
|
||||
<span>VP9 10bit</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fldHevcRextHwDecoding">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth10HevcRext" />
|
||||
<span>HEVC RExt 8/10bit</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkDecodingColorDepth12HevcRext" />
|
||||
<span>HEVC RExt 12bit</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer hide fldEnhancedNvdec">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnhancedNvdecDecoder" />
|
||||
<span>${EnableEnhancedNvdecDecoder}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${EnableEnhancedNvdecDecoderHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer hide fldSysNativeHwDecoder">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSystemNativeHwDecoder" />
|
||||
<span>${PreferSystemNativeHwDecoder}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer">
|
||||
<h3 class="checkboxListLabel">${LabelHardwareEncodingOptions}</h3>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkHardwareEncoding" />
|
||||
<span>${EnableHardwareEncoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList hide fldIntelLp">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIntelLpH264HwEncoder" />
|
||||
<span>${EnableIntelLowPowerH264HwEncoder}</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkIntelLpHevcHwEncoder" />
|
||||
<span>${EnableIntelLowPowerHevcHwEncoder}</span>
|
||||
</label>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/hardware-acceleration/intel#configure-and-verify-lp-mode-on-linux" target="_blank">${IntelLowPowerEncHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxListContainer">
|
||||
<h3 class="checkboxListLabel">${LabelEncodingFormatOptions}</h3>
|
||||
<div class="fieldDescription">${EncodingFormatHelp}</div>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkAllowHevcEncoding" />
|
||||
<span>${AllowHevcEncoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkboxList">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkAllowAv1Encoding" />
|
||||
<span>${AllowAv1Encoding}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="vppTonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkVppTonemapping" />
|
||||
<span>${EnableVppTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowVppTonemappingHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtVppTonemappingBrightness" pattern="[0-9]*" min="0" max="100" step=".00001" label="${LabelVppTonemappingBrightness}" />
|
||||
<div class="fieldDescription">${LabelVppTonemappingBrightnessHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtVppTonemappingContrast" pattern="[0-9]*" min="1" max="2" step=".00001" label="${LabelVppTonemappingContrast}" />
|
||||
<div class="fieldDescription">${LabelVppTonemappingContrastHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="videoToolboxTonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkVideoToolboxTonemapping" />
|
||||
<span>${EnableVideoToolboxTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowVideoToolboxTonemappingHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tonemappingOptions hide">
|
||||
<div class="checkboxListContainer checkboxContainer-withDescription fldTonemapCheckbox hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkTonemapping" />
|
||||
<span>${EnableTonemapping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription allowTonemappingHardwareHelp">${AllowTonemappingHelp}</div>
|
||||
<div class="fieldDescription checkboxFieldDescription allowTonemappingSoftwareHelp">${AllowTonemappingSoftwareHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingAlgorithm" label="${LabelTonemappingAlgorithm}">
|
||||
<option value="none">${None}</option>
|
||||
<option value="clip">Clip</option>
|
||||
<option value="linear">Linear</option>
|
||||
<option value="gamma">Gamma</option>
|
||||
<option value="reinhard">Reinhard</option>
|
||||
<option value="hable">Hable</option>
|
||||
<option value="mobius">Mobius</option>
|
||||
<option value="bt2390">BT.2390</option>
|
||||
</select>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="http://ffmpeg.org/ffmpeg-all.html#tonemap_005fopencl" target="_blank">${TonemappingAlgorithmHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tonemappingModeOptions selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingMode" label="${LabelTonemappingMode}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="max">MAX</option>
|
||||
<option value="rgb">RGB</option>
|
||||
<option value="lum">LUM</option>
|
||||
<option value="itp">ITP</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${TonemappingModeHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTonemappingRange" label="${LabelTonemappingRange}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="tv">TV</option>
|
||||
<option value="pc">PC</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${TonemappingRangeHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingDesat" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingDesat}" />
|
||||
<div class="fieldDescription">${LabelTonemappingDesatHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingPeak" pattern="[0-9]*" min="0" max="1.79769e+308" step=".00001" label="${LabelTonemappingPeak}" />
|
||||
<div class="fieldDescription">${LabelTonemappingPeakHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtTonemappingParam" pattern="[0-9]*" min="2.22507e-308" max="1.79769e+308" step=".00001" label="${LabelTonemappingParam}" />
|
||||
<div class="fieldDescription">${LabelTonemappingParamHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectThreadCount" label="${LabelTranscodingThreadCount}">
|
||||
<option value="-1">${Auto}</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
<option value="15">15</option>
|
||||
<option value="16">16</option>
|
||||
<option value="0">${OptionMax}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${LabelTranscodingThreadCountHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldEncoderPath">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" class="txtEncoderPath" label="${LabelffmpegPath}" autocomplete="off" dir="ltr" disabled/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fieldDescription">
|
||||
<div>${LabelffmpegPathHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtTranscodingTempPath" label="${LabelTranscodePath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectTranscodingTempPath" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelTranscodingTempPathHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtFallbackFontPath" label="${LabelFallbackFontPath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectFallbackFontPath" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/administration/configuration#fonts" target="_blank">${LabelFallbackFontPathHelp}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableFallbackFont" />
|
||||
<span>${EnableFallbackFont}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${EnableFallbackFontHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableAudioVbr" />
|
||||
<span>${LabelEnableAudioVbr}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableAudioVbrHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtDownMixAudioBoost" pattern="[0-9]*" required="required" min=".5" max="3" step=".1" label="${LabelDownMixAudioScale}" />
|
||||
<div class="fieldDescription">${LabelDownMixAudioScaleHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectStereoDownmixAlgorithm" label="${LabelStereoDownmixAlgorithm}">
|
||||
<option value="None">${None}</option>
|
||||
<option value="Dave750">Dave750</option>
|
||||
<option value="NightmodeDialogue">NightmodeDialogue</option>
|
||||
<option value="Rfc7845">RFC7845</option>
|
||||
<option value="Ac4">AC-4</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${StereoDownmixAlgorithmHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtMaxMuxingQueueSize" pattern="[0-9]*" required="required" min="128" max="2147483647" step="1" label="${LabelMaxMuxingQueueSize}" />
|
||||
<div class="fieldDescription">${LabelMaxMuxingQueueSizeHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectEncoderPreset" label="${LabelEncoderPreset}">
|
||||
<option value="auto">${Auto}</option>
|
||||
<option value="veryslow">veryslow</option>
|
||||
<option value="slower">slower</option>
|
||||
<option value="slow">slow</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="fast">fast</option>
|
||||
<option value="faster">faster</option>
|
||||
<option value="veryfast">veryfast</option>
|
||||
<option value="superfast">superfast</option>
|
||||
<option value="ultrafast">ultrafast</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${EncoderPresetHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtH265Crf" pattern="[0-9]*" min="0" max="51" step="1" label="${LabelH265Crf}" />
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtH264Crf" pattern="[0-9]*" min="0" max="51" step="1" label="${LabelH264Crf}" />
|
||||
<div class="fieldDescription">${H264CrfHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectDeinterlaceMethod" label="${LabelDeinterlaceMethod}">
|
||||
<option value="yadif">${Yadif}</option>
|
||||
<option value="bwdif">${Bwdif}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${DeinterlaceMethodHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkDoubleRateDeinterlacing" />
|
||||
<span>${UseDoubleRateDeinterlacing}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${UseDoubleRateDeinterlacingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableSubtitleExtraction" />
|
||||
<span>${AllowOnTheFlySubtitleExtraction}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowOnTheFlySubtitleExtractionHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableThrottling" />
|
||||
<span>${AllowFfmpegThrottling}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowFfmpegThrottlingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkEnableSegmentDeletion" />
|
||||
<span>${AllowSegmentDeletion}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowSegmentDeletionHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtThrottleDelaySeconds" pattern="[0-9]*" min="10" max="3600" step="1" label="${LabelThrottleDelaySeconds}" />
|
||||
<div class="fieldDescription">${LabelThrottleDelaySecondsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtSegmentKeepSeconds" pattern="[0-9]*" min="15" max="3600" step="1" label="${LabelSegmentKeepSeconds}" />
|
||||
<div class="fieldDescription">${LabelSegmentKeepSecondsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
309
src/apps/dashboard/controllers/encodingsettings.js
Normal file
309
src/apps/dashboard/controllers/encodingsettings.js
Normal file
|
@ -0,0 +1,309 @@
|
|||
import 'jquery';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, systemInfo) {
|
||||
Array.prototype.forEach.call(page.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
c.checked = (config.HardwareDecodingCodecs || []).indexOf(c.getAttribute('data-codec')) !== -1;
|
||||
});
|
||||
page.querySelector('#chkDecodingColorDepth10Hevc').checked = config.EnableDecodingColorDepth10Hevc;
|
||||
page.querySelector('#chkDecodingColorDepth10Vp9').checked = config.EnableDecodingColorDepth10Vp9;
|
||||
page.querySelector('#chkDecodingColorDepth10HevcRext').checked = config.EnableDecodingColorDepth10HevcRext;
|
||||
page.querySelector('#chkDecodingColorDepth12HevcRext').checked = config.EnableDecodingColorDepth12HevcRext;
|
||||
page.querySelector('#chkEnhancedNvdecDecoder').checked = config.EnableEnhancedNvdecDecoder;
|
||||
page.querySelector('#chkSystemNativeHwDecoder').checked = config.PreferSystemNativeHwDecoder;
|
||||
page.querySelector('#chkIntelLpH264HwEncoder').checked = config.EnableIntelLowPowerH264HwEncoder;
|
||||
page.querySelector('#chkIntelLpHevcHwEncoder').checked = config.EnableIntelLowPowerHevcHwEncoder;
|
||||
page.querySelector('#chkHardwareEncoding').checked = config.EnableHardwareEncoding;
|
||||
page.querySelector('#chkAllowHevcEncoding').checked = config.AllowHevcEncoding;
|
||||
page.querySelector('#chkAllowAv1Encoding').checked = config.AllowAv1Encoding;
|
||||
page.querySelector('#selectVideoDecoder').value = config.HardwareAccelerationType || 'none';
|
||||
page.querySelector('#selectThreadCount').value = config.EncodingThreadCount;
|
||||
page.querySelector('#chkEnableAudioVbr').checked = config.EnableAudioVbr;
|
||||
page.querySelector('#txtDownMixAudioBoost').value = config.DownMixAudioBoost;
|
||||
page.querySelector('#selectStereoDownmixAlgorithm').value = config.DownMixStereoAlgorithm || 'None';
|
||||
page.querySelector('#txtMaxMuxingQueueSize').value = config.MaxMuxingQueueSize || '';
|
||||
page.querySelector('.txtEncoderPath').value = config.EncoderAppPathDisplay || '';
|
||||
page.querySelector('#txtTranscodingTempPath').value = systemInfo.TranscodingTempPath || '';
|
||||
page.querySelector('#txtFallbackFontPath').value = config.FallbackFontPath || '';
|
||||
page.querySelector('#chkEnableFallbackFont').checked = config.EnableFallbackFont;
|
||||
page.querySelector('#txtVaapiDevice').value = config.VaapiDevice || '';
|
||||
page.querySelector('#txtQsvDevice').value = config.QsvDevice || '';
|
||||
page.querySelector('#chkTonemapping').checked = config.EnableTonemapping;
|
||||
page.querySelector('#chkVppTonemapping').checked = config.EnableVppTonemapping;
|
||||
page.querySelector('#chkVideoToolboxTonemapping').checked = config.EnableVideoToolboxTonemapping;
|
||||
page.querySelector('#selectTonemappingAlgorithm').value = config.TonemappingAlgorithm || 'none';
|
||||
page.querySelector('#selectTonemappingMode').value = config.TonemappingMode || 'auto';
|
||||
page.querySelector('#selectTonemappingRange').value = config.TonemappingRange || 'auto';
|
||||
page.querySelector('#txtTonemappingDesat').value = config.TonemappingDesat;
|
||||
page.querySelector('#txtTonemappingPeak').value = config.TonemappingPeak;
|
||||
page.querySelector('#txtTonemappingParam').value = config.TonemappingParam || '';
|
||||
page.querySelector('#txtVppTonemappingBrightness').value = config.VppTonemappingBrightness;
|
||||
page.querySelector('#txtVppTonemappingContrast').value = config.VppTonemappingContrast;
|
||||
page.querySelector('#selectEncoderPreset').value = config.EncoderPreset || 'auto';
|
||||
page.querySelector('#txtH264Crf').value = config.H264Crf || '';
|
||||
page.querySelector('#txtH265Crf').value = config.H265Crf || '';
|
||||
page.querySelector('#selectDeinterlaceMethod').value = config.DeinterlaceMethod || 'yadif';
|
||||
page.querySelector('#chkDoubleRateDeinterlacing').checked = config.DeinterlaceDoubleRate;
|
||||
page.querySelector('#chkEnableSubtitleExtraction').checked = config.EnableSubtitleExtraction || false;
|
||||
page.querySelector('#chkEnableThrottling').checked = config.EnableThrottling || false;
|
||||
page.querySelector('#chkEnableSegmentDeletion').checked = config.EnableSegmentDeletion || false;
|
||||
page.querySelector('#txtThrottleDelaySeconds').value = config.ThrottleDelaySeconds || '';
|
||||
page.querySelector('#txtSegmentKeepSeconds').value = config.SegmentKeepSeconds || '';
|
||||
page.querySelector('#selectVideoDecoder').dispatchEvent(new CustomEvent('change', {
|
||||
bubbles: true
|
||||
}));
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSaveEncodingPathFailure() {
|
||||
loading.hide();
|
||||
alert(globalize.translate('FFmpegSavePathNotFound'));
|
||||
}
|
||||
|
||||
function updateEncoder(form) {
|
||||
return ApiClient.getSystemInfo().then(function () {
|
||||
return ApiClient.ajax({
|
||||
url: ApiClient.getUrl('System/MediaEncoder/Path'),
|
||||
type: 'POST',
|
||||
data: JSON.stringify({
|
||||
Path: form.querySelector('.txtEncoderPath').value,
|
||||
PathType: 'Custom'
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(Dashboard.processServerConfigurationUpdateResult, onSaveEncodingPathFailure);
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const form = this;
|
||||
|
||||
const onDecoderConfirmed = function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
config.EnableAudioVbr = form.querySelector('#chkEnableAudioVbr').checked;
|
||||
config.DownMixAudioBoost = form.querySelector('#txtDownMixAudioBoost').value;
|
||||
config.DownMixStereoAlgorithm = form.querySelector('#selectStereoDownmixAlgorithm').value || 'None';
|
||||
config.MaxMuxingQueueSize = form.querySelector('#txtMaxMuxingQueueSize').value;
|
||||
config.TranscodingTempPath = form.querySelector('#txtTranscodingTempPath').value;
|
||||
config.FallbackFontPath = form.querySelector('#txtFallbackFontPath').value;
|
||||
config.EnableFallbackFont = form.querySelector('#txtFallbackFontPath').value ? form.querySelector('#chkEnableFallbackFont').checked : false;
|
||||
config.EncodingThreadCount = form.querySelector('#selectThreadCount').value;
|
||||
config.HardwareAccelerationType = form.querySelector('#selectVideoDecoder').value;
|
||||
config.VaapiDevice = form.querySelector('#txtVaapiDevice').value;
|
||||
config.QsvDevice = form.querySelector('#txtQsvDevice').value;
|
||||
config.EnableTonemapping = form.querySelector('#chkTonemapping').checked;
|
||||
config.EnableVppTonemapping = form.querySelector('#chkVppTonemapping').checked;
|
||||
config.EnableVideoToolboxTonemapping = form.querySelector('#chkVideoToolboxTonemapping').checked;
|
||||
config.TonemappingAlgorithm = form.querySelector('#selectTonemappingAlgorithm').value;
|
||||
config.TonemappingMode = form.querySelector('#selectTonemappingMode').value;
|
||||
config.TonemappingRange = form.querySelector('#selectTonemappingRange').value;
|
||||
config.TonemappingDesat = form.querySelector('#txtTonemappingDesat').value;
|
||||
config.TonemappingPeak = form.querySelector('#txtTonemappingPeak').value;
|
||||
config.TonemappingParam = form.querySelector('#txtTonemappingParam').value || '0';
|
||||
config.VppTonemappingBrightness = form.querySelector('#txtVppTonemappingBrightness').value;
|
||||
config.VppTonemappingContrast = form.querySelector('#txtVppTonemappingContrast').value;
|
||||
config.EncoderPreset = form.querySelector('#selectEncoderPreset').value;
|
||||
config.H264Crf = parseInt(form.querySelector('#txtH264Crf').value || '0', 10);
|
||||
config.H265Crf = parseInt(form.querySelector('#txtH265Crf').value || '0', 10);
|
||||
config.DeinterlaceMethod = form.querySelector('#selectDeinterlaceMethod').value;
|
||||
config.DeinterlaceDoubleRate = form.querySelector('#chkDoubleRateDeinterlacing').checked;
|
||||
config.EnableSubtitleExtraction = form.querySelector('#chkEnableSubtitleExtraction').checked;
|
||||
config.EnableThrottling = form.querySelector('#chkEnableThrottling').checked;
|
||||
config.EnableSegmentDeletion = form.querySelector('#chkEnableSegmentDeletion').checked;
|
||||
config.ThrottleDelaySeconds = parseInt(form.querySelector('#txtThrottleDelaySeconds').value || '0', 10);
|
||||
config.SegmentKeepSeconds = parseInt(form.querySelector('#txtSegmentKeepSeconds').value || '0', 10);
|
||||
config.HardwareDecodingCodecs = Array.prototype.map.call(Array.prototype.filter.call(form.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
return c.checked;
|
||||
}), function (c) {
|
||||
return c.getAttribute('data-codec');
|
||||
});
|
||||
config.EnableDecodingColorDepth10Hevc = form.querySelector('#chkDecodingColorDepth10Hevc').checked;
|
||||
config.EnableDecodingColorDepth10Vp9 = form.querySelector('#chkDecodingColorDepth10Vp9').checked;
|
||||
config.EnableDecodingColorDepth10HevcRext = form.querySelector('#chkDecodingColorDepth10HevcRext').checked;
|
||||
config.EnableDecodingColorDepth12HevcRext = form.querySelector('#chkDecodingColorDepth12HevcRext').checked;
|
||||
config.EnableEnhancedNvdecDecoder = form.querySelector('#chkEnhancedNvdecDecoder').checked;
|
||||
config.PreferSystemNativeHwDecoder = form.querySelector('#chkSystemNativeHwDecoder').checked;
|
||||
config.EnableIntelLowPowerH264HwEncoder = form.querySelector('#chkIntelLpH264HwEncoder').checked;
|
||||
config.EnableIntelLowPowerHevcHwEncoder = form.querySelector('#chkIntelLpHevcHwEncoder').checked;
|
||||
config.EnableHardwareEncoding = form.querySelector('#chkHardwareEncoding').checked;
|
||||
config.AllowHevcEncoding = form.querySelector('#chkAllowHevcEncoding').checked;
|
||||
config.AllowAv1Encoding = form.querySelector('#chkAllowAv1Encoding').checked;
|
||||
ApiClient.updateNamedConfiguration('encoding', config).then(function () {
|
||||
updateEncoder(form);
|
||||
}, function () {
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (form.querySelector('#selectVideoDecoder').value !== 'none') {
|
||||
alert({
|
||||
title: globalize.translate('TitleHardwareAcceleration'),
|
||||
text: globalize.translate('HardwareAccelerationWarning')
|
||||
}).then(onDecoderConfirmed);
|
||||
} else {
|
||||
onDecoderConfirmed();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function setDecodingCodecsVisible(context, value) {
|
||||
value = value || '';
|
||||
let any;
|
||||
Array.prototype.forEach.call(context.querySelectorAll('.chkDecodeCodec'), function (c) {
|
||||
if (c.getAttribute('data-types').split(',').indexOf(value) === -1) {
|
||||
dom.parentWithTag(c, 'LABEL').classList.add('hide');
|
||||
} else {
|
||||
dom.parentWithTag(c, 'LABEL').classList.remove('hide');
|
||||
any = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (any) {
|
||||
context.querySelector('.decodingCodecsList').classList.remove('hide');
|
||||
} else {
|
||||
context.querySelector('.decodingCodecsList').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
let systemInfo;
|
||||
function getSystemInfo() {
|
||||
return systemInfo ? Promise.resolve(systemInfo) : ApiClient.getPublicSystemInfo().then(
|
||||
info => {
|
||||
systemInfo = info;
|
||||
return info;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#encodingSettingsPage', function () {
|
||||
const page = this;
|
||||
getSystemInfo();
|
||||
page.querySelector('#selectVideoDecoder').addEventListener('change', function () {
|
||||
if (this.value == 'vaapi') {
|
||||
page.querySelector('.fldVaapiDevice').classList.remove('hide');
|
||||
page.querySelector('#txtVaapiDevice').setAttribute('required', 'required');
|
||||
} else {
|
||||
page.querySelector('.fldVaapiDevice').classList.add('hide');
|
||||
page.querySelector('#txtVaapiDevice').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (this.value == 'amf' || this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi' || this.value == 'rkmpp') {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fld10bitHevcVp9HwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc' || this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldHevcRextHwDecoding').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldHevcRextHwDecoding').classList.add('hide');
|
||||
}
|
||||
|
||||
const isHwaSelected = [ 'amf', 'nvenc', 'qsv', 'vaapi', 'rkmpp', 'videotoolbox' ].includes(this.value);
|
||||
if (this.value === 'none') {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.add('hide');
|
||||
} else if (isHwaSelected) {
|
||||
page.querySelector('.tonemappingOptions').classList.remove('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.tonemappingOptions').classList.add('hide');
|
||||
page.querySelector('.fldTonemapCheckbox').classList.add('hide');
|
||||
}
|
||||
|
||||
page.querySelector('.tonemappingModeOptions').classList.toggle('hide', !isHwaSelected);
|
||||
page.querySelector('.allowTonemappingHardwareHelp').classList.toggle('hide', !isHwaSelected);
|
||||
page.querySelector('.allowTonemappingSoftwareHelp').classList.toggle('hide', isHwaSelected);
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.fldIntelLp').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldIntelLp').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value === 'videotoolbox') {
|
||||
page.querySelector('.videoToolboxTonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.videoToolboxTonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv' || this.value == 'vaapi') {
|
||||
page.querySelector('.vppTonemappingOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.vppTonemappingOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'qsv') {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.remove('hide');
|
||||
page.querySelector('.fldQsvDevice').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldSysNativeHwDecoder').classList.add('hide');
|
||||
page.querySelector('.fldQsvDevice').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value == 'nvenc') {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.fldEnhancedNvdec').classList.add('hide');
|
||||
}
|
||||
|
||||
if (this.value !== 'none') {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.remove('hide');
|
||||
} else {
|
||||
page.querySelector('.hardwareAccelerationOptions').classList.add('hide');
|
||||
}
|
||||
|
||||
setDecodingCodecsVisible(page, this.value);
|
||||
});
|
||||
$('#btnSelectTranscodingTempPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtTranscodingTempPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectTranscodingPath'),
|
||||
instruction: globalize.translate('HeaderSelectTranscodingPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectFallbackFontPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeDirectories: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtFallbackFontPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
header: globalize.translate('HeaderSelectFallbackFontPath'),
|
||||
instruction: globalize.translate('HeaderSelectFallbackFontPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('.encodingSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#encodingSettingsPage', function () {
|
||||
loading.show();
|
||||
const page = this;
|
||||
ApiClient.getNamedConfiguration('encoding').then(function (config) {
|
||||
ApiClient.getSystemInfo().then(function (fetchedSystemInfo) {
|
||||
loadPage(page, config, fetchedSystemInfo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
84
src/apps/dashboard/controllers/general.html
Normal file
84
src/apps/dashboard/controllers/general.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
<div id="dashboardGeneralPage" data-role="page" class="page type-interior dashboardHomePage" data-title="${General}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardGeneralForm">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${Settings}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtServerName" label="${LabelServerName}" />
|
||||
<div class="fieldDescription">${LabelServerNameHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectLocalizationLanguage" label="${LabelPreferredDisplayLanguage}"></select>
|
||||
<div class="fieldDescription">
|
||||
<div>${LabelDisplayLanguageHelp}</div>
|
||||
<div style="margin-top: .25em;">
|
||||
<a is="emby-linkbutton" rel="noopener noreferrer" class="button-link" href="https://jellyfin.org/docs/general/contributing/#translating" target="_blank">${LearnHowYouCanContribute}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<h2>${HeaderPaths}</h2>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtCachePath" label="${LabelCachePath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectCachePath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelCachePathHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtMetadataPath" label="${LabelMetadataPath}" autocomplete="off" dir="ltr" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectMetadataPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelMetadataPathHelp}</div>
|
||||
<input type="hidden" id="txtMetadataNetworkPath" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${QuickConnect}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxList paperList" style="padding:.5em 1em;">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkQuickConnectAvailable" />
|
||||
<span>${EnableQuickConnect}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2>${HeaderPerformance}</h2>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtLibraryScanFanoutConcurrency" label="${LibraryScanFanoutConcurrency}" placeholder="0" type="number" pattern="[0-9]*" min="0" step="1" />
|
||||
<div class="fieldDescription">${LibraryScanFanoutConcurrencyHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtParallelImageEncodingLimit" label="${LabelParallelImageEncodingLimit}" placeholder="0" type="number" pattern="[0-9]*" min="0" step="1" />
|
||||
<div class="fieldDescription">${LabelParallelImageEncodingLimitHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
105
src/apps/dashboard/controllers/general.js
Normal file
105
src/apps/dashboard/controllers/general.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-textarea/emby-textarea';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, languageOptions, systemInfo) {
|
||||
page.querySelector('#txtServerName').value = systemInfo.ServerName;
|
||||
page.querySelector('#txtCachePath').value = systemInfo.CachePath || '';
|
||||
page.querySelector('#chkQuickConnectAvailable').checked = config.QuickConnectAvailable === true;
|
||||
page.querySelector('#txtMetadataPath').value = systemInfo.InternalMetadataPath || '';
|
||||
page.querySelector('#txtMetadataNetworkPath').value = systemInfo.MetadataNetworkPath || '';
|
||||
const localizationLanguageElem = page.querySelector('#selectLocalizationLanguage');
|
||||
localizationLanguageElem.innerHTML = languageOptions.map(function (language) {
|
||||
return '<option value="' + language.Value + '">' + language.Name + '</option>';
|
||||
}).join('');
|
||||
localizationLanguageElem.value = config.UICulture;
|
||||
page.querySelector('#txtLibraryScanFanoutConcurrency').value = config.LibraryScanFanoutConcurrency || '';
|
||||
page.querySelector('#txtParallelImageEncodingLimit').value = config.ParallelImageEncodingLimit || '';
|
||||
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function (config) {
|
||||
config.ServerName = form.querySelector('#txtServerName').value;
|
||||
config.UICulture = form.querySelector('#selectLocalizationLanguage').value;
|
||||
config.CachePath = form.querySelector('#txtCachePath').value;
|
||||
config.MetadataPath = form.querySelector('#txtMetadataPath').value;
|
||||
config.MetadataNetworkPath = form.querySelector('#txtMetadataNetworkPath').value;
|
||||
config.QuickConnectAvailable = form.querySelector('#chkQuickConnectAvailable').checked;
|
||||
config.LibraryScanFanoutConcurrency = parseInt(form.querySelector('#txtLibraryScanFanoutConcurrency').value || '0', 10);
|
||||
config.ParallelImageEncodingLimit = parseInt(form.querySelector('#txtParallelImageEncodingLimit').value || '0', 10);
|
||||
|
||||
return ApiClient.updateServerConfiguration(config)
|
||||
.then(() => {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
}).catch(() => {
|
||||
loading.hide();
|
||||
alert(globalize.translate('ErrorDefault'));
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
$('#btnSelectCachePath', view).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCachePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectServerCachePath'),
|
||||
instruction: globalize.translate('HeaderSelectServerCachePathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectMetadataPath', view).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
path: view.querySelector('#txtMetadataPath').value,
|
||||
networkSharePath: view.querySelector('#txtMetadataNetworkPath').value,
|
||||
callback: function (path, networkPath) {
|
||||
if (path) {
|
||||
view.querySelector('#txtMetadataPath').value = path;
|
||||
}
|
||||
|
||||
if (networkPath) {
|
||||
view.querySelector('#txtMetadataNetworkPath').value = networkPath;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true,
|
||||
header: globalize.translate('HeaderSelectMetadataPath'),
|
||||
instruction: globalize.translate('HeaderSelectMetadataPathHelp')
|
||||
});
|
||||
});
|
||||
});
|
||||
$('.dashboardGeneralForm', view).off('submit', onSubmit).on('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
const promiseConfig = ApiClient.getServerConfiguration();
|
||||
const promiseLanguageOptions = ApiClient.getJSON(ApiClient.getUrl('Localization/Options'));
|
||||
const promiseSystemInfo = ApiClient.getSystemInfo();
|
||||
Promise.all([promiseConfig, promiseLanguageOptions, promiseSystemInfo]).then(function (responses) {
|
||||
loadPage(view, responses[0], responses[1], responses[2]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
14
src/apps/dashboard/controllers/library.html
Normal file
14
src/apps/dashboard/controllers/library.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div id="mediaLibraryPage" data-role="page" class="page type-interior mediaLibraryPage librarySectionPage fullWidthContent" data-title="${HeaderLibraries}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="padded-top padded-bottom">
|
||||
<button is="emby-button" type="button" class="raised btnRefresh">
|
||||
<span>${ButtonScanAllLibraries}</span>
|
||||
</button>
|
||||
<progress max="100" min="0" style="display: inline-block; vertical-align: middle;" class="refreshProgress"></progress>
|
||||
</div>
|
||||
|
||||
<div id="divVirtualFolders"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
393
src/apps/dashboard/controllers/library.js
Normal file
393
src/apps/dashboard/controllers/library.js
Normal file
|
@ -0,0 +1,393 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import imageHelper from 'utils/image';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-itemrefreshindicator/emby-itemrefreshindicator';
|
||||
import Dashboard, { pageClassOn, pageIdOn } from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
function addVirtualFolder(page) {
|
||||
import('components/mediaLibraryCreator/mediaLibraryCreator').then(({ default: MediaLibraryCreator }) => {
|
||||
new MediaLibraryCreator({
|
||||
collectionTypeOptions: getCollectionTypeOptions().filter(function (f) {
|
||||
return !f.hidden;
|
||||
}),
|
||||
refresh: shouldRefreshLibraryAfterChanges(page)
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function editVirtualFolder(page, virtualFolder) {
|
||||
import('components/mediaLibraryEditor/mediaLibraryEditor').then(({ default: MediaLibraryEditor }) => {
|
||||
new MediaLibraryEditor({
|
||||
refresh: shouldRefreshLibraryAfterChanges(page),
|
||||
library: virtualFolder
|
||||
}).then(function (hasChanges) {
|
||||
if (hasChanges) {
|
||||
reloadLibrary(page);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteVirtualFolder(page, virtualFolder) {
|
||||
let msg = globalize.translate('MessageAreYouSureYouWishToRemoveMediaFolder');
|
||||
|
||||
if (virtualFolder.Locations.length) {
|
||||
msg += '<br/><br/>' + globalize.translate('MessageTheFollowingLocationWillBeRemovedFromLibrary') + '<br/><br/>';
|
||||
msg += virtualFolder.Locations.join('<br/>');
|
||||
}
|
||||
|
||||
confirm({
|
||||
text: msg,
|
||||
title: globalize.translate('HeaderRemoveMediaFolder'),
|
||||
confirmText: globalize.translate('Delete'),
|
||||
primary: 'delete'
|
||||
}).then(function () {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.removeVirtualFolder(virtualFolder.Name, refreshAfterChange).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function refreshVirtualFolder(page, virtualFolder) {
|
||||
import('components/refreshdialog/refreshdialog').then(({ default: RefreshDialog }) => {
|
||||
new RefreshDialog({
|
||||
itemIds: [virtualFolder.ItemId],
|
||||
serverId: ApiClient.serverId(),
|
||||
mode: 'scan'
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
|
||||
function renameVirtualFolder(page, virtualFolder) {
|
||||
import('components/prompt/prompt').then(({ default: prompt }) => {
|
||||
prompt({
|
||||
label: globalize.translate('LabelNewName'),
|
||||
description: globalize.translate('MessageRenameMediaFolder'),
|
||||
confirmText: globalize.translate('ButtonRename')
|
||||
}).then(function (newName) {
|
||||
if (newName && newName != virtualFolder.Name) {
|
||||
const refreshAfterChange = shouldRefreshLibraryAfterChanges(page);
|
||||
ApiClient.renameVirtualFolder(virtualFolder.Name, newName, refreshAfterChange).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showCardMenu(page, elem, virtualFolders) {
|
||||
const card = dom.parentWithClass(elem, 'card');
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
const menuItems = [];
|
||||
menuItems.push({
|
||||
name: globalize.translate('EditImages'),
|
||||
id: 'editimages',
|
||||
icon: 'photo'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ManageLibrary'),
|
||||
id: 'edit',
|
||||
icon: 'folder'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRename'),
|
||||
id: 'rename',
|
||||
icon: 'mode_edit'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ScanLibrary'),
|
||||
id: 'refresh',
|
||||
icon: 'refresh'
|
||||
});
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonRemove'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
|
||||
import('components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
||||
callback: function (resultId) {
|
||||
switch (resultId) {
|
||||
case 'edit':
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'editimages':
|
||||
editImages(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
renameVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
deleteVirtualFolder(page, virtualFolder);
|
||||
break;
|
||||
|
||||
case 'refresh':
|
||||
refreshVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadLibrary(page) {
|
||||
loading.show();
|
||||
ApiClient.getVirtualFolders().then(function (result) {
|
||||
reloadVirtualFolders(page, result);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldRefreshLibraryAfterChanges(page) {
|
||||
return page.id === 'mediaLibraryPage';
|
||||
}
|
||||
|
||||
function reloadVirtualFolders(page, virtualFolders) {
|
||||
let html = '';
|
||||
virtualFolders.push({
|
||||
Name: globalize.translate('ButtonAddMediaLibrary'),
|
||||
icon: 'add_circle',
|
||||
Locations: [],
|
||||
showType: false,
|
||||
showLocations: false,
|
||||
showMenu: false,
|
||||
showNameWithIcon: false,
|
||||
elementId: 'addLibrary'
|
||||
});
|
||||
|
||||
for (let i = 0; i < virtualFolders.length; i++) {
|
||||
const virtualFolder = virtualFolders[i];
|
||||
html += getVirtualFolderHtml(page, virtualFolder, i);
|
||||
}
|
||||
|
||||
const divVirtualFolders = page.querySelector('#divVirtualFolders');
|
||||
divVirtualFolders.innerHTML = html;
|
||||
divVirtualFolders.classList.add('itemsContainer');
|
||||
divVirtualFolders.classList.add('vertical-wrap');
|
||||
const btnCardMenuElements = divVirtualFolders.querySelectorAll('.btnCardMenu');
|
||||
btnCardMenuElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
showCardMenu(page, btn, virtualFolders);
|
||||
});
|
||||
});
|
||||
divVirtualFolders.querySelector('#addLibrary').addEventListener('click', function () {
|
||||
addVirtualFolder(page);
|
||||
});
|
||||
|
||||
const libraryEditElements = divVirtualFolders.querySelectorAll('.editLibrary');
|
||||
libraryEditElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const card = dom.parentWithClass(btn, 'card');
|
||||
const index = parseInt(card.getAttribute('data-index'), 10);
|
||||
const virtualFolder = virtualFolders[index];
|
||||
|
||||
if (virtualFolder.ItemId) {
|
||||
editVirtualFolder(page, virtualFolder);
|
||||
}
|
||||
});
|
||||
});
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function editImages(page, virtualFolder) {
|
||||
import('components/imageeditor/imageeditor').then((imageEditor) => {
|
||||
imageEditor.show({
|
||||
itemId: virtualFolder.ItemId,
|
||||
serverId: ApiClient.serverId()
|
||||
}).then(function () {
|
||||
reloadLibrary(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getLink(text, url) {
|
||||
return globalize.translate(text, '<a is="emby-linkbutton" class="button-link" href="' + url + '" target="_blank" data-autohide="true">', '</a>');
|
||||
}
|
||||
|
||||
function getCollectionTypeOptions() {
|
||||
return [{
|
||||
name: '',
|
||||
value: ''
|
||||
}, {
|
||||
name: globalize.translate('Movies'),
|
||||
value: 'movies',
|
||||
message: getLink('MovieLibraryHelp', 'https://jellyfin.org/docs/general/server/media/movies')
|
||||
}, {
|
||||
name: globalize.translate('TabMusic'),
|
||||
value: 'music',
|
||||
message: getLink('MusicLibraryHelp', 'https://jellyfin.org/docs/general/server/media/music')
|
||||
}, {
|
||||
name: globalize.translate('Shows'),
|
||||
value: 'tvshows',
|
||||
message: getLink('TvLibraryHelp', 'https://jellyfin.org/docs/general/server/media/shows')
|
||||
}, {
|
||||
name: globalize.translate('Books'),
|
||||
value: 'books',
|
||||
message: getLink('BookLibraryHelp', 'https://jellyfin.org/docs/general/server/media/books')
|
||||
}, {
|
||||
name: globalize.translate('HomeVideosPhotos'),
|
||||
value: 'homevideos'
|
||||
}, {
|
||||
name: globalize.translate('MusicVideos'),
|
||||
value: 'musicvideos'
|
||||
}, {
|
||||
name: globalize.translate('MixedMoviesShows'),
|
||||
value: 'mixed',
|
||||
message: globalize.translate('MessageUnsetContentHelp')
|
||||
}];
|
||||
}
|
||||
|
||||
function getVirtualFolderHtml(page, virtualFolder, index) {
|
||||
let html = '';
|
||||
let style = '';
|
||||
|
||||
if (page.classList.contains('wizardPage')) {
|
||||
style += 'min-width:33.3%;';
|
||||
}
|
||||
|
||||
const elementId = virtualFolder.elementId ? `id="${virtualFolder.elementId}" ` : '';
|
||||
html += '<div ' + elementId + 'class="card backdropCard scalableCard backdropCard-scalable" style="' + style + '" data-index="' + index + '" data-id="' + virtualFolder.ItemId + '">';
|
||||
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
let imgUrl = '';
|
||||
|
||||
if (virtualFolder.PrimaryImageItemId) {
|
||||
imgUrl = ApiClient.getScaledImageUrl(virtualFolder.PrimaryImageItemId, {
|
||||
maxWidth: Math.round(dom.getScreenWidth() * 0.40),
|
||||
type: 'Primary'
|
||||
});
|
||||
}
|
||||
|
||||
let hasCardImageContainer;
|
||||
|
||||
if (imgUrl) {
|
||||
html += `<div class="cardImageContainer editLibrary ${imgUrl ? '' : getDefaultBackgroundClass()}" style="cursor:pointer">`;
|
||||
html += `<img src="${imgUrl}" style="width:100%" />`;
|
||||
hasCardImageContainer = true;
|
||||
} else if (!virtualFolder.showNameWithIcon) {
|
||||
html += `<div class="cardImageContainer editLibrary ${getDefaultBackgroundClass()}" style="cursor:pointer;">`;
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
hasCardImageContainer = true;
|
||||
}
|
||||
|
||||
if (hasCardImageContainer) {
|
||||
html += '<div class="cardIndicators backdropCardIndicators">';
|
||||
html += '<div is="emby-itemrefreshindicator"' + (virtualFolder.RefreshProgress || virtualFolder.RefreshStatus && virtualFolder.RefreshStatus !== 'Idle' ? '' : ' class="hide"') + ' data-progress="' + (virtualFolder.RefreshProgress || 0) + '" data-status="' + virtualFolder.RefreshStatus + '"></div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
if (!imgUrl && virtualFolder.showNameWithIcon) {
|
||||
html += '<h3 class="cardImageContainer addLibrary" style="position:absolute;top:0;left:0;right:0;bottom:0;cursor:pointer;flex-direction:column;">';
|
||||
html += '<span class="cardImageIcon material-icons ' + (virtualFolder.icon || imageHelper.getLibraryIcon(virtualFolder.CollectionType)) + '" aria-hidden="true"></span>';
|
||||
|
||||
if (virtualFolder.showNameWithIcon) {
|
||||
html += '<div style="margin:1em 0;position:width:100%;">';
|
||||
html += escapeHtml(virtualFolder.Name);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</h3>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">'; // always show menu unless explicitly hidden
|
||||
|
||||
if (virtualFolder.showMenu !== false) {
|
||||
const dirTextAlign = globalize.getIsRTL() ? 'left' : 'right';
|
||||
html += '<div style="text-align:' + dirTextAlign + '; float:' + dirTextAlign + ';padding-top:5px;">';
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += "<div class='cardText'>";
|
||||
|
||||
if (virtualFolder.showNameWithIcon) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += escapeHtml(virtualFolder.Name);
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
let typeName = getCollectionTypeOptions().filter(function (t) {
|
||||
return t.value == virtualFolder.CollectionType;
|
||||
})[0];
|
||||
typeName = typeName ? typeName.name : globalize.translate('Other');
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
|
||||
if (virtualFolder.showType === false) {
|
||||
html += ' ';
|
||||
} else {
|
||||
html += typeName;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
if (virtualFolder.showLocations === false) {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += ' ';
|
||||
html += '</div>';
|
||||
} else if (virtualFolder.Locations.length && virtualFolder.Locations.length === 1) {
|
||||
html += "<div class='cardText cardText-secondary' dir='ltr' style='text-align:left;'>";
|
||||
html += virtualFolder.Locations[0];
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += globalize.translate('NumLocationsValue', virtualFolder.Locations.length);
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
window.WizardLibraryPage = {
|
||||
next: function () {
|
||||
Dashboard.navigate('wizardsettings.html');
|
||||
}
|
||||
};
|
||||
pageClassOn('pageshow', 'mediaLibraryPage', function () {
|
||||
reloadLibrary(this);
|
||||
});
|
||||
pageIdOn('pageshow', 'mediaLibraryPage', function () {
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
pageIdOn('pagebeforehide', 'mediaLibraryPage', function () {
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'off',
|
||||
progressElem: page.querySelector('.refreshProgress'),
|
||||
taskKey: 'RefreshLibrary',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
||||
|
57
src/apps/dashboard/controllers/librarydisplay.html
Normal file
57
src/apps/dashboard/controllers/librarydisplay.html
Normal file
|
@ -0,0 +1,57 @@
|
|||
<div id="libraryDisplayPage" data-role="page" class="page type-interior librarySectionPage" data-title="${Display}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectDateAdded" data-mini="true" label="${LabelDateAddedBehavior}">
|
||||
<option value="0">${OptionDateAddedImportTime}</option>
|
||||
<option value="1">${OptionDateAddedFileTime}</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${LabelDateAddedBehaviorHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFolderView" />
|
||||
<span>${OptionDisplayFolderView}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionDisplayFolderViewHelp}</div>
|
||||
</div>
|
||||
|
||||
<label class="checkboxContainer">
|
||||
<input type="checkbox" is="emby-checkbox" class="chkDisplaySpecialsWithinSeasons"/>
|
||||
<span>${LabelDisplaySpecialsWithinSeasons}</span>
|
||||
</label>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkGroupMoviesIntoCollections" />
|
||||
<span>${LabelGroupMoviesIntoCollections}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelGroupMoviesIntoCollectionsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input class="chkExternalContentInSuggestions" type="checkbox" is="emby-checkbox" />
|
||||
<span>${OptionEnableExternalContentInSuggestions}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionEnableExternalContentInSuggestionsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldSaveMetadataHidden hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkAirDays" id="chkSaveMetadataHidden" data-filter="Sunday" />
|
||||
<span>${OptionSaveMetadataAsHidden}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${OptionSaveMetadataAsHiddenHelp}</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
52
src/apps/dashboard/controllers/librarydisplay.js
Normal file
52
src/apps/dashboard/controllers/librarydisplay.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
export default function(view) {
|
||||
function loadData() {
|
||||
ApiClient.getServerConfiguration().then(function(config) {
|
||||
view.querySelector('.chkFolderView').checked = config.EnableFolderView;
|
||||
view.querySelector('.chkGroupMoviesIntoCollections').checked = config.EnableGroupingIntoCollections;
|
||||
view.querySelector('.chkDisplaySpecialsWithinSeasons').checked = config.DisplaySpecialsWithinSeasons;
|
||||
view.querySelector('.chkExternalContentInSuggestions').checked = config.EnableExternalContentInSuggestions;
|
||||
view.querySelector('#chkSaveMetadataHidden').checked = config.SaveMetadataHidden;
|
||||
});
|
||||
ApiClient.getNamedConfiguration('metadata').then(function(metadata) {
|
||||
view.querySelector('#selectDateAdded').selectedIndex = metadata.UseFileCreationTimeForDateAdded ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
view.querySelector('form').addEventListener('submit', function(e) {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getServerConfiguration().then(function(config) {
|
||||
config.EnableFolderView = form.querySelector('.chkFolderView').checked;
|
||||
config.EnableGroupingIntoCollections = form.querySelector('.chkGroupMoviesIntoCollections').checked;
|
||||
config.DisplaySpecialsWithinSeasons = form.querySelector('.chkDisplaySpecialsWithinSeasons').checked;
|
||||
config.EnableExternalContentInSuggestions = form.querySelector('.chkExternalContentInSuggestions').checked;
|
||||
config.SaveMetadataHidden = form.querySelector('#chkSaveMetadataHidden').checked;
|
||||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
ApiClient.getNamedConfiguration('metadata').then(function(config) {
|
||||
config.UseFileCreationTimeForDateAdded = form.querySelector('#selectDateAdded').value === '1';
|
||||
ApiClient.updateNamedConfiguration('metadata', config);
|
||||
});
|
||||
|
||||
e.preventDefault();
|
||||
loading.hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function() {
|
||||
loadData();
|
||||
ApiClient.getSystemInfo().then(function(info) {
|
||||
if (info.OperatingSystem === 'Windows') {
|
||||
view.querySelector('.fldSaveMetadataHidden').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldSaveMetadataHidden').classList.add('hide');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
7
src/apps/dashboard/controllers/livetvguideprovider.html
Normal file
7
src/apps/dashboard/controllers/livetvguideprovider.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<div id="liveTvGuideProviderPage" data-role="page" class="page type-interior liveTvSettingsPage">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="readOnlyContent providerTemplate" style="margin-top: 2em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
30
src/apps/dashboard/controllers/livetvguideprovider.js
Normal file
30
src/apps/dashboard/controllers/livetvguideprovider.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard, { pageIdOn } from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
import Events from 'utils/events';
|
||||
|
||||
function onListingsSubmitted() {
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}
|
||||
|
||||
function init(page, type, providerId) {
|
||||
import(`components/tvproviders/${type}`).then(({ default: ProviderFactory }) => {
|
||||
const instance = new ProviderFactory(page, providerId, {});
|
||||
Events.on(instance, 'submitted', onListingsSubmitted);
|
||||
instance.init();
|
||||
});
|
||||
}
|
||||
|
||||
function loadTemplate(page, type, providerId) {
|
||||
import(`components/tvproviders/${type}.template.html`).then(({ default: html }) => {
|
||||
page.querySelector('.providerTemplate').innerHTML = globalize.translateHtml(html);
|
||||
init(page, type, providerId);
|
||||
});
|
||||
}
|
||||
|
||||
pageIdOn('pageshow', 'liveTvGuideProviderPage', function () {
|
||||
loading.show();
|
||||
const providerId = getParameterByName('id');
|
||||
loadTemplate(this, getParameterByName('type'), providerId);
|
||||
});
|
120
src/apps/dashboard/controllers/livetvsettings.html
Normal file
120
src/apps/dashboard/controllers/livetvsettings.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
<div id="liveTvSettingsPage" data-role="page" class="page type-interior liveTvPage" data-title="${HeaderDVR}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${HeaderDVR}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="liveTvSettingsForm">
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectGuideDays" label="${LabelNumberOfGuideDays}">
|
||||
<option value="">${Auto}</option>
|
||||
<option value="1">1</option>
|
||||
<option value="2">2</option>
|
||||
<option value="3">3</option>
|
||||
<option value="4">4</option>
|
||||
<option value="5">5</option>
|
||||
<option value="6">6</option>
|
||||
<option value="7">7</option>
|
||||
<option value="8">8</option>
|
||||
<option value="9">9</option>
|
||||
<option value="10">10</option>
|
||||
<option value="11">11</option>
|
||||
<option value="12">12</option>
|
||||
<option value="13">13</option>
|
||||
<option value="14">14</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${LabelNumberOfGuideDaysHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtRecordingPath" label="${LabelRecordingPath}" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectRecordingPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelRecordingPathHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtMovieRecordingPath" label="${LabelMovieRecordingPath}" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectMovieRecordingPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" id="txtSeriesRecordingPath" label="${LabelSeriesRecordingPath}" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectSeriesRecordingPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="sectionTitle">${HeaderDefaultRecordingSettings}</h2>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow: 1;">
|
||||
<input is="emby-input" type="number" id="txtPrePaddingMinutes" pattern="[0-9]*" required="required" min="0" step="1" label="${LabelStartWhenPossible}" />
|
||||
</div>
|
||||
<div class="fieldDescription" style="margin-left:.5em;font-size:90%;margin-top:1.3em;">
|
||||
${MinutesBefore}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow: 1;">
|
||||
<input is="emby-input" type="number" id="txtPostPaddingMinutes" pattern="[0-9]*" required="required" min="0" step="1" label="${LabelStopWhenPossible}" />
|
||||
</div>
|
||||
<div class="fieldDescription" style="margin-left:.5em;font-size:90%;margin-top:1.3em;">
|
||||
${MinutesAfter}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="sectionTitle">${HeaderRecordingPostProcessing}</h2>
|
||||
<div class="inputContainer">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" type="text" id="txtPostProcessor" label="${LabelPostProcessor}" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectPostProcessorPath" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtPostProcessorArguments" label="${LabelPostProcessorArguments}" />
|
||||
<div class="fieldDescription">${LabelPostProcessorArgumentsHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verticalSection">
|
||||
<h2 class="sectionTitle">${HeaderRecordingMetadataSaving}</h2>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSaveRecordingNFO" />
|
||||
<span>${SaveRecordingNFO}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${SaveRecordingNFOHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input is="emby-checkbox" type="checkbox" id="chkSaveRecordingImages" />
|
||||
<span>${SaveRecordingImages}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${SaveRecordingImagesHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
133
src/apps/dashboard/controllers/livetvsettings.js
Normal file
133
src/apps/dashboard/controllers/livetvsettings.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config) {
|
||||
page.querySelector('.liveTvSettingsForm').classList.remove('hide');
|
||||
page.querySelector('.noLiveTvServices')?.classList.add('hide');
|
||||
page.querySelector('#selectGuideDays').value = config.GuideDays || '';
|
||||
page.querySelector('#txtPrePaddingMinutes').value = config.PrePaddingSeconds / 60;
|
||||
page.querySelector('#txtPostPaddingMinutes').value = config.PostPaddingSeconds / 60;
|
||||
page.querySelector('#txtRecordingPath').value = config.RecordingPath || '';
|
||||
page.querySelector('#txtMovieRecordingPath').value = config.MovieRecordingPath || '';
|
||||
page.querySelector('#txtSeriesRecordingPath').value = config.SeriesRecordingPath || '';
|
||||
page.querySelector('#txtPostProcessor').value = config.RecordingPostProcessor || '';
|
||||
page.querySelector('#txtPostProcessorArguments').value = config.RecordingPostProcessorArguments || '';
|
||||
page.querySelector('#chkSaveRecordingNFO').checked = config.SaveRecordingNFO;
|
||||
page.querySelector('#chkSaveRecordingImages').checked = config.SaveRecordingImages;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
config.GuideDays = form.querySelector('#selectGuideDays').value || null;
|
||||
const recordingPath = form.querySelector('#txtRecordingPath').value || null;
|
||||
const movieRecordingPath = form.querySelector('#txtMovieRecordingPath').value || null;
|
||||
const seriesRecordingPath = form.querySelector('#txtSeriesRecordingPath').value || null;
|
||||
const recordingPathChanged = recordingPath != config.RecordingPath || movieRecordingPath != config.MovieRecordingPath || seriesRecordingPath != config.SeriesRecordingPath;
|
||||
config.RecordingPath = recordingPath;
|
||||
config.MovieRecordingPath = movieRecordingPath;
|
||||
config.SeriesRecordingPath = seriesRecordingPath;
|
||||
config.RecordingEncodingFormat = 'mkv';
|
||||
config.PrePaddingSeconds = 60 * form.querySelector('#txtPrePaddingMinutes').value;
|
||||
config.PostPaddingSeconds = 60 * form.querySelector('#txtPostPaddingMinutes').value;
|
||||
config.RecordingPostProcessor = form.querySelector('#txtPostProcessor').value;
|
||||
config.RecordingPostProcessorArguments = form.querySelector('#txtPostProcessorArguments').value;
|
||||
config.SaveRecordingNFO = form.querySelector('#chkSaveRecordingNFO').checked;
|
||||
config.SaveRecordingImages = form.querySelector('#chkSaveRecordingImages').checked;
|
||||
ApiClient.updateNamedConfiguration('livetv', config).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
showSaveMessage(recordingPathChanged);
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function showSaveMessage(recordingPathChanged) {
|
||||
let msg = '';
|
||||
|
||||
if (recordingPathChanged) {
|
||||
msg += globalize.translate('MessageChangeRecordingPath');
|
||||
}
|
||||
|
||||
if (msg) {
|
||||
alert(msg);
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#liveTvSettingsPage', function () {
|
||||
const page = this;
|
||||
$('.liveTvSettingsForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
$('#btnSelectRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectMovieRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtMovieRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectSeriesRecordingPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtSeriesRecordingPath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
validateWriteable: true
|
||||
});
|
||||
});
|
||||
});
|
||||
$('#btnSelectPostProcessorPath', page).on('click.selectDirectory', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
page.querySelector('#txtPostProcessor').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}).on('pageshow', '#liveTvSettingsPage', function () {
|
||||
loading.show();
|
||||
const page = this;
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
loadPage(page, config);
|
||||
});
|
||||
});
|
40
src/apps/dashboard/controllers/livetvstatus.html
Normal file
40
src/apps/dashboard/controllers/livetvstatus.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<div id="liveTvStatusPage" data-role="page" class="page type-interior liveTvSettingsPage" data-title="${LiveTV}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer sectionTitleContainer-cards">
|
||||
<h2 class="sectionTitle sectionTitle-cards">
|
||||
<span>${HeaderTunerDevices}</span>
|
||||
</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddDevice submit sectionTitleButton" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="devicesList itemsContainer vertical-wrap" data-hovermenu="false" data-multiselect="false" style="margin-top: .5em;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="readOnlyContent">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer">
|
||||
<h2 class="sectionTitle">${HeaderGuideProviders}</h2>
|
||||
<button is="emby-button" type="button" class="fab btnAddProvider submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="providerList">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button is="emby-button" type="button" class="raised btnRefresh block button-cancel">
|
||||
<span>${ButtonRefreshGuideData}</span>
|
||||
</button>
|
||||
<progress max="100" min="0" style="width: 100%;" class="refreshGuideProgress"></progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
338
src/apps/dashboard/controllers/livetvstatus.js
Normal file
338
src/apps/dashboard/controllers/livetvstatus.js
Normal file
|
@ -0,0 +1,338 @@
|
|||
import 'jquery';
|
||||
|
||||
import globalize from 'lib/globalize';
|
||||
import taskButton from 'scripts/taskbutton';
|
||||
import dom from 'scripts/dom';
|
||||
import layoutManager from 'components/layoutManager';
|
||||
import loading from 'components/loading/loading';
|
||||
import browser from 'scripts/browser';
|
||||
import 'components/listview/listview.scss';
|
||||
import 'styles/flexstyles.scss';
|
||||
import 'elements/emby-itemscontainer/emby-itemscontainer';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'material-design-icons-iconfont';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
const enableFocusTransform = !browser.slow && !browser.edge;
|
||||
|
||||
function getDeviceHtml(device) {
|
||||
const padderClass = 'cardPadder-backdrop';
|
||||
let cssClass = 'card scalableCard backdropCard backdropCard-scalable';
|
||||
const cardBoxCssClass = 'cardBox visualCardBox';
|
||||
let html = '';
|
||||
|
||||
// TODO move card creation code to Card component
|
||||
|
||||
if (layoutManager.tv) {
|
||||
cssClass += ' show-focus';
|
||||
|
||||
if (enableFocusTransform) {
|
||||
cssClass += ' show-animation';
|
||||
}
|
||||
}
|
||||
|
||||
html += '<div type="button" class="' + cssClass + '" data-id="' + device.Id + '">';
|
||||
html += '<div class="' + cardBoxCssClass + '">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="' + padderClass + '"></div>';
|
||||
html += '<div class="cardContent searchImage">';
|
||||
html += `<div class="cardImageContainer coveredImage ${getDefaultBackgroundClass()}"><span class="cardImageIcon material-icons dvr" aria-hidden="true"></span></div>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter visualCardBox-cardFooter">';
|
||||
html += '<button is="paper-icon-button-light" class="itemAction btnCardOptions autoSize" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '<div class="cardText">' + (device.FriendlyName || getTunerName(device.Type)) + '</div>';
|
||||
html += '<div class="cardText cardText-secondary">';
|
||||
html += device.Url || ' ';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderDevices(page, devices) {
|
||||
page.querySelector('.devicesList').innerHTML = devices.map(getDeviceHtml).join('');
|
||||
}
|
||||
|
||||
function deleteDevice(page, id) {
|
||||
const message = globalize.translate('MessageConfirmDeleteTunerDevice');
|
||||
|
||||
confirm(message, globalize.translate('HeaderDeleteDevice')).then(function () {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('LiveTv/TunerHosts', {
|
||||
Id: id
|
||||
})
|
||||
}).then(function () {
|
||||
reload(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reload(page) {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
renderDevices(page, config.TunerHosts);
|
||||
renderProviders(page, config.ListingProviders);
|
||||
});
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function submitAddDeviceForm(page) {
|
||||
page.querySelector('.dlgAddDevice').close();
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('LiveTv/TunerHosts'),
|
||||
data: JSON.stringify({
|
||||
Type: page.querySelector('#selectTunerDeviceType').value,
|
||||
Url: page.querySelector('#txtDevicePath').value
|
||||
}),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
reload(page);
|
||||
}, function () {
|
||||
Dashboard.alert({
|
||||
message: globalize.translate('ErrorAddingTunerDevice')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderProviders(page, providers) {
|
||||
let html = '';
|
||||
|
||||
if (providers.length) {
|
||||
html += '<div class="paperList">';
|
||||
|
||||
for (let i = 0, length = providers.length; i < length; i++) {
|
||||
const provider = providers[i];
|
||||
html += '<div class="listItem">';
|
||||
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true"></span>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
html += '<a is="emby-linkbutton" style="display:block;padding:0;margin:0;text-align:left;" class="clearLink" href="' + getProviderConfigurationUrl(provider.Type) + '&id=' + provider.Id + '">';
|
||||
html += '<h3 class="listItemBodyText">';
|
||||
html += getProviderName(provider.Type);
|
||||
html += '</h3>';
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
html += provider.Path || provider.ListingsId || '';
|
||||
html += '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnOptions" data-id="' + provider.Id + '"><span class="material-icons listItemAside more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
const elem = page.querySelector('.providerList');
|
||||
elem.innerHTML = html;
|
||||
if (elem.querySelector('.btnOptions')) {
|
||||
const btnOptionElements = elem.querySelectorAll('.btnOptions');
|
||||
btnOptionElements.forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
const id = this.getAttribute('data-id');
|
||||
showProviderOptions(page, id, btn);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showProviderOptions(page, providerId, button) {
|
||||
const items = [];
|
||||
items.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete'
|
||||
});
|
||||
items.push({
|
||||
name: globalize.translate('MapChannels'),
|
||||
id: 'map'
|
||||
});
|
||||
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: items,
|
||||
positionTo: button
|
||||
}).then(function (id) {
|
||||
switch (id) {
|
||||
case 'delete':
|
||||
deleteProvider(page, providerId);
|
||||
break;
|
||||
|
||||
case 'map':
|
||||
mapChannels(page, providerId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function mapChannels(page, providerId) {
|
||||
import('components/channelMapper/channelMapper').then(({ default: ChannelMapper }) => {
|
||||
new ChannelMapper({
|
||||
serverId: ApiClient.serverInfo().Id,
|
||||
providerId: providerId
|
||||
}).show();
|
||||
});
|
||||
}
|
||||
|
||||
function deleteProvider(page, id) {
|
||||
const message = globalize.translate('MessageConfirmDeleteGuideProvider');
|
||||
|
||||
confirm(message, globalize.translate('HeaderDeleteProvider')).then(function () {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'DELETE',
|
||||
url: ApiClient.getUrl('LiveTv/ListingProviders', {
|
||||
Id: id
|
||||
})
|
||||
}).then(function () {
|
||||
reload(page);
|
||||
}, function () {
|
||||
reload(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTunerName(providerId) {
|
||||
switch (providerId.toLowerCase()) {
|
||||
case 'm3u':
|
||||
return 'M3U';
|
||||
case 'hdhomerun':
|
||||
return 'HDHomeRun';
|
||||
case 'hauppauge':
|
||||
return 'Hauppauge';
|
||||
case 'satip':
|
||||
return 'DVB';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderName(providerId) {
|
||||
switch (providerId.toLowerCase()) {
|
||||
case 'schedulesdirect':
|
||||
return 'Schedules Direct';
|
||||
case 'xmltv':
|
||||
return 'XMLTV';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function getProviderConfigurationUrl(providerId) {
|
||||
switch (providerId.toLowerCase()) {
|
||||
case 'xmltv':
|
||||
return '#/dashboard/livetv/guide?type=xmltv';
|
||||
case 'schedulesdirect':
|
||||
return '#/dashboard/livetv/guide?type=schedulesdirect';
|
||||
}
|
||||
}
|
||||
|
||||
function addProvider(button) {
|
||||
const menuItems = [];
|
||||
menuItems.push({
|
||||
name: 'Schedules Direct',
|
||||
id: 'SchedulesDirect'
|
||||
});
|
||||
menuItems.push({
|
||||
name: 'XMLTV',
|
||||
id: 'xmltv'
|
||||
});
|
||||
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: button,
|
||||
callback: function (id) {
|
||||
Dashboard.navigate(getProviderConfigurationUrl(id));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addDevice() {
|
||||
Dashboard.navigate('dashboard/livetv/tuner');
|
||||
}
|
||||
|
||||
function showDeviceMenu(button, tunerDeviceId) {
|
||||
const items = [];
|
||||
items.push({
|
||||
name: globalize.translate('Delete'),
|
||||
id: 'delete'
|
||||
});
|
||||
items.push({
|
||||
name: globalize.translate('Edit'),
|
||||
id: 'edit'
|
||||
});
|
||||
|
||||
import('components/actionSheet/actionSheet').then(({ default: actionsheet }) => {
|
||||
actionsheet.show({
|
||||
items: items,
|
||||
positionTo: button
|
||||
}).then(function (id) {
|
||||
switch (id) {
|
||||
case 'delete':
|
||||
deleteDevice(dom.parentWithClass(button, 'page'), tunerDeviceId);
|
||||
break;
|
||||
|
||||
case 'edit':
|
||||
Dashboard.navigate('dashboard/livetv/tuner?id=' + tunerDeviceId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onDevicesListClick(e) {
|
||||
const card = dom.parentWithClass(e.target, 'card');
|
||||
|
||||
if (card) {
|
||||
const id = card.getAttribute('data-id');
|
||||
const btnCardOptions = dom.parentWithClass(e.target, 'btnCardOptions');
|
||||
|
||||
if (btnCardOptions) {
|
||||
showDeviceMenu(btnCardOptions, id);
|
||||
} else {
|
||||
Dashboard.navigate('dashboard/livetv/tuner?id=' + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#liveTvStatusPage', function () {
|
||||
const page = this;
|
||||
page.querySelector('.btnAddDevice').addEventListener('click', function () {
|
||||
addDevice();
|
||||
});
|
||||
if (page.querySelector('.formAddDevice')) {
|
||||
// NOTE: unused?
|
||||
page.querySelector('.formAddDevice').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
submitAddDeviceForm(page);
|
||||
});
|
||||
}
|
||||
page.querySelector('.btnAddProvider').addEventListener('click', function () {
|
||||
addProvider(this);
|
||||
});
|
||||
page.querySelector('.devicesList').addEventListener('click', onDevicesListClick);
|
||||
}).on('pageshow', '#liveTvStatusPage', function () {
|
||||
const page = this;
|
||||
reload(page);
|
||||
taskButton({
|
||||
mode: 'on',
|
||||
progressElem: page.querySelector('.refreshGuideProgress'),
|
||||
taskKey: 'RefreshGuide',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
}).on('pagehide', '#liveTvStatusPage', function () {
|
||||
const page = this;
|
||||
taskButton({
|
||||
mode: 'off',
|
||||
progressElem: page.querySelector('.refreshGuideProgress'),
|
||||
taskKey: 'RefreshGuide',
|
||||
button: page.querySelector('.btnRefresh')
|
||||
});
|
||||
});
|
107
src/apps/dashboard/controllers/livetvtuner.html
Normal file
107
src/apps/dashboard/controllers/livetvtuner.html
Normal file
|
@ -0,0 +1,107 @@
|
|||
<div id="liveTvTunerPage" data-role="page" class="page type-interior liveTvSettingsPage">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form>
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h1 class="sectionTitle">${HeaderLiveTvTunerSetup}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" class="selectType" label="${LabelTunerType}" required="required"></select>
|
||||
</div>
|
||||
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnDetect hide" style="margin-bottom:3em;">${HeaderDetectMyDevices}</button>
|
||||
|
||||
<div class="inputContainer fldFriendlyName hide">
|
||||
<input is="emby-input" type="text" class="txtFriendlyName" label="${LabelFriendlyName}" autocomplete="off" />
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldPath hide">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow: 1;">
|
||||
<input is="emby-input" type="text" class="txtDevicePath" label="${LabelFileOrUrl}" required="required" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" class="btnSelectPath hide emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldUserAgent hide">
|
||||
<input is="emby-input" type="text" class="txtUserAgent" label="${LabelUserAgent}" autocomplete="off" />
|
||||
<div class="fieldDescription">${UserAgentHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldTunerCount hide">
|
||||
<input is="emby-input" type="number" pattern="[0-9]*" required="required" min="0" step="1" class="txtTunerCount" label="${LabelSimultaneousConnectionLimit}" autocomplete="off" value="0" />
|
||||
<div class="fieldDescription">${SimultaneousConnectionLimitHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldFallbackMaxStreamingBitrate hide">
|
||||
<input is="emby-input" type="number" pattern="[0-9]*" required="required" min="1" step="1" class="txtFallbackMaxStreamingBitrate" label="${LabelFallbackMaxStreamingBitrate}" autocomplete="off" value="30" />
|
||||
<div class="fieldDescription">${FallbackMaxStreamingBitrateHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldFavorites hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFavorite" />
|
||||
<span>${LabelImportOnlyFavoriteChannels}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${ImportFavoriteChannelsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldTranscode hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkTranscode" />
|
||||
<span>${LabelAllowHWTranscoding}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowHWTranscodingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldFmp4Container hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkFmp4Container" />
|
||||
<span>${LabelAllowFmp4TranscodingContainer}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowFmp4TranscodingContainerHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldStreamSharing hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkStreamSharing" checked />
|
||||
<span>${LabelAllowStreamSharing}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowStreamSharingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldStreamLoop hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkStreamLoop" />
|
||||
<span>${EnableStreamLooping}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${EnableStreamLoopingHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription fldIgnoreDts hide">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" class="chkIgnoreDts" checked />
|
||||
<span>${IgnoreDts}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${IgnoreDtsHelp}</div>
|
||||
</div>
|
||||
|
||||
<p class="drmMessage hide">${DrmChannelsNotImported}</p>
|
||||
<br />
|
||||
<input type="hidden" class="fldDeviceId" />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" class="raised button-cancel block btnCancel" onclick="history.back();">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
252
src/apps/dashboard/controllers/livetvtuner.js
Normal file
252
src/apps/dashboard/controllers/livetvtuner.js
Normal file
|
@ -0,0 +1,252 @@
|
|||
import globalize from 'lib/globalize';
|
||||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import { getParameterByName } from 'utils/url';
|
||||
|
||||
function isM3uVariant(type) {
|
||||
return ['nextpvr'].indexOf(type || '') !== -1;
|
||||
}
|
||||
|
||||
function fillTypes(view, currentId) {
|
||||
return ApiClient.getJSON(ApiClient.getUrl('LiveTv/TunerHosts/Types')).then(function (types) {
|
||||
const selectType = view.querySelector('.selectType');
|
||||
let html = '';
|
||||
html += types.map(function (tuner) {
|
||||
return '<option value="' + tuner.Id + '">' + tuner.Name + '</option>';
|
||||
}).join('');
|
||||
html += '<option value="other">';
|
||||
html += globalize.translate('TabOther');
|
||||
html += '</option>';
|
||||
selectType.innerHTML = html;
|
||||
selectType.disabled = currentId != null;
|
||||
selectType.value = '';
|
||||
onTypeChange.call(selectType);
|
||||
});
|
||||
}
|
||||
|
||||
function reload(view, providerId) {
|
||||
view.querySelector('.txtDevicePath').value = '';
|
||||
view.querySelector('.chkFavorite').checked = false;
|
||||
view.querySelector('.txtDevicePath').value = '';
|
||||
|
||||
if (providerId) {
|
||||
ApiClient.getNamedConfiguration('livetv').then(function (config) {
|
||||
const info = config.TunerHosts.filter(function (i) {
|
||||
return i.Id === providerId;
|
||||
})[0];
|
||||
fillTunerHostInfo(view, info);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fillTunerHostInfo(view, info) {
|
||||
const selectType = view.querySelector('.selectType');
|
||||
let type = info.Type || '';
|
||||
|
||||
if (info.Source && isM3uVariant(info.Source)) {
|
||||
type = info.Source;
|
||||
}
|
||||
|
||||
selectType.value = type;
|
||||
onTypeChange.call(selectType);
|
||||
view.querySelector('.txtDevicePath').value = info.Url || '';
|
||||
view.querySelector('.txtFriendlyName').value = info.FriendlyName || '';
|
||||
view.querySelector('.txtUserAgent').value = info.UserAgent || '';
|
||||
view.querySelector('.fldDeviceId').value = info.DeviceId || '';
|
||||
view.querySelector('.chkFavorite').checked = info.ImportFavoritesOnly;
|
||||
view.querySelector('.chkTranscode').checked = info.AllowHWTranscoding;
|
||||
view.querySelector('.chkStreamLoop').checked = info.EnableStreamLooping;
|
||||
view.querySelector('.chkFmp4Container').checked = info.AllowFmp4TranscodingContainer;
|
||||
view.querySelector('.chkStreamSharing').checked = info.AllowStreamSharing;
|
||||
view.querySelector('.chkIgnoreDts').checked = info.IgnoreDts;
|
||||
view.querySelector('.txtFallbackMaxStreamingBitrate').value = info.FallbackMaxStreamingBitrate / 1e6 || '30';
|
||||
view.querySelector('.txtTunerCount').value = info.TunerCount || '0';
|
||||
}
|
||||
|
||||
function submitForm(page) {
|
||||
loading.show();
|
||||
const info = {
|
||||
Type: page.querySelector('.selectType').value,
|
||||
Url: page.querySelector('.txtDevicePath').value || null,
|
||||
UserAgent: page.querySelector('.txtUserAgent').value || null,
|
||||
FriendlyName: page.querySelector('.txtFriendlyName').value || null,
|
||||
DeviceId: page.querySelector('.fldDeviceId').value || null,
|
||||
TunerCount: page.querySelector('.txtTunerCount').value || 0,
|
||||
FallbackMaxStreamingBitrate: parseInt(1e6 * parseFloat(page.querySelector('.txtFallbackMaxStreamingBitrate').value || '30'), 10),
|
||||
ImportFavoritesOnly: page.querySelector('.chkFavorite').checked,
|
||||
AllowHWTranscoding: page.querySelector('.chkTranscode').checked,
|
||||
AllowFmp4TranscodingContainer: page.querySelector('.chkFmp4Container').checked,
|
||||
AllowStreamSharing: page.querySelector('.chkStreamSharing').checked,
|
||||
EnableStreamLooping: page.querySelector('.chkStreamLoop').checked,
|
||||
IgnoreDts: page.querySelector('.chkIgnoreDts').checked
|
||||
};
|
||||
|
||||
if (isM3uVariant(info.Type)) {
|
||||
info.Source = info.Type;
|
||||
info.Type = 'm3u';
|
||||
}
|
||||
|
||||
if (getParameterByName('id')) {
|
||||
info.Id = getParameterByName('id');
|
||||
}
|
||||
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('LiveTv/TunerHosts'),
|
||||
data: JSON.stringify(info),
|
||||
contentType: 'application/json'
|
||||
}).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
Dashboard.navigate('dashboard/livetv');
|
||||
}, function () {
|
||||
loading.hide();
|
||||
Dashboard.alert({
|
||||
message: globalize.translate('ErrorSavingTvProvider')
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDetectedDevice() {
|
||||
return import('components/tunerPicker').then(({ default: TunerPicker }) => {
|
||||
return new TunerPicker().show({
|
||||
serverId: ApiClient.serverId()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function onTypeChange() {
|
||||
const value = this.value;
|
||||
const view = dom.parentWithClass(this, 'page');
|
||||
const mayIncludeUnsupportedDrmChannels = value === 'hdhomerun';
|
||||
const supportsTranscoding = value === 'hdhomerun';
|
||||
const supportsFavorites = value === 'hdhomerun';
|
||||
const supportsTunerIpAddress = value === 'hdhomerun';
|
||||
const supportsTunerFileOrUrl = value === 'm3u';
|
||||
const supportsStreamLooping = value === 'm3u';
|
||||
const supportsIgnoreDts = value === 'm3u';
|
||||
const supportsTunerCount = value === 'm3u';
|
||||
const supportsUserAgent = value === 'm3u';
|
||||
const supportsFmp4Container = value === 'm3u';
|
||||
const supportsStreamSharing = value === 'm3u';
|
||||
const supportsFallbackBitrate = value === 'm3u' || value === 'hdhomerun';
|
||||
const suppportsSubmit = value !== 'other';
|
||||
const supportsSelectablePath = supportsTunerFileOrUrl;
|
||||
const txtDevicePath = view.querySelector('.txtDevicePath');
|
||||
|
||||
if (supportsTunerIpAddress) {
|
||||
txtDevicePath.label(globalize.translate('LabelTunerIpAddress'));
|
||||
view.querySelector('.fldPath').classList.remove('hide');
|
||||
} else if (supportsTunerFileOrUrl) {
|
||||
txtDevicePath.label(globalize.translate('LabelFileOrUrl'));
|
||||
view.querySelector('.fldPath').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldPath').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsSelectablePath) {
|
||||
view.querySelector('.btnSelectPath').classList.remove('hide');
|
||||
view.querySelector('.txtDevicePath').setAttribute('required', 'required');
|
||||
} else {
|
||||
view.querySelector('.btnSelectPath').classList.add('hide');
|
||||
view.querySelector('.txtDevicePath').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (supportsUserAgent) {
|
||||
view.querySelector('.fldUserAgent').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldUserAgent').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsFavorites) {
|
||||
view.querySelector('.fldFavorites').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldFavorites').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsTranscoding) {
|
||||
view.querySelector('.fldTranscode').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldTranscode').classList.add('hide');
|
||||
}
|
||||
|
||||
view.querySelector('.fldFmp4Container').classList.toggle('hide', !supportsFmp4Container);
|
||||
view.querySelector('.fldStreamSharing').classList.toggle('hide', !supportsStreamSharing);
|
||||
view.querySelector('.fldFallbackMaxStreamingBitrate').classList.toggle('hide', !supportsFallbackBitrate);
|
||||
|
||||
if (supportsStreamLooping) {
|
||||
view.querySelector('.fldStreamLoop').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldStreamLoop').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsIgnoreDts) {
|
||||
view.querySelector('.fldIgnoreDts').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldIgnoreDts').classList.add('hide');
|
||||
}
|
||||
|
||||
if (supportsTunerCount) {
|
||||
view.querySelector('.fldTunerCount').classList.remove('hide');
|
||||
view.querySelector('.txtTunerCount').setAttribute('required', 'required');
|
||||
} else {
|
||||
view.querySelector('.fldTunerCount').classList.add('hide');
|
||||
view.querySelector('.txtTunerCount').removeAttribute('required');
|
||||
}
|
||||
|
||||
if (mayIncludeUnsupportedDrmChannels) {
|
||||
view.querySelector('.drmMessage').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.drmMessage').classList.add('hide');
|
||||
}
|
||||
|
||||
if (suppportsSubmit) {
|
||||
view.querySelector('.button-submit').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.button-submit').classList.add('hide');
|
||||
}
|
||||
}
|
||||
|
||||
export default function (view, params) {
|
||||
if (!params.id) {
|
||||
view.querySelector('.btnDetect').classList.remove('hide');
|
||||
}
|
||||
|
||||
view.addEventListener('viewshow', function () {
|
||||
const currentId = params.id;
|
||||
fillTypes(view, currentId).then(function () {
|
||||
reload(view, currentId);
|
||||
});
|
||||
});
|
||||
view.querySelector('form').addEventListener('submit', function (e) {
|
||||
submitForm(view);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
return false;
|
||||
});
|
||||
view.querySelector('.selectType').addEventListener('change', onTypeChange);
|
||||
view.querySelector('.btnDetect').addEventListener('click', function () {
|
||||
getDetectedDevice().then(function (info) {
|
||||
fillTunerHostInfo(view, info);
|
||||
});
|
||||
});
|
||||
view.querySelector('.btnSelectPath').addEventListener('click', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('.txtDevicePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
94
src/apps/dashboard/controllers/metadataImages.js
Normal file
94
src/apps/dashboard/controllers/metadataImages.js
Normal file
|
@ -0,0 +1,94 @@
|
|||
import { ImageResolution } from '@jellyfin/sdk/lib/generated-client/models/image-resolution';
|
||||
|
||||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
|
||||
import 'components/listview/listview.scss';
|
||||
|
||||
function populateImageResolutionOptions(select) {
|
||||
let html = '';
|
||||
[
|
||||
{
|
||||
name: globalize.translate('ResolutionMatchSource'),
|
||||
value: ImageResolution.MatchSource
|
||||
},
|
||||
{ name: '2160p', value: ImageResolution.P2160 },
|
||||
{ name: '1440p', value: ImageResolution.P1440 },
|
||||
{ name: '1080p', value: ImageResolution.P1080 },
|
||||
{ name: '720p', value: ImageResolution.P720 },
|
||||
{ name: '480p', value: ImageResolution.P480 },
|
||||
{ name: '360p', value: ImageResolution.P360 },
|
||||
{ name: '240p', value: ImageResolution.P240 },
|
||||
{ name: '144p', value: ImageResolution.P144 }
|
||||
].forEach(({ value, name }) => {
|
||||
html += `<option value="${value}">${name}</option>`;
|
||||
});
|
||||
select.innerHTML = html;
|
||||
}
|
||||
|
||||
function populateLanguages(select) {
|
||||
return ApiClient.getCultures().then(function(languages) {
|
||||
let html = '';
|
||||
html += "<option value=''></option>";
|
||||
for (let i = 0, length = languages.length; i < length; i++) {
|
||||
const culture = languages[i];
|
||||
html += "<option value='" + culture.TwoLetterISOLanguageName + "'>" + culture.DisplayName + '</option>';
|
||||
}
|
||||
select.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function populateCountries(select) {
|
||||
return ApiClient.getCountries().then(function(allCountries) {
|
||||
let html = '';
|
||||
html += "<option value=''></option>";
|
||||
for (let i = 0, length = allCountries.length; i < length; i++) {
|
||||
const culture = allCountries[i];
|
||||
html += "<option value='" + culture.TwoLetterISORegionName + "'>" + culture.DisplayName + '</option>';
|
||||
}
|
||||
select.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
function loadPage(page) {
|
||||
const promises = [
|
||||
ApiClient.getServerConfiguration(),
|
||||
populateLanguages(page.querySelector('#selectLanguage')),
|
||||
populateCountries(page.querySelector('#selectCountry'))
|
||||
];
|
||||
|
||||
populateImageResolutionOptions(page.querySelector('#txtChapterImageResolution'));
|
||||
|
||||
Promise.all(promises).then(function(responses) {
|
||||
const config = responses[0];
|
||||
page.querySelector('#selectLanguage').value = config.PreferredMetadataLanguage || '';
|
||||
page.querySelector('#selectCountry').value = config.MetadataCountryCode || '';
|
||||
page.querySelector('#valDummyChapterDuration').value = config.DummyChapterDuration || '0';
|
||||
page.querySelector('#txtChapterImageResolution').value = config.ChapterImageResolution || '';
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
const form = this;
|
||||
loading.show();
|
||||
ApiClient.getServerConfiguration().then(function(config) {
|
||||
config.PreferredMetadataLanguage = form.querySelector('#selectLanguage').value;
|
||||
config.MetadataCountryCode = form.querySelector('#selectCountry').value;
|
||||
config.DummyChapterDuration = form.querySelector('#valDummyChapterDuration').value;
|
||||
config.ChapterImageResolution = form.querySelector('#txtChapterImageResolution').value;
|
||||
ApiClient.updateServerConfiguration(config).then(Dashboard.processServerConfigurationUpdateResult);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
$(document).on('pageinit', '#metadataImagesConfigurationPage', function() {
|
||||
$('.metadataImagesConfigurationForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#metadataImagesConfigurationPage', function() {
|
||||
loading.show();
|
||||
loadPage(this);
|
||||
});
|
||||
|
44
src/apps/dashboard/controllers/metadataimages.html
Normal file
44
src/apps/dashboard/controllers/metadataimages.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
<div id="metadataImagesConfigurationPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${LabelMetadata}">
|
||||
|
||||
<div>
|
||||
|
||||
<div class="content-primary">
|
||||
|
||||
<form class="metadataImagesConfigurationForm">
|
||||
<div class="verticalSection">
|
||||
<h2 style="margin-top:0;">${HeaderPreferredMetadataLanguage}</h2>
|
||||
|
||||
<p style="margin:1.5em 0;">${DefaultMetadataLangaugeDescription}</p>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectLanguage" required="required" label="${LabelLanguage}"></select>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectCountry" required="required" label="${LabelCountry}"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="verticalSection">
|
||||
<h2>${HeaderDummyChapter}</h2>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="valDummyChapterDuration" label="${LabelDummyChapterDuration}" min="0"></input>
|
||||
<div class="fieldDescription">${LabelDummyChapterDurationHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="txtChapterImageResolution" label="${LabelChapterImageResolution}"></select>
|
||||
<div class="fieldDescription">
|
||||
<div>${LabelChapterImageResolutionHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
49
src/apps/dashboard/controllers/metadatanfo.html
Normal file
49
src/apps/dashboard/controllers/metadatanfo.html
Normal file
|
@ -0,0 +1,49 @@
|
|||
<div id="metadataNfoPage" data-role="page" class="page type-interior metadataConfigurationPage" data-title="${TabNfoSettings}">
|
||||
|
||||
<div>
|
||||
|
||||
<div class="content-primary">
|
||||
<form class="metadataNfoForm">
|
||||
|
||||
<p>${HeaderKodiMetadataHelp}</p>
|
||||
<br />
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" name="selectUser" id="selectUser" label="${LabelKodiMetadataUser}"></select>
|
||||
<div class="fieldDescription">${LabelKodiMetadataUserHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" name="selectReleaseDateFormat" id="selectReleaseDateFormat" label="${LabelKodiMetadataDateFormat}">
|
||||
<option value="yyyy-MM-dd">yyyy-MM-dd</option>
|
||||
</select>
|
||||
<div class="fieldDescription">${LabelKodiMetadataDateFormatHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkSaveImagePaths" />
|
||||
<span>${LabelKodiMetadataSaveImagePaths}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelKodiMetadataSaveImagePathsHelp}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnablePathSubstitution" />
|
||||
<span>${LabelKodiMetadataEnablePathSubstitution}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">
|
||||
<div>${LabelKodiMetadataEnablePathSubstitutionHelp}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableExtraThumbs" />
|
||||
<span>${LabelKodiMetadataEnableExtraThumbs}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelKodiMetadataEnableExtraThumbsHelp}</div>
|
||||
</div>
|
||||
<div><button is="emby-button" type="submit" class="raised button-submit block"><span>${Save}</span></button></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
61
src/apps/dashboard/controllers/metadatanfo.js
Normal file
61
src/apps/dashboard/controllers/metadatanfo.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import escapeHtml from 'escape-html';
|
||||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function loadPage(page, config, users) {
|
||||
let html = '<option value="" selected="selected">' + globalize.translate('None') + '</option>';
|
||||
html += users.map(function (user) {
|
||||
return '<option value="' + user.Id + '">' + escapeHtml(user.Name) + '</option>';
|
||||
}).join('');
|
||||
const elem = page.querySelector('#selectUser');
|
||||
elem.innerHTML = html;
|
||||
elem.value = config.UserId || '';
|
||||
page.querySelector('#selectReleaseDateFormat').value = config.ReleaseDateFormat;
|
||||
page.querySelector('#chkSaveImagePaths').checked = config.SaveImagePathsInNfo;
|
||||
page.querySelector('#chkEnablePathSubstitution').checked = config.EnablePathSubstitution;
|
||||
page.querySelector('#chkEnableExtraThumbs').checked = config.EnableExtraThumbsDuplication;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSubmit() {
|
||||
loading.show();
|
||||
const form = this;
|
||||
ApiClient.getNamedConfiguration(metadataKey).then(function (config) {
|
||||
config.UserId = form.querySelector('#selectUser').value || null;
|
||||
config.ReleaseDateFormat = form.querySelector('#selectReleaseDateFormat').value;
|
||||
config.SaveImagePathsInNfo = form.querySelector('#chkSaveImagePaths').checked;
|
||||
config.EnablePathSubstitution = form.querySelector('#chkEnablePathSubstitution').checked;
|
||||
config.EnableExtraThumbsDuplication = form.querySelector('#chkEnableExtraThumbs').checked;
|
||||
ApiClient.updateNamedConfiguration(metadataKey, config).then(function () {
|
||||
Dashboard.processServerConfigurationUpdateResult();
|
||||
showConfirmMessage();
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
function showConfirmMessage() {
|
||||
const msg = [];
|
||||
msg.push(globalize.translate('MetadataSettingChangeHelp'));
|
||||
alert({
|
||||
text: msg.join('<br/><br/>')
|
||||
});
|
||||
}
|
||||
|
||||
const metadataKey = 'xbmcmetadata';
|
||||
$(document).on('pageinit', '#metadataNfoPage', function () {
|
||||
$('.metadataNfoForm').off('submit', onSubmit).on('submit', onSubmit);
|
||||
}).on('pageshow', '#metadataNfoPage', function () {
|
||||
loading.show();
|
||||
const page = this;
|
||||
const promise1 = ApiClient.getUsers();
|
||||
const promise2 = ApiClient.getNamedConfiguration(metadataKey);
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
loadPage(page, responses[1], responses[0]);
|
||||
});
|
||||
});
|
||||
|
156
src/apps/dashboard/controllers/networking.html
Normal file
156
src/apps/dashboard/controllers/networking.html
Normal file
|
@ -0,0 +1,156 @@
|
|||
<div id="networkingPage" data-role="page" class="page type-interior advancedConfigurationPage" data-title="${TabNetworking}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<form class="dashboardHostingForm">
|
||||
<div class="verticalSection verticalSection-extrabottompadding">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabNetworking}</h2>
|
||||
</div>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderServerAddressSettings}</h3></legend>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtPortNumber" label="${LabelLocalHttpServerPortNumber}" pattern="[0-9]*" required="required" min="1" max="65535" />
|
||||
<div class="fieldDescription">${LabelLocalHttpServerPortNumberHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableHttps" />
|
||||
<span>${LabelEnableHttps}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableHttpsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="number" id="txtHttpsPort" pattern="[0-9]*" required="required" min="1" max="65535" label="${LabelHttpsPort}" />
|
||||
<div class="fieldDescription">${LabelHttpsPortHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldBaseUrl">
|
||||
<input is="emby-input" id="txtBaseUrl" type="text" label="${LabelBaseUrl}" />
|
||||
<div class="fieldDescription">${LabelBaseUrlHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtLocalAddress" label="${LabelBindToLocalNetworkAddress}" />
|
||||
<div class="fieldDescription">${LabelBindToLocalNetworkAddressHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtLanNetworks" label="${LabelLanNetworks}" />
|
||||
<div class="fieldDescription">${LanNetworksHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtKnownProxies" label="${LabelKnownProxies}" />
|
||||
<div class="fieldDescription">${KnownProxiesHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderHttpsSettings}</h3></legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRequireHttps" />
|
||||
<span>${LabelRequireHttps}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelRequireHttpsHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldCertificatePath">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<div style="flex-grow:1;">
|
||||
<input is="emby-input" type="text" id="txtCertificatePath" label="${LabelCustomCertificatePath}" autocomplete="off" />
|
||||
</div>
|
||||
<button type="button" is="paper-icon-button-light" id="btnSelectCertPath" title="${ButtonSelectDirectory}" class="emby-input-iconbutton"><span class="material-icons search" aria-hidden="true"></span></button>
|
||||
</div>
|
||||
<div class="fieldDescription">${LabelCustomCertificatePathHelp}</div>
|
||||
</div>
|
||||
|
||||
<div class="inputContainer fldCertPassword">
|
||||
<input is="emby-input" id="txtCertPassword" type="password" label="${LabelCertificatePassword}" autocomplete="new-password" />
|
||||
<div class="fieldDescription">${LabelCertificatePasswordHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderRemoteAccessSettings}</h3></legend>
|
||||
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkRemoteAccess" />
|
||||
<span>${AllowRemoteAccess}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${AllowRemoteAccessHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer fldExternalAddressFilter hide">
|
||||
<input is="emby-input" type="text" id="txtExternalAddressFilter" label="${LabelAllowedRemoteAddresses}" />
|
||||
<div class="fieldDescription">${AllowedRemoteAddressesHelp}</div>
|
||||
</div>
|
||||
<div class="selectContainer fldExternalAddressFilterMode hide">
|
||||
<select is="emby-select" id="selectExternalAddressFilterMode" label="${LabelAllowedRemoteAddressesMode}">
|
||||
<option value="whitelist">${Whitelist}</option>
|
||||
<option value="blacklist">${Blacklist}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inputContainer fldPublicHttpPort hide">
|
||||
<input is="emby-input" type="number" label="${LabelPublicHttpPort}" id="txtPublicHttpPort" pattern="[0-9]*" required="required" min="1" max="65535" />
|
||||
<div class="fieldDescription">${LabelPublicHttpPortHelp}</div>
|
||||
</div>
|
||||
<div class="inputContainer fldPublicHttpsPort hide">
|
||||
<input is="emby-input" type="number" id="txtPublicHttpsPort" pattern="[0-9]*" required="required" min="1" max="65535" label="${LabelPublicHttpsPort}" />
|
||||
<div class="fieldDescription">${LabelPublicHttpsPortHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderNetworking}</h3></legend>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableIP4" />
|
||||
<span>${LabelEnableIP4}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableIP4Help}</div>
|
||||
</div>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkEnableIP6" />
|
||||
<span>${LabelEnableIP6}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelEnableIP6Help}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding hide'>
|
||||
<legend><h3>${HeaderAutoDiscovery}</h3></legend>
|
||||
<div class="checkboxContainer checkboxContainer-withDescription">
|
||||
<label>
|
||||
<input type="checkbox" is="emby-checkbox" id="chkAutodiscovery" />
|
||||
<span>${LabelAutomaticDiscovery}</span>
|
||||
</label>
|
||||
<div class="fieldDescription checkboxFieldDescription">${LabelAutomaticDiscoveryHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class='verticalSection verticalSection-extrabottompadding'>
|
||||
<legend><h3>${HeaderPortRanges}</h3></legend>
|
||||
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" type="text" id="txtPublishedServer" label="${LabelPublishedServerUri}" />
|
||||
<div class="fieldDescription">${LabelPublishedServerUriHelp}</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block">
|
||||
<span>${Save}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
186
src/apps/dashboard/controllers/networking.js
Normal file
186
src/apps/dashboard/controllers/networking.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import Dashboard from 'utils/dashboard';
|
||||
import alert from 'components/alert';
|
||||
|
||||
function onSubmit(e) {
|
||||
const form = this;
|
||||
const localAddress = form.querySelector('#txtLocalAddress').value;
|
||||
confirmSelections(localAddress, function () {
|
||||
const validationResult = getValidationAlert(form);
|
||||
|
||||
if (validationResult) {
|
||||
showAlertText(validationResult);
|
||||
return;
|
||||
}
|
||||
|
||||
validateHttps(form).then(function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
config.LocalNetworkSubnets = form.querySelector('#txtLanNetworks').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.RemoteIPFilter = form.querySelector('#txtExternalAddressFilter').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.KnownProxies = form.querySelector('#txtKnownProxies').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
config.LocalNetworkAddresses = form.querySelector('#txtLocalAddress').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
|
||||
config.PublishedServerUriBySubnet = form.querySelector('#txtPublishedServer').value.split(',').map(function (s) {
|
||||
return s.trim();
|
||||
}).filter(function (s) {
|
||||
return s.length > 0;
|
||||
});
|
||||
|
||||
config.IsRemoteIPFilterBlacklist = form.querySelector('#selectExternalAddressFilterMode').value === 'blacklist';
|
||||
config.PublicHttpPort = form.querySelector('#txtPublicHttpPort').value;
|
||||
config.PublicHttpsPort = form.querySelector('#txtPublicHttpsPort').value;
|
||||
config.InternalHttpPort = form.querySelector('#txtPortNumber').value;
|
||||
config.InternalHttpsPort = form.querySelector('#txtHttpsPort').value;
|
||||
config.EnableHttps = form.querySelector('#chkEnableHttps').checked;
|
||||
config.RequireHttps = form.querySelector('#chkRequireHttps').checked;
|
||||
config.BaseUrl = form.querySelector('#txtBaseUrl').value;
|
||||
config.EnableRemoteAccess = form.querySelector('#chkRemoteAccess').checked;
|
||||
config.CertificatePath = form.querySelector('#txtCertificatePath').value || null;
|
||||
config.CertificatePassword = form.querySelector('#txtCertPassword').value || null;
|
||||
config.AutoDiscovery = form.querySelector('#chkAutodiscovery').checked;
|
||||
config.EnableIPv6 = form.querySelector('#chkEnableIP6').checked;
|
||||
config.EnableIPv4 = form.querySelector('#chkEnableIP4').checked;
|
||||
ApiClient.updateNamedConfiguration('network', config).then(Dashboard.processServerConfigurationUpdateResult, Dashboard.processErrorResponse);
|
||||
});
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
function triggerChange(select) {
|
||||
const evt = new Event('change', { bubbles: false, cancelable: true });
|
||||
select.dispatchEvent(evt);
|
||||
}
|
||||
|
||||
function getValidationAlert(form) {
|
||||
if (form.querySelector('#txtPublicHttpPort').value === form.querySelector('#txtPublicHttpsPort').value) {
|
||||
return 'The public http and https ports must be different.';
|
||||
}
|
||||
|
||||
if (form.querySelector('#txtPortNumber').value === form.querySelector('#txtHttpsPort').value) {
|
||||
return 'The http and https ports must be different.';
|
||||
}
|
||||
|
||||
if (!form.querySelector('#chkEnableIP6').checked && !form.querySelector('#chkEnableIP4').checked) {
|
||||
return 'Either IPv4 or IPv6 need to be checked.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateHttps(form) {
|
||||
const certPath = form.querySelector('#txtCertificatePath').value || null;
|
||||
const httpsEnabled = form.querySelector('#chkEnableHttps').checked;
|
||||
|
||||
if (httpsEnabled && !certPath) {
|
||||
return showAlertText({
|
||||
title: globalize.translate('TitleHostingSettings'),
|
||||
text: globalize.translate('HttpsRequiresCert')
|
||||
}).then(Promise.reject);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function showAlertText(options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
alert(options).then(resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
function confirmSelections(localAddress, callback) {
|
||||
if (localAddress) {
|
||||
showAlertText({
|
||||
title: globalize.translate('TitleHostingSettings'),
|
||||
text: globalize.translate('SettingsWarning')
|
||||
}).then(callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
function loadPage(page, config) {
|
||||
page.querySelector('#txtPortNumber').value = config.InternalHttpPort;
|
||||
page.querySelector('#txtPublicHttpPort').value = config.PublicHttpPort;
|
||||
page.querySelector('#txtPublicHttpsPort').value = config.PublicHttpsPort;
|
||||
page.querySelector('#txtLocalAddress').value = (config.LocalNetworkAddresses || []).join(', ');
|
||||
page.querySelector('#txtLanNetworks').value = (config.LocalNetworkSubnets || []).join(', ');
|
||||
page.querySelector('#txtKnownProxies').value = (config.KnownProxies || []).join(', ');
|
||||
page.querySelector('#txtExternalAddressFilter').value = (config.RemoteIPFilter || []).join(', ');
|
||||
page.querySelector('#selectExternalAddressFilterMode').value = config.IsRemoteIPFilterBlacklist ? 'blacklist' : 'whitelist';
|
||||
page.querySelector('#chkRemoteAccess').checked = config.EnableRemoteAccess == null || config.EnableRemoteAccess;
|
||||
page.querySelector('#txtHttpsPort').value = config.InternalHttpsPort;
|
||||
page.querySelector('#chkEnableHttps').checked = config.EnableHttps;
|
||||
page.querySelector('#chkRequireHttps').checked = config.RequireHttps;
|
||||
page.querySelector('#txtBaseUrl').value = config.BaseUrl || '';
|
||||
const txtCertificatePath = page.querySelector('#txtCertificatePath');
|
||||
txtCertificatePath.value = config.CertificatePath || '';
|
||||
page.querySelector('#txtCertPassword').value = config.CertificatePassword || '';
|
||||
triggerChange(page.querySelector('#chkRemoteAccess'));
|
||||
page.querySelector('#chkAutodiscovery').checked = config.AutoDiscovery;
|
||||
page.querySelector('#chkEnableIP6').checked = config.EnableIPv6;
|
||||
page.querySelector('#chkEnableIP4').checked = config.EnableIPv4;
|
||||
page.querySelector('#txtPublishedServer').value = (config.PublishedServerUriBySubnet || []).join(', ');
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
view.querySelector('#chkRemoteAccess').addEventListener('change', function () {
|
||||
if (this.checked) {
|
||||
view.querySelector('.fldExternalAddressFilter').classList.remove('hide');
|
||||
view.querySelector('.fldExternalAddressFilterMode').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.remove('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.remove('hide');
|
||||
} else {
|
||||
view.querySelector('.fldExternalAddressFilter').classList.add('hide');
|
||||
view.querySelector('.fldExternalAddressFilterMode').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpPort').classList.add('hide');
|
||||
view.querySelector('.fldPublicHttpsPort').classList.add('hide');
|
||||
}
|
||||
});
|
||||
view.querySelector('#btnSelectCertPath').addEventListener('click', function () {
|
||||
import('components/directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
|
||||
const picker = new DirectoryBrowser();
|
||||
picker.show({
|
||||
includeFiles: true,
|
||||
includeDirectories: true,
|
||||
callback: function (path) {
|
||||
if (path) {
|
||||
view.querySelector('#txtCertificatePath').value = path;
|
||||
}
|
||||
|
||||
picker.close();
|
||||
},
|
||||
header: globalize.translate('HeaderSelectCertificatePath')
|
||||
});
|
||||
});
|
||||
});
|
||||
view.querySelector('.dashboardHostingForm').addEventListener('submit', onSubmit);
|
||||
view.addEventListener('viewshow', function () {
|
||||
loading.show();
|
||||
ApiClient.getNamedConfiguration('network').then(function (config) {
|
||||
loadPage(view, config);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
42
src/apps/dashboard/controllers/playback.html
Normal file
42
src/apps/dashboard/controllers/playback.html
Normal file
|
@ -0,0 +1,42 @@
|
|||
<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>
|
39
src/apps/dashboard/controllers/playback.js
Normal file
39
src/apps/dashboard/controllers/playback.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
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);
|
||||
});
|
||||
});
|
17
src/apps/dashboard/controllers/plugins/available/index.html
Normal file
17
src/apps/dashboard/controllers/plugins/available/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<div id="pluginCatalogPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabCatalog}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabCatalog}</h2>
|
||||
<a is="emby-linkbutton" class="fab" href="#/dashboard/plugins/repositories" style="margin-left:1em;" title="${Settings}">
|
||||
<span class="material-icons settings" aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input id="txtSearchPlugins" name="txtSearchPlugins" type="text" is="emby-input" label="${Search}" />
|
||||
</div>
|
||||
<div id="noPlugins" class="hide">${MessageNoAvailablePlugins}</div>
|
||||
<div id="pluginTiles" style="text-align:left;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
163
src/apps/dashboard/controllers/plugins/available/index.js
Normal file
163
src/apps/dashboard/controllers/plugins/available/index.js
Normal file
|
@ -0,0 +1,163 @@
|
|||
import escapeHTML from 'escape-html';
|
||||
|
||||
import { CATEGORY_LABELS } from 'apps/dashboard/features/plugins/constants/categoryLabels';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
|
||||
function reloadList(page) {
|
||||
loading.show();
|
||||
const promise1 = ApiClient.getAvailablePlugins();
|
||||
const promise2 = ApiClient.getInstalledPlugins();
|
||||
Promise.all([promise1, promise2]).then(function (responses) {
|
||||
populateList({
|
||||
catalogElement: page.querySelector('#pluginTiles'),
|
||||
noItemsElement: page.querySelector('#noPlugins'),
|
||||
availablePlugins: responses[0],
|
||||
installedPlugins: responses[1]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getHeaderText(category) {
|
||||
const categoryKey = category.replaceAll(' ', '');
|
||||
|
||||
if (CATEGORY_LABELS[categoryKey]) {
|
||||
return globalize.translate(CATEGORY_LABELS[categoryKey]);
|
||||
}
|
||||
|
||||
console.warn('[AvailablePlugins] unmapped category label', category);
|
||||
return category;
|
||||
}
|
||||
|
||||
function populateList(options) {
|
||||
const availablePlugins = options.availablePlugins;
|
||||
const installedPlugins = options.installedPlugins;
|
||||
|
||||
availablePlugins.forEach(function (plugin, index, array) {
|
||||
plugin.category = plugin.category || 'Other';
|
||||
plugin.categoryDisplayName = getHeaderText(plugin.category);
|
||||
array[index] = plugin;
|
||||
});
|
||||
|
||||
availablePlugins.sort(function (a, b) {
|
||||
if (a.category > b.category) {
|
||||
return 1;
|
||||
} else if (b.category > a.category) {
|
||||
return -1;
|
||||
}
|
||||
if (a.name > b.name) {
|
||||
return 1;
|
||||
} else if (b.name > a.name) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
let currentCategory = null;
|
||||
let html = '';
|
||||
|
||||
for (const plugin of availablePlugins) {
|
||||
const category = plugin.categoryDisplayName;
|
||||
if (category != currentCategory) {
|
||||
if (currentCategory) {
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="verticalSection">';
|
||||
html += '<h2 class="sectionTitle sectionTitle-cards">' + escapeHTML(category) + '</h2>';
|
||||
html += '<div class="itemsContainer vertical-wrap">';
|
||||
currentCategory = category;
|
||||
}
|
||||
html += getPluginHtml(plugin, options, installedPlugins);
|
||||
}
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
|
||||
if (!availablePlugins.length && options.noItemsElement) {
|
||||
options.noItemsElement.classList.remove('hide');
|
||||
}
|
||||
|
||||
const searchBar = document.getElementById('txtSearchPlugins');
|
||||
if (searchBar) {
|
||||
searchBar.addEventListener('input', () => onSearchBarType(searchBar));
|
||||
}
|
||||
|
||||
options.catalogElement.innerHTML = html;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function onSearchBarType(searchBar) {
|
||||
const filter = searchBar.value.toLowerCase();
|
||||
for (const header of document.querySelectorAll('div .verticalSection')) {
|
||||
// keep track of shown cards after each search
|
||||
let shown = 0;
|
||||
for (const card of header.querySelectorAll('div .card')) {
|
||||
if (filter && filter != '' && !card.textContent.toLowerCase().includes(filter)) {
|
||||
card.style.display = 'none';
|
||||
} else {
|
||||
card.style.display = 'unset';
|
||||
shown++;
|
||||
}
|
||||
}
|
||||
// hide title if no cards are shown
|
||||
if (shown <= 0) {
|
||||
header.style.display = 'none';
|
||||
} else {
|
||||
header.style.display = 'unset';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPluginHtml(plugin, options, installedPlugins) {
|
||||
let html = '';
|
||||
let href = plugin.externalUrl ? plugin.externalUrl :
|
||||
`#/dashboard/plugins/${plugin.guid}?name=${encodeURIComponent(plugin.name)}`;
|
||||
|
||||
if (options.context) {
|
||||
href += '&context=' + options.context;
|
||||
}
|
||||
|
||||
const target = plugin.externalUrl ? ' target="_blank"' : '';
|
||||
html += "<div class='card backdropCard'>";
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable visualCardBox-cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
html += `<a class="cardImageContainer" is="emby-linkbutton" style="margin:0;padding:0" href="${href}" ${target}>`;
|
||||
|
||||
if (plugin.imageUrl) {
|
||||
html += `<img src="${escapeHTML(plugin.imageUrl)}" style="width:100%" />`;
|
||||
} else {
|
||||
html += `<div class="cardImage flex align-items-center justify-content-center ${getDefaultBackgroundClass()}">`;
|
||||
html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter">';
|
||||
html += "<div class='cardText'>";
|
||||
html += escapeHTML(plugin.name);
|
||||
html += '</div>';
|
||||
const installedPlugin = installedPlugins.find(installed => installed.Id === plugin.guid);
|
||||
html += "<div class='cardText cardText-secondary'>";
|
||||
html += installedPlugin ? globalize.translate('LabelVersionInstalled', installedPlugin.Version) : ' ';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
export default function (view) {
|
||||
view.addEventListener('viewshow', function () {
|
||||
reloadList(this);
|
||||
});
|
||||
}
|
13
src/apps/dashboard/controllers/plugins/installed/index.html
Normal file
13
src/apps/dashboard/controllers/plugins/installed/index.html
Normal file
|
@ -0,0 +1,13 @@
|
|||
<div id="pluginsPage" data-role="page" class="page type-interior pluginConfigurationPage fullWidthContent" data-title="${TabPlugins}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabMyPlugins}</h2>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input id="txtSearchPlugins" name="txtSearchPlugins" type="text" is="emby-input" label="${Search}" />
|
||||
</div>
|
||||
<div class="installedPlugins"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
251
src/apps/dashboard/controllers/plugins/installed/index.js
Normal file
251
src/apps/dashboard/controllers/plugins/installed/index.js
Normal file
|
@ -0,0 +1,251 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'components/cardbuilder/card.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import Dashboard, { pageIdOn } from 'utils/dashboard';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getDefaultBackgroundClass } from 'components/cardbuilder/cardBuilderUtils';
|
||||
|
||||
function deletePlugin(page, uniqueid, version, name) {
|
||||
const msg = globalize.translate('UninstallPluginConfirmation', name);
|
||||
|
||||
confirm({
|
||||
title: globalize.translate('HeaderUninstallPlugin'),
|
||||
text: msg,
|
||||
primary: 'delete',
|
||||
confirmText: globalize.translate('HeaderUninstallPlugin')
|
||||
}).then(function () {
|
||||
loading.show();
|
||||
ApiClient.uninstallPluginByVersion(uniqueid, version).then(function () {
|
||||
reloadList(page);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function enablePlugin(page, uniqueid, version) {
|
||||
loading.show();
|
||||
ApiClient.enablePlugin(uniqueid, version).then(function () {
|
||||
reloadList(page);
|
||||
});
|
||||
}
|
||||
|
||||
function disablePlugin(page, uniqueid, version) {
|
||||
loading.show();
|
||||
ApiClient.disablePlugin(uniqueid, version).then(function () {
|
||||
reloadList(page);
|
||||
});
|
||||
}
|
||||
|
||||
function showNoConfigurationMessage() {
|
||||
Dashboard.alert({
|
||||
message: globalize.translate('MessageNoPluginConfiguration')
|
||||
});
|
||||
}
|
||||
|
||||
function showConnectMessage() {
|
||||
Dashboard.alert({
|
||||
message: globalize.translate('MessagePluginConfigurationRequiresLocalAccess')
|
||||
});
|
||||
}
|
||||
|
||||
function getPluginCardHtml(plugin, pluginConfigurationPages) {
|
||||
const configPage = pluginConfigurationPages.filter(function (pluginConfigurationPage) {
|
||||
return pluginConfigurationPage.PluginId == plugin.Id;
|
||||
})[0];
|
||||
|
||||
const configPageUrl = configPage ? Dashboard.getPluginUrl(configPage.Name) : null;
|
||||
let html = '';
|
||||
|
||||
html += `<div data-id='${plugin.Id}' data-version='${plugin.Version}' data-name='${plugin.Name}' data-removable='${plugin.CanUninstall}' data-status='${plugin.Status}' class='card backdropCard'>`;
|
||||
html += '<div class="cardBox visualCardBox">';
|
||||
html += '<div class="cardScalable">';
|
||||
html += '<div class="cardPadder cardPadder-backdrop"></div>';
|
||||
html += '<div class="cardContent">';
|
||||
if (configPageUrl) {
|
||||
html += `<a class="cardImageContainer" is="emby-linkbutton" style="margin:0;padding:0" href="${configPageUrl}">`;
|
||||
} else {
|
||||
html += '<div class="cardImageContainer noConfigPluginCard noHoverEffect emby-button" style="margin:0;padding:0">';
|
||||
}
|
||||
|
||||
if (plugin.HasImage) {
|
||||
const imageUrl = ApiClient.getUrl(`/Plugins/${plugin.Id}/${plugin.Version}/Image`);
|
||||
html += `<img src="${imageUrl}" style="width:100%" />`;
|
||||
} else {
|
||||
html += `<div class="cardImage flex align-items-center justify-content-center ${getDefaultBackgroundClass()}">`;
|
||||
html += '<span class="cardImageIcon material-icons extension" aria-hidden="true"></span>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += configPageUrl ? '</a>' : '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '<div class="cardFooter">';
|
||||
|
||||
if (configPage || plugin.CanUninstall) {
|
||||
if (globalize.getIsRTL()) {
|
||||
html += '<div style="text-align:left; float:left;padding-top:5px;">';
|
||||
} else {
|
||||
html += '<div style="text-align:right; float:right;padding-top:5px;">';
|
||||
}
|
||||
html += '<button type="button" is="paper-icon-button-light" class="btnCardMenu autoSize"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '<div class="cardText">';
|
||||
html += `${plugin.Name}<span class='cardText cardText-secondary'>${plugin.Version}</span>`;
|
||||
html += '</div>';
|
||||
html += `<div class="cardText">${globalize.translate('LabelStatus')} ${plugin.Status}</div>`;
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderPlugins(page, plugins) {
|
||||
ApiClient.getJSON(ApiClient.getUrl('web/configurationpages') + '?pageType=PluginConfiguration').then(function (configPages) {
|
||||
populateList(page, plugins, configPages);
|
||||
});
|
||||
}
|
||||
|
||||
function populateList(page, plugins, pluginConfigurationPages) {
|
||||
plugins = plugins.sort(function (plugin1, plugin2) {
|
||||
if (plugin1.Name > plugin2.Name) {
|
||||
return 1;
|
||||
}
|
||||
return -1;
|
||||
});
|
||||
|
||||
let html = plugins.map(function (p) {
|
||||
return getPluginCardHtml(p, pluginConfigurationPages);
|
||||
}).join('');
|
||||
|
||||
const installedPluginsElement = page.querySelector('.installedPlugins');
|
||||
installedPluginsElement.removeEventListener('click', onInstalledPluginsClick);
|
||||
installedPluginsElement.addEventListener('click', onInstalledPluginsClick);
|
||||
|
||||
if (plugins.length) {
|
||||
installedPluginsElement.classList.add('itemsContainer');
|
||||
installedPluginsElement.classList.add('vertical-wrap');
|
||||
} else {
|
||||
html += '<div class="centerMessage">';
|
||||
html += '<h1>' + globalize.translate('MessageNoPluginsInstalled') + '</h1>';
|
||||
html += '<p><a is="emby-linkbutton" class="button-link" href="#/dashboard/plugins/catalog">';
|
||||
html += globalize.translate('MessageBrowsePluginCatalog');
|
||||
html += '</a></p>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
// add search box listener
|
||||
const searchBar = page.querySelector('#txtSearchPlugins');
|
||||
if (searchBar) {
|
||||
searchBar.addEventListener('input', () => onFilterType(page, searchBar));
|
||||
}
|
||||
|
||||
installedPluginsElement.innerHTML = html;
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function showPluginMenu(page, elem) {
|
||||
const card = dom.parentWithClass(elem, 'card');
|
||||
const id = card.getAttribute('data-id');
|
||||
const name = card.getAttribute('data-name');
|
||||
const removable = card.getAttribute('data-removable');
|
||||
const configHref = card.querySelector('.cardImageContainer').getAttribute('href');
|
||||
const status = card.getAttribute('data-status');
|
||||
const version = card.getAttribute('data-version');
|
||||
const menuItems = [];
|
||||
|
||||
if (configHref) {
|
||||
menuItems.push({
|
||||
name: globalize.translate('Settings'),
|
||||
id: 'open',
|
||||
icon: 'mode_edit'
|
||||
});
|
||||
}
|
||||
|
||||
if (removable === 'true') {
|
||||
if (status === 'Disabled') {
|
||||
menuItems.push({
|
||||
name: globalize.translate('EnablePlugin'),
|
||||
id: 'enable',
|
||||
icon: 'check_circle_outline'
|
||||
});
|
||||
}
|
||||
|
||||
if (status === 'Active') {
|
||||
menuItems.push({
|
||||
name: globalize.translate('DisablePlugin'),
|
||||
id: 'disable',
|
||||
icon: 'do_not_disturb'
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.push({
|
||||
name: globalize.translate('ButtonUninstall'),
|
||||
id: 'delete',
|
||||
icon: 'delete'
|
||||
});
|
||||
}
|
||||
|
||||
import('components/actionSheet/actionSheet').then((actionsheet) => {
|
||||
actionsheet.show({
|
||||
items: menuItems,
|
||||
positionTo: elem,
|
||||
callback: function (resultId) {
|
||||
switch (resultId) {
|
||||
case 'open':
|
||||
Dashboard.navigate(configHref);
|
||||
break;
|
||||
case 'delete':
|
||||
deletePlugin(page, id, version, name);
|
||||
break;
|
||||
case 'enable':
|
||||
enablePlugin(page, id, version);
|
||||
break;
|
||||
case 'disable':
|
||||
disablePlugin(page, id, version);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reloadList(page) {
|
||||
loading.show();
|
||||
ApiClient.getInstalledPlugins().then(function (plugins) {
|
||||
renderPlugins(page, plugins);
|
||||
});
|
||||
}
|
||||
|
||||
function onInstalledPluginsClick(e) {
|
||||
if (dom.parentWithClass(e.target, 'noConfigPluginCard')) {
|
||||
showNoConfigurationMessage();
|
||||
} else if (dom.parentWithClass(e.target, 'connectModePluginCard')) {
|
||||
showConnectMessage();
|
||||
} else {
|
||||
const btnCardMenu = dom.parentWithClass(e.target, 'btnCardMenu');
|
||||
if (btnCardMenu) {
|
||||
showPluginMenu(dom.parentWithClass(btnCardMenu, 'page'), btnCardMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onFilterType(page, searchBar) {
|
||||
const filter = searchBar.value.toLowerCase();
|
||||
for (const card of page.querySelectorAll('.card')) {
|
||||
if (filter && filter != '' && !card.textContent.toLowerCase().includes(filter)) {
|
||||
card.style.display = 'none';
|
||||
} else {
|
||||
card.style.display = 'unset';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageIdOn('pageshow', 'pluginsPage', function () {
|
||||
reloadList(this);
|
||||
});
|
||||
|
||||
window.PluginsPage = {
|
||||
renderPlugins: renderPlugins
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
<div id="repositories" data-role="page" class="page type-interior fullWidthContent" data-title="${TabRepositories}">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle">${TabRepositories}</h2>
|
||||
<button is="emby-button" type="button" class="fab btnNewRepository submit" style="margin-left:1em;" title="${Add}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="repositories"></div>
|
||||
|
||||
<div id="none" class="noItemsMessage centerMessage hide">
|
||||
<h1>${MessageNoRepositories}</h1>
|
||||
<p>${MessageAddRepository}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
190
src/apps/dashboard/controllers/plugins/repositories/index.js
Normal file
190
src/apps/dashboard/controllers/plugins/repositories/index.js
Normal file
|
@ -0,0 +1,190 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dialogHelper from 'components/dialogHelper/dialogHelper';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-checkbox/emby-checkbox';
|
||||
import 'elements/emby-select/emby-select';
|
||||
|
||||
import 'components/formdialog.scss';
|
||||
import 'components/listview/listview.scss';
|
||||
|
||||
let repositories = [];
|
||||
|
||||
function reloadList(page) {
|
||||
loading.show();
|
||||
ApiClient.getJSON(ApiClient.getUrl('Repositories')).then(list => {
|
||||
repositories = list;
|
||||
populateList({
|
||||
listElement: page.querySelector('#repositories'),
|
||||
noneElement: page.querySelector('#none'),
|
||||
repositories: repositories
|
||||
});
|
||||
}).catch(e => {
|
||||
console.error('error loading repositories', e);
|
||||
page.querySelector('#none').classList.remove('hide');
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function saveList(page) {
|
||||
loading.show();
|
||||
ApiClient.ajax({
|
||||
type: 'POST',
|
||||
url: ApiClient.getUrl('Repositories'),
|
||||
data: JSON.stringify(repositories),
|
||||
contentType: 'application/json'
|
||||
}).then(() => {
|
||||
reloadList(page);
|
||||
}).catch(e => {
|
||||
console.error('error saving repositories', e);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function populateList(options) {
|
||||
const paperList = document.createElement('div');
|
||||
paperList.className = 'paperList';
|
||||
|
||||
options.repositories.forEach(repo => {
|
||||
paperList.appendChild(getRepositoryElement(repo));
|
||||
});
|
||||
|
||||
if (!options.repositories.length) {
|
||||
options.noneElement.classList.remove('hide');
|
||||
} else {
|
||||
options.noneElement.classList.add('hide');
|
||||
}
|
||||
|
||||
options.listElement.innerHTML = '';
|
||||
options.listElement.appendChild(paperList);
|
||||
loading.hide();
|
||||
}
|
||||
|
||||
function getRepositoryElement(repository) {
|
||||
const listItem = document.createElement('div');
|
||||
listItem.className = 'listItem listItem-border';
|
||||
|
||||
const repoLink = document.createElement('a', 'emby-linkbutton');
|
||||
repoLink.classList.add('clearLink', 'listItemIconContainer');
|
||||
repoLink.style.margin = '0';
|
||||
repoLink.style.padding = '0';
|
||||
repoLink.rel = 'noopener noreferrer';
|
||||
repoLink.target = '_blank';
|
||||
repoLink.href = repository.Url;
|
||||
repoLink.innerHTML = '<span class="material-icons listItemIcon open_in_new" aria-hidden="true"></span>';
|
||||
listItem.appendChild(repoLink);
|
||||
|
||||
const body = document.createElement('div');
|
||||
body.className = 'listItemBody two-line';
|
||||
|
||||
const name = document.createElement('h3');
|
||||
name.className = 'listItemBodyText';
|
||||
name.innerText = repository.Name;
|
||||
body.appendChild(name);
|
||||
|
||||
const url = document.createElement('div');
|
||||
url.className = 'listItemBodyText secondary';
|
||||
url.innerText = repository.Url;
|
||||
body.appendChild(url);
|
||||
|
||||
listItem.appendChild(body);
|
||||
|
||||
const button = document.createElement('button', 'paper-icon-button-light');
|
||||
button.type = 'button';
|
||||
button.classList.add('btnDelete');
|
||||
button.id = repository.Url;
|
||||
button.title = globalize.translate('Delete');
|
||||
button.innerHTML = '<span class="material-icons delete" aria-hidden="true"></span>';
|
||||
listItem.appendChild(button);
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
export default function(view) {
|
||||
view.addEventListener('viewshow', function () {
|
||||
reloadList(this);
|
||||
|
||||
const save = this;
|
||||
$('#repositories', view).on('click', '.btnDelete', function() {
|
||||
const button = this;
|
||||
repositories = repositories.filter(function (r) {
|
||||
return r.Url !== button.id;
|
||||
});
|
||||
|
||||
saveList(save);
|
||||
});
|
||||
});
|
||||
|
||||
view.querySelector('.btnNewRepository').addEventListener('click', () => {
|
||||
const dialog = dialogHelper.createDialog({
|
||||
scrollY: false,
|
||||
size: 'large',
|
||||
modal: false,
|
||||
removeOnClose: true
|
||||
});
|
||||
|
||||
let html = '';
|
||||
|
||||
html += '<div class="formDialogHeader">';
|
||||
html += `<button type="button" is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1" title="${globalize.translate('ButtonBack')}"><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
|
||||
html += `<h3 class="formDialogHeaderTitle">${globalize.translate('HeaderNewRepository')}</h3>`;
|
||||
html += '</div>';
|
||||
html += '<form class="newPluginForm" style="margin:4em">';
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" id="txtRepositoryName" label="${globalize.translate('LabelRepositoryName')}" type="text" required />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('LabelRepositoryNameHelp')}</div>`;
|
||||
html += '</div>';
|
||||
html += '<div class="inputContainer">';
|
||||
html += `<input is="emby-input" id="txtRepositoryUrl" label="${globalize.translate('LabelRepositoryUrl')}" type="url" required />`;
|
||||
html += `<div class="fieldDescription">${globalize.translate('LabelRepositoryUrlHelp')}</div>`;
|
||||
html += '</div>';
|
||||
html += `<button is="emby-button" type="submit" class="raised button-submit block"><span>${globalize.translate('Save')}</span></button>`;
|
||||
html += '</div>';
|
||||
html += '</form>';
|
||||
|
||||
dialog.innerHTML = html;
|
||||
dialog.querySelector('.btnCancel').addEventListener('click', () => {
|
||||
dialogHelper.close(dialog);
|
||||
});
|
||||
|
||||
dialog.querySelector('.newPluginForm').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
|
||||
const repositoryUrl = dialog.querySelector('#txtRepositoryUrl').value.toLowerCase();
|
||||
|
||||
const alertCallback = function () {
|
||||
repositories.push({
|
||||
Name: dialog.querySelector('#txtRepositoryName').value,
|
||||
Url: dialog.querySelector('#txtRepositoryUrl').value,
|
||||
Enabled: true
|
||||
});
|
||||
saveList(view);
|
||||
dialogHelper.close(dialog);
|
||||
};
|
||||
|
||||
// Check the repository URL for the official Jellyfin repository domain, or
|
||||
// present the warning for 3rd party plugins.
|
||||
if (!repositoryUrl.startsWith('https://repo.jellyfin.org/')) {
|
||||
let msg = globalize.translate('MessageRepositoryInstallDisclaimer');
|
||||
msg += '<br/>';
|
||||
msg += '<br/>';
|
||||
msg += globalize.translate('PleaseConfirmRepositoryInstallation');
|
||||
|
||||
confirm(msg, globalize.translate('HeaderConfirmRepositoryInstallation')).then(function () {
|
||||
alertCallback();
|
||||
}).catch(() => {
|
||||
console.debug('repository not installed');
|
||||
dialogHelper.close(dialog);
|
||||
});
|
||||
} else {
|
||||
alertCallback();
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
dialogHelper.open(dialog);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
<div id="scheduledTaskPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage">
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="verticalSection">
|
||||
<div class="sectionTitleContainer flex align-items-center">
|
||||
<h2 class="sectionTitle taskName"></h2>
|
||||
</div>
|
||||
<p id="pTaskDescription"></p>
|
||||
</div>
|
||||
|
||||
<div class="readOnlyContent">
|
||||
<div>
|
||||
<h2 style="vertical-align: middle; display: inline-block;">${HeaderTaskTriggers}</h2>
|
||||
<button is="emby-button" type="button" class="fab fab-mini btnAddTrigger submit" style="margin-left: 1em;" title="${ButtonAddScheduledTaskTrigger}">
|
||||
<span class="material-icons add" aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="taskTriggers"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-role="popup" id="popupAddTrigger" class="dialog dialog-fixedSize dialog-medium hide" style="position: fixed; top: 10%; z-index: 999999;">
|
||||
<form class="addTriggerForm" style="padding:1em;">
|
||||
<div class="ui-bar-a">
|
||||
<h3>${ButtonAddScheduledTaskTrigger}</h3>
|
||||
</div>
|
||||
<div data-role="content">
|
||||
<div class="selectContainer">
|
||||
<select is="emby-select" id="selectTriggerType" class="selectTriggerType" label="${LabelTriggerType}">
|
||||
<option value="DailyTrigger">${OptionDaily}</option>
|
||||
<option value="WeeklyTrigger">${OptionWeekly}</option>
|
||||
<option value="IntervalTrigger">${OptionOnInterval}</option>
|
||||
<option value="StartupTrigger">${OnApplicationStartup}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="fldDayOfWeek" class="selectContainer">
|
||||
<select is="emby-select" id="selectDayOfWeek" name="selectDayOfWeek" label="${LabelDay}">
|
||||
<option value="Sunday">${Sunday}</option>
|
||||
<option value="Monday">${Monday}</option>
|
||||
<option value="Tuesday">${Tuesday}</option>
|
||||
<option value="Wednesday">${Wednesday}</option>
|
||||
<option value="Thursday">${Thursday}</option>
|
||||
<option value="Friday">${Friday}</option>
|
||||
<option value="Saturday">${Saturday}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="fldTimeOfDay" class="selectContainer">
|
||||
<select is="emby-select" id="selectTimeOfDay" label="${LabelTime}"></select>
|
||||
</div>
|
||||
<div id="fldSelectSystemEvent" class="selectContainer">
|
||||
<select is="emby-select" id="selectSystemEvent" name="selectSystemEvent" label="${LabelEvent}">
|
||||
<option value="WakeFromSleep">${OptionWakeFromSleep}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="fldSelectInterval" class="selectContainer">
|
||||
<select is="emby-select" id="selectInterval" label="${LabelEveryXMinutes}">
|
||||
<option value="9000000000">15 minutes</option>
|
||||
<option value="18000000000">30 minutes</option>
|
||||
<option value="27000000000">45 minutes</option>
|
||||
<option value="36000000000">1 hour</option>
|
||||
<option value="72000000000">2 hours</option>
|
||||
<option value="108000000000">3 hours</option>
|
||||
<option value="144000000000">4 hours</option>
|
||||
<option value="216000000000">6 hours</option>
|
||||
<option value="288000000000">8 hours</option>
|
||||
<option value="432000000000">12 hours</option>
|
||||
<option value="864000000000">24 hours</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="inputContainer">
|
||||
<input is="emby-input" id="txtTimeLimit" type="number" pattern="[0-9]*" min="1" step=".5" label="${LabelTimeLimitHours}" />
|
||||
</div>
|
||||
<div>
|
||||
<button is="emby-button" type="submit" class="raised button-submit block" data-icon="check">
|
||||
<span>${Add}</span>
|
||||
</button>
|
||||
<button is="emby-button" type="button" class="raised button-cancel block" data-icon="delete" onclick="$(this).parents('.dialog').addClass('hide');">
|
||||
<span>${ButtonCancel}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
236
src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js
Normal file
236
src/apps/dashboard/controllers/scheduledtasks/scheduledtask.js
Normal file
|
@ -0,0 +1,236 @@
|
|||
import loading from 'components/loading/loading';
|
||||
import datetime from 'scripts/datetime';
|
||||
import dom from 'scripts/dom';
|
||||
import globalize from 'lib/globalize';
|
||||
import 'elements/emby-input/emby-input';
|
||||
import 'elements/emby-button/emby-button';
|
||||
import 'elements/emby-select/emby-select';
|
||||
import confirm from 'components/confirm/confirm';
|
||||
import { getParameterByName } from 'utils/url.ts';
|
||||
|
||||
function fillTimeOfDay(select) {
|
||||
const options = [];
|
||||
|
||||
for (let i = 0; i < 86400000; i += 900000) {
|
||||
options.push({
|
||||
name: ScheduledTaskPage.getDisplayTime(i * 10000),
|
||||
value: i * 10000
|
||||
});
|
||||
}
|
||||
|
||||
select.innerHTML = options.map(function (o) {
|
||||
return '<option value="' + o.value + '">' + o.name + '</option>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const ScheduledTaskPage = {
|
||||
refreshScheduledTask: function (view) {
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.getScheduledTask(id).then(function (task) {
|
||||
ScheduledTaskPage.loadScheduledTask(view, task);
|
||||
});
|
||||
},
|
||||
loadScheduledTask: function (view, task) {
|
||||
view.querySelector('.taskName').innerHTML = task.Name;
|
||||
view.querySelector('#pTaskDescription').innerHTML = task.Description;
|
||||
|
||||
import('components/listview/listview.scss').then(() => {
|
||||
ScheduledTaskPage.loadTaskTriggers(view, task);
|
||||
});
|
||||
|
||||
loading.hide();
|
||||
},
|
||||
loadTaskTriggers: function (context, task) {
|
||||
let html = '';
|
||||
html += '<div class="paperList">';
|
||||
|
||||
for (let i = 0, length = task.Triggers.length; i < length; i++) {
|
||||
const trigger = task.Triggers[i];
|
||||
|
||||
html += '<div class="listItem listItem-border">';
|
||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||
if (trigger.MaxRuntimeTicks) {
|
||||
html += '<div class="listItemBody two-line">';
|
||||
} else {
|
||||
html += '<div class="listItemBody">';
|
||||
}
|
||||
html += "<div class='listItemBodyText'>" + ScheduledTaskPage.getTriggerFriendlyName(trigger) + '</div>';
|
||||
if (trigger.MaxRuntimeTicks) {
|
||||
html += '<div class="listItemBodyText secondary">';
|
||||
const hours = trigger.MaxRuntimeTicks / 36e9;
|
||||
if (hours == 1) {
|
||||
html += globalize.translate('ValueTimeLimitSingleHour');
|
||||
} else {
|
||||
html += globalize.translate('ValueTimeLimitMultiHour', hours);
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
html += '<button class="btnDeleteTrigger" data-index="' + i + '" type="button" is="paper-icon-button-light" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
context.querySelector('.taskTriggers').innerHTML = html;
|
||||
},
|
||||
// TODO: Replace this mess with date-fns and remove datetime completely
|
||||
getTriggerFriendlyName: function (trigger) {
|
||||
if (trigger.Type == 'DailyTrigger') {
|
||||
return globalize.translate('DailyAt', ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks));
|
||||
}
|
||||
|
||||
if (trigger.Type == 'WeeklyTrigger') {
|
||||
// TODO: The day of week isn't localised as well
|
||||
return globalize.translate('WeeklyAt', trigger.DayOfWeek, ScheduledTaskPage.getDisplayTime(trigger.TimeOfDayTicks));
|
||||
}
|
||||
|
||||
if (trigger.Type == 'SystemEventTrigger' && trigger.SystemEvent == 'WakeFromSleep') {
|
||||
return globalize.translate('OnWakeFromSleep');
|
||||
}
|
||||
|
||||
if (trigger.Type == 'IntervalTrigger') {
|
||||
const hours = trigger.IntervalTicks / 36e9;
|
||||
|
||||
if (hours == 0.25) {
|
||||
return globalize.translate('EveryXMinutes', '15');
|
||||
}
|
||||
if (hours == 0.5) {
|
||||
return globalize.translate('EveryXMinutes', '30');
|
||||
}
|
||||
if (hours == 0.75) {
|
||||
return globalize.translate('EveryXMinutes', '45');
|
||||
}
|
||||
if (hours == 1) {
|
||||
return globalize.translate('EveryHour');
|
||||
}
|
||||
|
||||
return globalize.translate('EveryXHours', hours);
|
||||
}
|
||||
|
||||
if (trigger.Type == 'StartupTrigger') {
|
||||
return globalize.translate('OnApplicationStartup');
|
||||
}
|
||||
|
||||
return trigger.Type;
|
||||
},
|
||||
getDisplayTime: function (ticks) {
|
||||
const ms = ticks / 1e4;
|
||||
const now = new Date();
|
||||
now.setHours(0, 0, 0, 0);
|
||||
now.setTime(now.getTime() + ms);
|
||||
return datetime.getDisplayTime(now);
|
||||
},
|
||||
showAddTriggerPopup: function (view) {
|
||||
view.querySelector('#selectTriggerType').value = 'DailyTrigger';
|
||||
view.querySelector('#selectTriggerType').dispatchEvent(new CustomEvent('change', {}));
|
||||
view.querySelector('#popupAddTrigger').classList.remove('hide');
|
||||
},
|
||||
confirmDeleteTrigger: function (view, index) {
|
||||
confirm(globalize.translate('MessageDeleteTaskTrigger'), globalize.translate('HeaderDeleteTaskTrigger')).then(function () {
|
||||
ScheduledTaskPage.deleteTrigger(view, index);
|
||||
});
|
||||
},
|
||||
deleteTrigger: function (view, index) {
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.getScheduledTask(id).then(function (task) {
|
||||
task.Triggers.splice(index, 1);
|
||||
ApiClient.updateScheduledTaskTriggers(task.Id, task.Triggers).then(function () {
|
||||
ScheduledTaskPage.refreshScheduledTask(view);
|
||||
});
|
||||
});
|
||||
},
|
||||
refreshTriggerFields: function (page, triggerType) {
|
||||
if (triggerType == 'DailyTrigger') {
|
||||
page.querySelector('#fldTimeOfDay').classList.remove('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').setAttribute('required', 'required');
|
||||
} else if (triggerType == 'WeeklyTrigger') {
|
||||
page.querySelector('#fldTimeOfDay').classList.remove('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.remove('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').setAttribute('required', 'required');
|
||||
} else if (triggerType == 'SystemEventTrigger') {
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.remove('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
} else if (triggerType == 'IntervalTrigger') {
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.remove('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
} else if (triggerType == 'StartupTrigger') {
|
||||
page.querySelector('#fldTimeOfDay').classList.add('hide');
|
||||
page.querySelector('#fldDayOfWeek').classList.add('hide');
|
||||
page.querySelector('#fldSelectSystemEvent').classList.add('hide');
|
||||
page.querySelector('#fldSelectInterval').classList.add('hide');
|
||||
page.querySelector('#selectTimeOfDay').removeAttribute('required');
|
||||
}
|
||||
},
|
||||
getTriggerToAdd: function (page) {
|
||||
const trigger = {
|
||||
Type: page.querySelector('#selectTriggerType').value
|
||||
};
|
||||
|
||||
if (trigger.Type == 'DailyTrigger') {
|
||||
trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value;
|
||||
} else if (trigger.Type == 'WeeklyTrigger') {
|
||||
trigger.DayOfWeek = page.querySelector('#selectDayOfWeek').value;
|
||||
trigger.TimeOfDayTicks = page.querySelector('#selectTimeOfDay').value;
|
||||
} else if (trigger.Type == 'SystemEventTrigger') {
|
||||
trigger.SystemEvent = page.querySelector('#selectSystemEvent').value;
|
||||
} else if (trigger.Type == 'IntervalTrigger') {
|
||||
trigger.IntervalTicks = page.querySelector('#selectInterval').value;
|
||||
}
|
||||
|
||||
let timeLimit = page.querySelector('#txtTimeLimit').value || '0';
|
||||
timeLimit = parseFloat(timeLimit) * 3600000;
|
||||
|
||||
trigger.MaxRuntimeTicks = timeLimit * 1e4 || null;
|
||||
|
||||
return trigger;
|
||||
}
|
||||
};
|
||||
export default function (view) {
|
||||
function onSubmit(e) {
|
||||
loading.show();
|
||||
const id = getParameterByName('id');
|
||||
ApiClient.getScheduledTask(id).then(function (task) {
|
||||
task.Triggers.push(ScheduledTaskPage.getTriggerToAdd(view));
|
||||
ApiClient.updateScheduledTaskTriggers(task.Id, task.Triggers).then(function () {
|
||||
document.querySelector('#popupAddTrigger').classList.add('hide');
|
||||
ScheduledTaskPage.refreshScheduledTask(view);
|
||||
});
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
view.querySelector('.addTriggerForm').addEventListener('submit', onSubmit);
|
||||
fillTimeOfDay(view.querySelector('#selectTimeOfDay'));
|
||||
view.querySelector('#popupAddTrigger').parentNode.trigger(new Event('create'));
|
||||
view.querySelector('.selectTriggerType').addEventListener('change', function () {
|
||||
ScheduledTaskPage.refreshTriggerFields(view, this.value);
|
||||
});
|
||||
view.querySelector('.btnAddTrigger').addEventListener('click', function () {
|
||||
ScheduledTaskPage.showAddTriggerPopup(view);
|
||||
});
|
||||
view.addEventListener('click', function (e) {
|
||||
const btnDeleteTrigger = dom.parentWithClass(e.target, 'btnDeleteTrigger');
|
||||
|
||||
if (btnDeleteTrigger) {
|
||||
ScheduledTaskPage.confirmDeleteTrigger(view, parseInt(btnDeleteTrigger.getAttribute('data-index'), 10));
|
||||
}
|
||||
});
|
||||
view.addEventListener('viewshow', function () {
|
||||
ScheduledTaskPage.refreshScheduledTask(view);
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<div id="scheduledTasksPage" data-role="page" class="page type-interior scheduledTasksConfigurationPage" data-title="${TabScheduledTasks}">
|
||||
<style>
|
||||
.taskProgressOuter {
|
||||
height: 6px;
|
||||
background: #eee;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.taskProgressInner {
|
||||
border-radius: 2px;
|
||||
height: 100%;
|
||||
background: #00a4dc;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div class="content-primary">
|
||||
<div class="divScheduledTasks readOnlyContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
197
src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js
Normal file
197
src/apps/dashboard/controllers/scheduledtasks/scheduledtasks.js
Normal file
|
@ -0,0 +1,197 @@
|
|||
import { formatDistance, formatDistanceToNow } from 'date-fns';
|
||||
import 'jquery';
|
||||
|
||||
import loading from 'components/loading/loading';
|
||||
import globalize from 'lib/globalize';
|
||||
import dom from 'scripts/dom';
|
||||
import serverNotifications from 'scripts/serverNotifications';
|
||||
import { getLocale, getLocaleWithSuffix } from 'utils/dateFnsLocale.ts';
|
||||
import Events from 'utils/events.ts';
|
||||
|
||||
import 'components/listview/listview.scss';
|
||||
import 'elements/emby-button/emby-button';
|
||||
|
||||
function reloadList(page) {
|
||||
ApiClient.getScheduledTasks({
|
||||
isHidden: false
|
||||
}).then(function(tasks) {
|
||||
populateList(page, tasks);
|
||||
loading.hide();
|
||||
});
|
||||
}
|
||||
|
||||
function populateList(page, tasks) {
|
||||
tasks = tasks.sort(function(a, b) {
|
||||
a = a.Category + ' ' + a.Name;
|
||||
b = b.Category + ' ' + b.Name;
|
||||
if (a > b) {
|
||||
return 1;
|
||||
} else if (a < b) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
let currentCategory;
|
||||
let html = '';
|
||||
for (const task of tasks) {
|
||||
if (task.Category != currentCategory) {
|
||||
currentCategory = task.Category;
|
||||
if (currentCategory) {
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
html += '<div class="verticalSection verticalSection-extrabottompadding">';
|
||||
html += '<div class="sectionTitleContainer" style="margin-bottom:1em;">';
|
||||
html += '<h2 class="sectionTitle">';
|
||||
html += currentCategory;
|
||||
html += '</h2>';
|
||||
html += '</div>';
|
||||
html += '<div class="paperList">';
|
||||
}
|
||||
html += '<div class="listItem listItem-border scheduledTaskPaperIconItem" data-status="' + task.State + '">';
|
||||
html += "<a is='emby-linkbutton' style='margin:0;padding:0;' class='clearLink listItemIconContainer' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += '<span class="material-icons listItemIcon schedule" aria-hidden="true"></span>';
|
||||
html += '</a>';
|
||||
html += '<div class="listItemBody two-line">';
|
||||
const textAlignStyle = globalize.getIsRTL() ? 'right' : 'left';
|
||||
html += "<a class='clearLink' style='margin:0;padding:0;display:block;text-align:" + textAlignStyle + ";' is='emby-linkbutton' href='/dashboard/tasks/edit?id=" + task.Id + "'>";
|
||||
html += "<h3 class='listItemBodyText'>" + task.Name + '</h3>';
|
||||
html += "<div class='secondary listItemBodyText' id='taskProgress" + task.Id + "'>" + getTaskProgressHtml(task) + '</div>';
|
||||
html += '</a>';
|
||||
html += '</div>';
|
||||
if (task.State === 'Running') {
|
||||
html += '<button type="button" is="paper-icon-button-light" id="btnTask' + task.Id + '" class="btnStopTask" data-taskid="' + task.Id + '" title="' + globalize.translate('ButtonStop') + '"><span class="material-icons stop" aria-hidden="true"></span></button>';
|
||||
} else if (task.State === 'Idle') {
|
||||
html += '<button type="button" is="paper-icon-button-light" id="btnTask' + task.Id + '" class="btnStartTask" data-taskid="' + task.Id + '" title="' + globalize.translate('ButtonStart') + '"><span class="material-icons play_arrow" aria-hidden="true"></span></button>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
if (tasks.length) {
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
page.querySelector('.divScheduledTasks').innerHTML = html;
|
||||
}
|
||||
|
||||
function getTaskProgressHtml(task) {
|
||||
let html = '';
|
||||
if (task.State === 'Idle') {
|
||||
if (task.LastExecutionResult) {
|
||||
const endtime = Date.parse(task.LastExecutionResult.EndTimeUtc);
|
||||
const starttime = Date.parse(task.LastExecutionResult.StartTimeUtc);
|
||||
html += globalize.translate('LabelScheduledTaskLastRan', formatDistanceToNow(endtime, getLocaleWithSuffix()),
|
||||
formatDistance(starttime, endtime, { locale: getLocale() }));
|
||||
if (task.LastExecutionResult.Status === 'Failed') {
|
||||
html += " <span style='color:#FF0000;'>(" + globalize.translate('LabelFailed') + ')</span>';
|
||||
} else if (task.LastExecutionResult.Status === 'Cancelled') {
|
||||
html += " <span style='color:#0026FF;'>(" + globalize.translate('LabelCancelled') + ')</span>';
|
||||
} else if (task.LastExecutionResult.Status === 'Aborted') {
|
||||
html += " <span style='color:#FF0000;'>" + globalize.translate('LabelAbortedByServerShutdown') + '</span>';
|
||||
}
|
||||
}
|
||||
} else if (task.State === 'Running') {
|
||||
const progress = (task.CurrentProgressPercentage || 0).toFixed(1);
|
||||
html += '<div style="display:flex;align-items:center;">';
|
||||
html += '<div class="taskProgressOuter" title="' + progress + '%" style="flex-grow:1;">';
|
||||
html += '<div class="taskProgressInner" style="width:' + progress + '%;">';
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
html += "<span style='color:#00a4dc;margin-left:5px;'>" + progress + '%</span>';
|
||||
html += '</div>';
|
||||
} else {
|
||||
html += "<span style='color:#FF0000;'>" + globalize.translate('LabelStopping') + '</span>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function setTaskButtonIcon(button, icon) {
|
||||
const inner = button.querySelector('.material-icons');
|
||||
inner.classList.remove('stop', 'play_arrow');
|
||||
inner.classList.add(icon);
|
||||
}
|
||||
|
||||
function updateTaskButton(elem, state) {
|
||||
if (state === 'Running') {
|
||||
elem.classList.remove('btnStartTask');
|
||||
elem.classList.add('btnStopTask');
|
||||
setTaskButtonIcon(elem, 'stop');
|
||||
elem.title = globalize.translate('ButtonStop');
|
||||
} else if (state === 'Idle') {
|
||||
elem.classList.add('btnStartTask');
|
||||
elem.classList.remove('btnStopTask');
|
||||
setTaskButtonIcon(elem, 'play_arrow');
|
||||
elem.title = globalize.translate('ButtonStart');
|
||||
}
|
||||
dom.parentWithClass(elem, 'listItem').setAttribute('data-status', state);
|
||||
}
|
||||
|
||||
export default function(view) {
|
||||
function updateTasks(tasks) {
|
||||
for (const task of tasks) {
|
||||
const taskProgress = view.querySelector(`#taskProgress${task.Id}`);
|
||||
if (taskProgress) taskProgress.innerHTML = getTaskProgressHtml(task);
|
||||
|
||||
const taskButton = view.querySelector(`#btnTask${task.Id}`);
|
||||
if (taskButton) updateTaskButton(taskButton, task.State);
|
||||
}
|
||||
}
|
||||
|
||||
function onPollIntervalFired() {
|
||||
if (!ApiClient.isMessageChannelOpen()) {
|
||||
reloadList(view);
|
||||
}
|
||||
}
|
||||
|
||||
function onScheduledTasksUpdate(e, apiClient, info) {
|
||||
if (apiClient.serverId() === serverId) {
|
||||
updateTasks(info);
|
||||
}
|
||||
}
|
||||
|
||||
function startInterval() {
|
||||
ApiClient.sendMessage('ScheduledTasksInfoStart', '1000,1000');
|
||||
pollInterval && clearInterval(pollInterval);
|
||||
pollInterval = setInterval(onPollIntervalFired, 1e4);
|
||||
}
|
||||
|
||||
function stopInterval() {
|
||||
ApiClient.sendMessage('ScheduledTasksInfoStop');
|
||||
pollInterval && clearInterval(pollInterval);
|
||||
}
|
||||
|
||||
let pollInterval;
|
||||
const serverId = ApiClient.serverId();
|
||||
|
||||
$('.divScheduledTasks', view).on('click', '.btnStartTask', function() {
|
||||
const button = this;
|
||||
const id = button.getAttribute('data-taskid');
|
||||
ApiClient.startScheduledTask(id).then(function() {
|
||||
updateTaskButton(button, 'Running');
|
||||
reloadList(view);
|
||||
});
|
||||
});
|
||||
|
||||
$('.divScheduledTasks', view).on('click', '.btnStopTask', function() {
|
||||
const button = this;
|
||||
const id = button.getAttribute('data-taskid');
|
||||
ApiClient.stopScheduledTask(id).then(function() {
|
||||
updateTaskButton(button, '');
|
||||
reloadList(view);
|
||||
});
|
||||
});
|
||||
|
||||
view.addEventListener('viewbeforehide', function() {
|
||||
Events.off(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
stopInterval();
|
||||
});
|
||||
|
||||
view.addEventListener('viewshow', function() {
|
||||
loading.show();
|
||||
startInterval();
|
||||
reloadList(view);
|
||||
Events.on(serverNotifications, 'ScheduledTasksInfo', onScheduledTasksUpdate);
|
||||
});
|
||||
}
|
||||
|
20
src/apps/dashboard/controllers/streaming.html
Normal file
20
src/apps/dashboard/controllers/streaming.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<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>
|
31
src/apps/dashboard/controllers/streaming.js
Normal file
31
src/apps/dashboard/controllers/streaming.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -6,92 +6,92 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
path: '/dashboard',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/dashboard',
|
||||
view: 'dashboard/dashboard.html'
|
||||
controller: 'dashboard',
|
||||
view: 'dashboard.html'
|
||||
}
|
||||
}, {
|
||||
path: 'settings',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/general',
|
||||
view: 'dashboard/general.html'
|
||||
controller: 'general',
|
||||
view: 'general.html'
|
||||
}
|
||||
}, {
|
||||
path: 'networking',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/networking',
|
||||
view: 'dashboard/networking.html'
|
||||
controller: 'networking',
|
||||
view: 'networking.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/devices/devices',
|
||||
view: 'dashboard/devices/devices.html'
|
||||
controller: 'devices/devices',
|
||||
view: 'devices/devices.html'
|
||||
}
|
||||
}, {
|
||||
path: 'devices/edit',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/devices/device',
|
||||
view: 'dashboard/devices/device.html'
|
||||
controller: 'devices/device',
|
||||
view: 'devices/device.html'
|
||||
}
|
||||
}, {
|
||||
path: 'libraries',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/library',
|
||||
view: 'dashboard/library.html'
|
||||
controller: 'library',
|
||||
view: 'library.html'
|
||||
}
|
||||
}, {
|
||||
path: 'libraries/display',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/librarydisplay',
|
||||
view: 'dashboard/librarydisplay.html'
|
||||
controller: 'librarydisplay',
|
||||
view: 'librarydisplay.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/transcoding',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/encodingsettings',
|
||||
view: 'dashboard/encodingsettings.html'
|
||||
controller: 'encodingsettings',
|
||||
view: 'encodingsettings.html'
|
||||
}
|
||||
}, {
|
||||
path: 'libraries/metadata',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/metadataImages',
|
||||
view: 'dashboard/metadataimages.html'
|
||||
controller: 'metadataImages',
|
||||
view: 'metadataimages.html'
|
||||
}
|
||||
}, {
|
||||
path: 'libraries/nfo',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/metadatanfo',
|
||||
view: 'dashboard/metadatanfo.html'
|
||||
controller: 'metadatanfo',
|
||||
view: 'metadatanfo.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/resume',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/playback',
|
||||
view: 'dashboard/playback.html'
|
||||
controller: 'playback',
|
||||
view: 'playback.html'
|
||||
}
|
||||
}, {
|
||||
path: 'plugins/catalog',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/plugins/available/index',
|
||||
view: 'dashboard/plugins/available/index.html'
|
||||
controller: 'plugins/available/index',
|
||||
view: 'plugins/available/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'plugins/repositories',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/plugins/repositories/index',
|
||||
view: 'dashboard/plugins/repositories/index.html'
|
||||
controller: 'plugins/repositories/index',
|
||||
view: 'plugins/repositories/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'livetv/guide',
|
||||
|
@ -125,29 +125,29 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
|||
path: 'plugins',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/plugins/installed/index',
|
||||
view: 'dashboard/plugins/installed/index.html'
|
||||
controller: 'plugins/installed/index',
|
||||
view: 'plugins/installed/index.html'
|
||||
}
|
||||
}, {
|
||||
path: 'tasks/edit',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/scheduledtasks/scheduledtask',
|
||||
view: 'dashboard/scheduledtasks/scheduledtask.html'
|
||||
controller: 'scheduledtasks/scheduledtask',
|
||||
view: 'scheduledtasks/scheduledtask.html'
|
||||
}
|
||||
}, {
|
||||
path: 'tasks',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||
controller: 'scheduledtasks/scheduledtasks',
|
||||
view: 'scheduledtasks/scheduledtasks.html'
|
||||
}
|
||||
}, {
|
||||
path: 'playback/streaming',
|
||||
pageProps: {
|
||||
appType: AppType.Dashboard,
|
||||
view: 'dashboard/streaming.html',
|
||||
controller: 'dashboard/streaming'
|
||||
view: 'streaming.html',
|
||||
controller: 'streaming'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue