1
0
Fork 0
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:
Bill Thornton 2025-02-13 16:22:36 -05:00 committed by GitHub
commit d5db15367b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 246 additions and 222 deletions

View 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>

View 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();
}
});
}

View 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%;
}
}

View 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>

View 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);
}

View 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>

View 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 += '&nbsp;';
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);
});
}

View 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>

View 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);
});
});
});

View 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>

View 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]);
});
});
}

View 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>

View 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 += '&nbsp;';
} 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 += '&nbsp;';
} else {
html += typeName;
}
html += '</div>';
if (virtualFolder.showLocations === false) {
html += "<div class='cardText cardText-secondary'>";
html += '&nbsp;';
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')
});
});

View 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>

View 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');
}
});
});
}

View 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>

View 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);
});

View 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>

View 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);
});
});

View 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>

View 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 || '&nbsp;';
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')
});
});

View 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>

View 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();
}
});
});
});
}

View 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);
});

View 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>

View 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>

View 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]);
});
});

View 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>

View 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);
});
});
}

View 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>

View 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);
});
});

View 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>

View 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) : '&nbsp;';
html += '</div>';
html += '</div>';
html += '</div>';
html += '</div>';
return html;
}
export default function (view) {
view.addEventListener('viewshow', function () {
reloadList(this);
});
}

View 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>

View 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
};

View file

@ -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>

View 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);
});
}

View file

@ -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>

View 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);
});
}

View file

@ -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>

View 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);
});
}

View 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>

View 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);
});
});

View file

@ -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'
}
}
];