Merge branch 'master' into enable-airplay-audioplayer

This commit is contained in:
stamatovg 2023-04-20 12:00:25 +03:00 committed by GitHub
commit 9a69156487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
264 changed files with 53855 additions and 48500 deletions

View file

@ -1,6 +1,3 @@
/* eslint-disable indent */
/**
* Module for controlling user parental control from.
* @module components/accessSchedule/accessSchedule
@ -14,84 +11,82 @@ import '../../elements/emby-button/paper-icon-button-light';
import '../formdialog.scss';
import template from './accessSchedule.template.html';
function getDisplayTime(hours) {
let minutes = 0;
const pct = hours % 1;
function getDisplayTime(hours) {
let minutes = 0;
const pct = hours % 1;
if (pct) {
minutes = parseInt(60 * pct, 10);
}
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
if (pct) {
minutes = parseInt(60 * pct, 10);
}
function populateHours(context) {
let html = '';
return datetime.getDisplayTime(new Date(2000, 1, 1, hours, minutes, 0, 0));
}
for (let i = 0; i < 24; i += 0.5) {
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
}
function populateHours(context) {
let html = '';
html += `<option value="24">${getDisplayTime(0)}</option>`;
context.querySelector('#selectStart').innerHTML = html;
context.querySelector('#selectEnd').innerHTML = html;
for (let i = 0; i < 24; i += 0.5) {
html += `<option value="${i}">${getDisplayTime(i)}</option>`;
}
function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
context.querySelector('#selectStart').value = StartHour || 0;
context.querySelector('#selectEnd').value = EndHour || 0;
html += `<option value="24">${getDisplayTime(0)}</option>`;
context.querySelector('#selectStart').innerHTML = html;
context.querySelector('#selectEnd').innerHTML = html;
}
function loadSchedule(context, { DayOfWeek, StartHour, EndHour }) {
context.querySelector('#selectDay').value = DayOfWeek || 'Sunday';
context.querySelector('#selectStart').value = StartHour || 0;
context.querySelector('#selectEnd').value = EndHour || 0;
}
function submitSchedule(context, options) {
const updatedSchedule = {
DayOfWeek: context.querySelector('#selectDay').value,
StartHour: context.querySelector('#selectStart').value,
EndHour: context.querySelector('#selectEnd').value
};
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
return;
}
function submitSchedule(context, options) {
const updatedSchedule = {
DayOfWeek: context.querySelector('#selectDay').value,
StartHour: context.querySelector('#selectStart').value,
EndHour: context.querySelector('#selectEnd').value
};
context.submitted = true;
options.schedule = Object.assign(options.schedule, updatedSchedule);
dialogHelper.close(context);
}
if (parseFloat(updatedSchedule.StartHour) >= parseFloat(updatedSchedule.EndHour)) {
alert(globalize.translate('ErrorStartHourGreaterThanEnd'));
return;
}
context.submitted = true;
options.schedule = Object.assign(options.schedule, updatedSchedule);
dialogHelper.close(context);
}
export function show(options) {
return new Promise((resolve, reject) => {
const dlg = dialogHelper.createDialog({
removeOnClose: true,
size: 'small'
});
dlg.classList.add('formDialog');
let html = '';
html += globalize.translateHtml(template);
dlg.innerHTML = html;
populateHours(dlg);
loadSchedule(dlg, options.schedule);
dialogHelper.open(dlg);
dlg.addEventListener('close', () => {
if (dlg.submitted) {
resolve(options.schedule);
} else {
reject();
}
});
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.querySelector('form').addEventListener('submit', event => {
submitSchedule(dlg, options);
event.preventDefault();
return false;
});
export function show(options) {
return new Promise((resolve, reject) => {
const dlg = dialogHelper.createDialog({
removeOnClose: true,
size: 'small'
});
}
/* eslint-enable indent */
dlg.classList.add('formDialog');
let html = '';
html += globalize.translateHtml(template);
dlg.innerHTML = html;
populateHours(dlg);
loadSchedule(dlg, options.schedule);
dialogHelper.open(dlg);
dlg.addEventListener('close', () => {
if (dlg.submitted) {
resolve(options.schedule);
} else {
reject();
}
});
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.querySelector('form').addEventListener('submit', event => {
submitSchedule(dlg, options);
event.preventDefault();
return false;
});
});
}
export default {
show: show

View file

@ -11,130 +11,128 @@ import alert from './alert';
import { getLocale } from '../utils/dateFnsLocale.ts';
import { toBoolean } from '../utils/string.ts';
/* eslint-disable indent */
function getEntryHtml(entry, apiClient) {
let html = '';
html += '<div class="listItem listItem-border">';
let color = '#00a4dc';
let icon = 'notifications';
function getEntryHtml(entry, apiClient) {
let html = '';
html += '<div class="listItem listItem-border">';
let color = '#00a4dc';
let icon = 'notifications';
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
color = '#cc0000';
icon = 'notification_important';
}
if (entry.Severity == 'Error' || entry.Severity == 'Fatal' || entry.Severity == 'Warn') {
color = '#cc0000';
icon = 'notification_important';
}
if (entry.UserId && entry.UserPrimaryImageTag) {
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
type: 'Primary',
tag: entry.UserPrimaryImageTag
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
} else {
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
}
if (entry.UserId && entry.UserPrimaryImageTag) {
html += '<span class="listItemIcon material-icons dvr" aria-hidden="true" style="width:2em!important;height:2em!important;padding:0;color:transparent;background-color:' + color + ";background-image:url('" + apiClient.getUserImageUrl(entry.UserId, {
type: 'Primary',
tag: entry.UserPrimaryImageTag
}) + "');background-repeat:no-repeat;background-position:center center;background-size: cover;\"></span>";
} else {
html += '<span class="listItemIcon material-icons ' + icon + '" aria-hidden="true" style="background-color:' + color + '"></span>';
}
html += '<div class="listItemBody three-line">';
html += '<div class="listItemBodyText">';
html += escapeHtml(entry.Name);
html += '</div>';
html += '<div class="listItemBodyText secondary">';
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
html += '</div>';
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
html += escapeHtml(entry.ShortOverview || '');
html += '</div>';
html += '</div>';
html += '<div class="listItemBody three-line">';
html += '<div class="listItemBodyText">';
html += escapeHtml(entry.Name);
html += '</div>';
html += '<div class="listItemBodyText secondary">';
html += formatRelative(Date.parse(entry.Date), Date.now(), { locale: getLocale() });
html += '</div>';
html += '<div class="listItemBodyText secondary listItemBodyText-nowrap">';
html += escapeHtml(entry.ShortOverview || '');
html += '</div>';
html += '</div>';
if (entry.Overview) {
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
if (entry.Overview) {
html += `<button type="button" is="paper-icon-button-light" class="btnEntryInfo" data-id="${entry.Id}" title="${globalize.translate('Info')}">
<span class="material-icons info" aria-hidden="true"></span>
</button>`;
}
html += '</div>';
return html;
}
function renderList(elem, apiClient, result) {
elem.innerHTML = result.Items.map(function (i) {
return getEntryHtml(i, apiClient);
}).join('');
html += '</div>';
return html;
}
function renderList(elem, apiClient, result) {
elem.innerHTML = result.Items.map(function (i) {
return getEntryHtml(i, apiClient);
}).join('');
}
function reloadData(instance, elem, apiClient, startIndex, limit) {
if (startIndex == null) {
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0', 10);
}
function reloadData(instance, elem, apiClient, startIndex, limit) {
if (startIndex == null) {
startIndex = parseInt(elem.getAttribute('data-activitystartindex') || '0', 10);
}
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7', 10);
const minDate = new Date();
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
limit = limit || parseInt(elem.getAttribute('data-activitylimit') || '7', 10);
const minDate = new Date();
const hasUserId = toBoolean(elem.getAttribute('data-useractivity'), true);
// TODO: Use date-fns
if (hasUserId) {
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
} else {
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
}
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
startIndex: startIndex,
limit: limit,
minDate: minDate.toISOString(),
hasUserId: hasUserId
})).then(function (result) {
elem.setAttribute('data-activitystartindex', startIndex);
elem.setAttribute('data-activitylimit', limit);
if (!startIndex) {
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
if (activityContainer) {
if (result.Items.length) {
activityContainer.classList.remove('hide');
} else {
activityContainer.classList.add('hide');
}
}
}
instance.items = result.Items;
renderList(elem, apiClient, result);
});
// TODO: Use date-fns
if (hasUserId) {
minDate.setTime(minDate.getTime() - 24 * 60 * 60 * 1000); // one day back
} else {
minDate.setTime(minDate.getTime() - 7 * 24 * 60 * 60 * 1000); // one week back
}
function onActivityLogUpdate(e, apiClient) {
const options = this.options;
ApiClient.getJSON(ApiClient.getUrl('System/ActivityLog/Entries', {
startIndex: startIndex,
limit: limit,
minDate: minDate.toISOString(),
hasUserId: hasUserId
})).then(function (result) {
elem.setAttribute('data-activitystartindex', startIndex);
elem.setAttribute('data-activitylimit', limit);
if (!startIndex) {
const activityContainer = dom.parentWithClass(elem, 'activityContainer');
if (options && options.serverId === apiClient.serverId()) {
reloadData(this, options.element, apiClient);
}
}
function onListClick(e) {
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
if (btnEntryInfo) {
const id = btnEntryInfo.getAttribute('data-id');
const items = this.items;
if (items) {
const item = items.filter(function (i) {
return i.Id.toString() === id;
})[0];
if (item) {
showItemOverview(item);
if (activityContainer) {
if (result.Items.length) {
activityContainer.classList.remove('hide');
} else {
activityContainer.classList.add('hide');
}
}
}
}
function showItemOverview(item) {
alert({
text: item.Overview
});
instance.items = result.Items;
renderList(elem, apiClient, result);
});
}
function onActivityLogUpdate(e, apiClient) {
const options = this.options;
if (options && options.serverId === apiClient.serverId()) {
reloadData(this, options.element, apiClient);
}
}
function onListClick(e) {
const btnEntryInfo = dom.parentWithClass(e.target, 'btnEntryInfo');
if (btnEntryInfo) {
const id = btnEntryInfo.getAttribute('data-id');
const items = this.items;
if (items) {
const item = items.filter(function (i) {
return i.Id.toString() === id;
})[0];
if (item) {
showItemOverview(item);
}
}
}
}
function showItemOverview(item) {
alert({
text: item.Overview
});
}
class ActivityLog {
constructor(options) {
@ -169,5 +167,3 @@ class ActivityLog {
}
export default ActivityLog;
/* eslint-enable indent */

View file

@ -3,45 +3,41 @@ import browser from '../scripts/browser';
import dialog from './dialog/dialog';
import globalize from '../scripts/globalize';
/* eslint-disable indent */
function useNativeAlert() {
// webOS seems to block modals
// Tizen 2.x seems to block modals
return !browser.web0s
function useNativeAlert() {
// webOS seems to block modals
// Tizen 2.x seems to block modals
return !browser.web0s
&& !(browser.tizenVersion && browser.tizenVersion < 3)
&& browser.tv
&& window.alert;
}
export default async function (text, title) {
let options;
if (typeof text === 'string') {
options = {
title: title,
text: text
};
} else {
options = text;
}
export default async function (text, title) {
let options;
if (typeof text === 'string') {
options = {
title: title,
text: text
};
} else {
options = text;
}
await appRouter.ready();
await appRouter.ready();
if (useNativeAlert()) {
alert((options.text || '').replaceAll('<br/>', '\n'));
return Promise.resolve();
} else {
const items = [];
if (useNativeAlert()) {
alert((options.text || '').replaceAll('<br/>', '\n'));
return Promise.resolve();
} else {
const items = [];
items.push({
name: globalize.translate('ButtonGotIt'),
id: 'ok',
type: 'submit'
});
items.push({
name: globalize.translate('ButtonGotIt'),
id: 'ok',
type: 'submit'
});
options.buttons = items;
return dialog.show(options);
}
options.buttons = items;
return dialog.show(options);
}
/* eslint-enable indent */
}

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module alphaPicker.
* @module components/alphaPicker/alphaPicker
@ -13,312 +11,311 @@ import './style.scss';
import '../../elements/emby-button/paper-icon-button-light';
import 'material-design-icons-iconfont';
const selectedButtonClass = 'alphaPickerButton-selected';
const selectedButtonClass = 'alphaPickerButton-selected';
function focus() {
const scope = this;
const selected = scope.querySelector(`.${selectedButtonClass}`);
function focus() {
const scope = this;
const selected = scope.querySelector(`.${selectedButtonClass}`);
if (selected) {
focusManager.focus(selected);
} else {
focusManager.autoFocus(scope, true);
}
if (selected) {
focusManager.focus(selected);
} else {
focusManager.autoFocus(scope, true);
}
}
function getAlphaPickerButtonClassName(vertical) {
let alphaPickerButtonClassName = 'alphaPickerButton';
if (layoutManager.tv) {
alphaPickerButtonClassName += ' alphaPickerButton-tv';
}
function getAlphaPickerButtonClassName(vertical) {
let alphaPickerButtonClassName = 'alphaPickerButton';
if (layoutManager.tv) {
alphaPickerButtonClassName += ' alphaPickerButton-tv';
}
if (vertical) {
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
}
return alphaPickerButtonClassName;
if (vertical) {
alphaPickerButtonClassName += ' alphaPickerButton-vertical';
}
function getLetterButton(l, vertical) {
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
return alphaPickerButtonClassName;
}
function getLetterButton(l, vertical) {
return `<button data-value="${l}" class="${getAlphaPickerButtonClassName(vertical)}">${l}</button>`;
}
function mapLetters(letters, vertical) {
return letters.map(l => {
return getLetterButton(l, vertical);
});
}
function render(element, options) {
element.classList.add('alphaPicker');
if (layoutManager.tv) {
element.classList.add('alphaPicker-tv');
}
function mapLetters(letters, vertical) {
return letters.map(l => {
return getLetterButton(l, vertical);
});
const vertical = element.classList.contains('alphaPicker-vertical');
if (!vertical) {
element.classList.add('focuscontainer-x');
}
function render(element, options) {
element.classList.add('alphaPicker');
let html = '';
let letters;
if (layoutManager.tv) {
element.classList.add('alphaPicker-tv');
}
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
const vertical = element.classList.contains('alphaPicker-vertical');
let rowClassName = 'alphaPickerRow';
if (!vertical) {
element.classList.add('focuscontainer-x');
}
if (vertical) {
rowClassName += ' alphaPickerRow-vertical';
}
let html = '';
let letters;
const alphaPickerButtonClassName = getAlphaPickerButtonClassName(vertical);
let rowClassName = 'alphaPickerRow';
if (vertical) {
rowClassName += ' alphaPickerRow-vertical';
}
html += `<div class="${rowClassName}">`;
if (options.mode === 'keyboard') {
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
} else {
letters = ['#'];
html += mapLetters(letters, vertical).join('');
}
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
html += `<div class="${rowClassName}">`;
if (options.mode === 'keyboard') {
html += `<button data-value=" " is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonSpace')}"><span class="material-icons alphaPickerButtonIcon space_bar" aria-hidden="true"></span></button>`;
} else {
letters = ['#'];
html += mapLetters(letters, vertical).join('');
if (options.mode === 'keyboard') {
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
html += '</div>';
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
html += `<div class="${rowClassName}">`;
html += '<br/>';
html += mapLetters(letters, vertical).join('');
html += '</div>';
} else {
html += '</div>';
}
element.innerHTML = html;
element.classList.add('focusable');
element.focus = focus;
}
export class AlphaPicker {
constructor(options) {
const self = this;
letters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
html += mapLetters(letters, vertical).join('');
this.options = options;
if (options.mode === 'keyboard') {
html += `<button data-value="backspace" is="paper-icon-button-light" class="${alphaPickerButtonClassName}" aria-label="${globalize.translate('ButtonBackspace')}"><span class="material-icons alphaPickerButtonIcon backspace" aria-hidden="true"></span></button>`;
html += '</div>';
const element = options.element;
const itemsContainer = options.itemsContainer;
const itemClass = options.itemClass;
letters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
html += `<div class="${rowClassName}">`;
html += '<br/>';
html += mapLetters(letters, vertical).join('');
html += '</div>';
} else {
html += '</div>';
}
let itemFocusValue;
let itemFocusTimeout;
element.innerHTML = html;
function onItemFocusTimeout() {
itemFocusTimeout = null;
self.value(itemFocusValue);
}
element.classList.add('focusable');
element.focus = focus;
}
let alphaFocusedElement;
let alphaFocusTimeout;
export class AlphaPicker {
constructor(options) {
const self = this;
function onAlphaFocusTimeout() {
alphaFocusTimeout = null;
this.options = options;
if (document.activeElement === alphaFocusedElement) {
const value = alphaFocusedElement.getAttribute('data-value');
self.value(value, true);
}
}
const element = options.element;
const itemsContainer = options.itemsContainer;
const itemClass = options.itemClass;
function onAlphaPickerInKeyboardModeClick(e) {
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
let itemFocusValue;
let itemFocusTimeout;
if (alphaPickerButton) {
const value = alphaPickerButton.getAttribute('data-value');
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
cancelable: false,
detail: {
value
}
}));
}
}
function onAlphaPickerClick(e) {
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
if (alphaPickerButton) {
const value = alphaPickerButton.getAttribute('data-value');
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
this.value(null, true);
} else {
this.value(value, true);
}
}
}
function onAlphaPickerFocusIn(e) {
if (alphaFocusTimeout) {
clearTimeout(alphaFocusTimeout);
alphaFocusTimeout = null;
}
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
if (alphaPickerButton) {
alphaFocusedElement = alphaPickerButton;
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
}
}
function onItemsFocusIn(e) {
const item = dom.parentWithClass(e.target, itemClass);
if (item) {
const prefix = item.getAttribute('data-prefix');
if (prefix && prefix.length) {
itemFocusValue = prefix[0];
if (itemFocusTimeout) {
clearTimeout(itemFocusTimeout);
}
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
}
}
}
this.enabled = function (enabled) {
if (enabled) {
if (itemsContainer) {
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
}
if (options.mode === 'keyboard') {
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
}
if (options.valueChangeEvent !== 'click') {
element.addEventListener('focus', onAlphaPickerFocusIn, true);
} else {
element.addEventListener('click', onAlphaPickerClick.bind(this));
}
} else {
if (itemsContainer) {
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
}
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
element.removeEventListener('click', onAlphaPickerClick.bind(this));
}
};
render(element, options);
this.enabled(true);
this.visible(true);
function onItemFocusTimeout() {
itemFocusTimeout = null;
self.value(itemFocusValue);
}
value(value, applyValue) {
const element = this.options.element;
let btn;
let selected;
let alphaFocusedElement;
let alphaFocusTimeout;
if (value !== undefined) {
if (value != null) {
value = value.toUpperCase();
this._currentValue = value;
function onAlphaFocusTimeout() {
alphaFocusTimeout = null;
if (this.options.mode !== 'keyboard') {
selected = element.querySelector(`.${selectedButtonClass}`);
try {
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
} catch (err) {
console.error('error in querySelector:', err);
}
if (btn && btn !== selected) {
btn.classList.add(selectedButtonClass);
}
if (selected && selected !== btn) {
selected.classList.remove(selectedButtonClass);
}
}
} else {
this._currentValue = value;
selected = element.querySelector(`.${selectedButtonClass}`);
if (selected) {
selected.classList.remove(selectedButtonClass);
}
}
if (document.activeElement === alphaFocusedElement) {
const value = alphaFocusedElement.getAttribute('data-value');
self.value(value, true);
}
}
if (applyValue) {
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
function onAlphaPickerInKeyboardModeClick(e) {
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
if (alphaPickerButton) {
const value = alphaPickerButton.getAttribute('data-value');
element.dispatchEvent(new CustomEvent('alphavalueclicked', {
cancelable: false,
detail: {
value
}
}));
}
return this._currentValue;
}
on(name, fn) {
const element = this.options.element;
element.addEventListener(name, fn);
function onAlphaPickerClick(e) {
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
if (alphaPickerButton) {
const value = alphaPickerButton.getAttribute('data-value');
if ((this._currentValue || '').toUpperCase() === value.toUpperCase()) {
this.value(null, true);
} else {
this.value(value, true);
}
}
}
off(name, fn) {
const element = this.options.element;
element.removeEventListener(name, fn);
function onAlphaPickerFocusIn(e) {
if (alphaFocusTimeout) {
clearTimeout(alphaFocusTimeout);
alphaFocusTimeout = null;
}
const alphaPickerButton = dom.parentWithClass(e.target, 'alphaPickerButton');
if (alphaPickerButton) {
alphaFocusedElement = alphaPickerButton;
alphaFocusTimeout = setTimeout(onAlphaFocusTimeout, 600);
}
}
updateControls(query) {
if (query.NameLessThan) {
this.value('#');
function onItemsFocusIn(e) {
const item = dom.parentWithClass(e.target, itemClass);
if (item) {
const prefix = item.getAttribute('data-prefix');
if (prefix && prefix.length) {
itemFocusValue = prefix[0];
if (itemFocusTimeout) {
clearTimeout(itemFocusTimeout);
}
itemFocusTimeout = setTimeout(onItemFocusTimeout, 100);
}
}
}
this.enabled = function (enabled) {
if (enabled) {
if (itemsContainer) {
itemsContainer.addEventListener('focus', onItemsFocusIn, true);
}
if (options.mode === 'keyboard') {
element.addEventListener('click', onAlphaPickerInKeyboardModeClick);
}
if (options.valueChangeEvent !== 'click') {
element.addEventListener('focus', onAlphaPickerFocusIn, true);
} else {
element.addEventListener('click', onAlphaPickerClick.bind(this));
}
} else {
this.value(query.NameStartsWith);
if (itemsContainer) {
itemsContainer.removeEventListener('focus', onItemsFocusIn, true);
}
element.removeEventListener('click', onAlphaPickerInKeyboardModeClick);
element.removeEventListener('focus', onAlphaPickerFocusIn, true);
element.removeEventListener('click', onAlphaPickerClick.bind(this));
}
};
this.visible(query.SortBy.indexOf('SortName') !== -1);
}
render(element, options);
visible(visible) {
const element = this.options.element;
element.style.visibility = visible ? 'visible' : 'hidden';
}
values() {
const element = this.options.element;
const elems = element.querySelectorAll('.alphaPickerButton');
const values = [];
for (let i = 0, length = elems.length; i < length; i++) {
values.push(elems[i].getAttribute('data-value'));
}
return values;
}
focus() {
const element = this.options.element;
focusManager.autoFocus(element, true);
}
destroy() {
const element = this.options.element;
this.enabled(false);
element.classList.remove('focuscontainer-x');
this.options = null;
}
this.enabled(true);
this.visible(true);
}
/* eslint-enable indent */
value(value, applyValue) {
const element = this.options.element;
let btn;
let selected;
if (value !== undefined) {
if (value != null) {
value = value.toUpperCase();
this._currentValue = value;
if (this.options.mode !== 'keyboard') {
selected = element.querySelector(`.${selectedButtonClass}`);
try {
btn = element.querySelector(`.alphaPickerButton[data-value='${value}']`);
} catch (err) {
console.error('error in querySelector:', err);
}
if (btn && btn !== selected) {
btn.classList.add(selectedButtonClass);
}
if (selected && selected !== btn) {
selected.classList.remove(selectedButtonClass);
}
}
} else {
this._currentValue = value;
selected = element.querySelector(`.${selectedButtonClass}`);
if (selected) {
selected.classList.remove(selectedButtonClass);
}
}
}
if (applyValue) {
element.dispatchEvent(new CustomEvent('alphavaluechanged', {
cancelable: false,
detail: {
value
}
}));
}
return this._currentValue;
}
on(name, fn) {
const element = this.options.element;
element.addEventListener(name, fn);
}
off(name, fn) {
const element = this.options.element;
element.removeEventListener(name, fn);
}
updateControls(query) {
if (query.NameLessThan) {
this.value('#');
} else {
this.value(query.NameStartsWith);
}
this.visible(query.SortBy.indexOf('SortName') !== -1);
}
visible(visible) {
const element = this.options.element;
element.style.visibility = visible ? 'visible' : 'hidden';
}
values() {
const element = this.options.element;
const elems = element.querySelectorAll('.alphaPickerButton');
const values = [];
for (let i = 0, length = elems.length; i < length; i++) {
values.push(elems[i].getAttribute('data-value'));
}
return values;
}
focus() {
const element = this.options.element;
focusManager.autoFocus(element, true);
}
destroy() {
const element = this.options.element;
this.enabled(false);
element.classList.remove('focuscontainer-x');
this.options = null;
}
}
export default AlphaPicker;

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module for performing auto-focus.
* @module components/autoFocuser
@ -8,93 +6,91 @@
import focusManager from './focusManager';
import layoutManager from './layoutManager';
/**
/**
* Previously selected element.
*/
let activeElement;
let activeElement;
/**
/**
* Returns _true_ if AutoFocuser is enabled.
*/
export function isEnabled() {
return layoutManager.tv;
}
export function isEnabled() {
return layoutManager.tv;
}
/**
/**
* Start AutoFocuser.
*/
export function enable() {
if (!isEnabled()) {
return;
}
window.addEventListener('focusin', function (e) {
activeElement = e.target;
});
console.debug('AutoFocuser enabled');
export function enable() {
if (!isEnabled()) {
return;
}
/**
window.addEventListener('focusin', function (e) {
activeElement = e.target;
});
console.debug('AutoFocuser enabled');
}
/**
* Set focus on a suitable element, taking into account the previously selected.
* @param {HTMLElement} [container] - Element to limit scope.
* @returns {HTMLElement} Focused element.
*/
export function autoFocus(container) {
if (!isEnabled()) {
return null;
}
container = container || document.body;
let candidates = [];
if (activeElement) {
// These elements are recreated
if (activeElement.classList.contains('btnPreviousPage')) {
candidates.push(container.querySelector('.btnPreviousPage'));
candidates.push(container.querySelector('.btnNextPage'));
} else if (activeElement.classList.contains('btnNextPage')) {
candidates.push(container.querySelector('.btnNextPage'));
candidates.push(container.querySelector('.btnPreviousPage'));
} else if (activeElement.classList.contains('btnSelectView')) {
candidates.push(container.querySelector('.btnSelectView'));
}
candidates.push(activeElement);
}
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
let focusedElement;
candidates.every(function (element) {
if (focusManager.isCurrentlyFocusable(element)) {
focusManager.focus(element);
focusedElement = element;
return false;
}
return true;
});
if (!focusedElement) {
// FIXME: Multiple itemsContainers
const itemsContainer = container.querySelector('.itemsContainer');
if (itemsContainer) {
focusedElement = focusManager.autoFocus(itemsContainer);
}
}
if (!focusedElement) {
focusedElement = focusManager.autoFocus(container);
}
return focusedElement;
export function autoFocus(container) {
if (!isEnabled()) {
return null;
}
/* eslint-enable indent */
container = container || document.body;
let candidates = [];
if (activeElement) {
// These elements are recreated
if (activeElement.classList.contains('btnPreviousPage')) {
candidates.push(container.querySelector('.btnPreviousPage'));
candidates.push(container.querySelector('.btnNextPage'));
} else if (activeElement.classList.contains('btnNextPage')) {
candidates.push(container.querySelector('.btnNextPage'));
candidates.push(container.querySelector('.btnPreviousPage'));
} else if (activeElement.classList.contains('btnSelectView')) {
candidates.push(container.querySelector('.btnSelectView'));
}
candidates.push(activeElement);
}
candidates = candidates.concat(Array.from(container.querySelectorAll('.btnPlay')));
let focusedElement;
candidates.every(function (element) {
if (focusManager.isCurrentlyFocusable(element)) {
focusManager.focus(element);
focusedElement = element;
return false;
}
return true;
});
if (!focusedElement) {
// FIXME: Multiple itemsContainers
const itemsContainer = container.querySelector('.itemsContainer');
if (itemsContainer) {
focusedElement = focusManager.autoFocus(itemsContainer);
}
}
if (!focusedElement) {
focusedElement = focusManager.autoFocus(container);
}
return focusedElement;
}
export default {
isEnabled: isEnabled,

View file

@ -7,282 +7,278 @@ import ServerConnections from '../ServerConnections';
import './backdrop.scss';
/* eslint-disable indent */
function enableAnimation() {
return !browser.slow;
}
function enableAnimation() {
return !browser.slow;
}
function enableRotation() {
return !browser.tv
function enableRotation() {
return !browser.tv
// Causes high cpu usage
&& !browser.firefox;
}
}
class Backdrop {
load(url, parent, existingBackdropImage) {
const img = new Image();
const self = this;
class Backdrop {
load(url, parent, existingBackdropImage) {
const img = new Image();
const self = this;
img.onload = () => {
if (self.isDestroyed) {
return;
}
const backdropImage = document.createElement('div');
backdropImage.classList.add('backdropImage');
backdropImage.classList.add('displayingBackdropImage');
backdropImage.style.backgroundImage = `url('${url}')`;
backdropImage.setAttribute('data-url', url);
backdropImage.classList.add('backdropImageFadeIn');
parent.appendChild(backdropImage);
if (!enableAnimation()) {
if (existingBackdropImage && existingBackdropImage.parentNode) {
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
}
internalBackdrop(true);
return;
}
const onAnimationComplete = () => {
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
once: true
});
if (backdropImage === self.currentAnimatingElement) {
self.currentAnimatingElement = null;
}
if (existingBackdropImage && existingBackdropImage.parentNode) {
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
}
};
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
once: true
});
internalBackdrop(true);
};
img.src = url;
}
cancelAnimation() {
const elem = this.currentAnimatingElement;
if (elem) {
elem.classList.remove('backdropImageFadeIn');
this.currentAnimatingElement = null;
}
}
destroy() {
this.isDestroyed = true;
this.cancelAnimation();
}
}
let backdropContainer;
function getBackdropContainer() {
if (!backdropContainer) {
backdropContainer = document.querySelector('.backdropContainer');
}
if (!backdropContainer) {
backdropContainer = document.createElement('div');
backdropContainer.classList.add('backdropContainer');
document.body.insertBefore(backdropContainer, document.body.firstChild);
}
return backdropContainer;
}
export function clearBackdrop(clearAll) {
clearRotation();
if (currentLoadingBackdrop) {
currentLoadingBackdrop.destroy();
currentLoadingBackdrop = null;
}
const elem = getBackdropContainer();
elem.innerHTML = '';
if (clearAll) {
hasExternalBackdrop = false;
}
internalBackdrop(false);
}
let backgroundContainer;
function getBackgroundContainer() {
if (!backgroundContainer) {
backgroundContainer = document.querySelector('.backgroundContainer');
}
return backgroundContainer;
}
function setBackgroundContainerBackgroundEnabled() {
if (hasInternalBackdrop || hasExternalBackdrop) {
getBackgroundContainer().classList.add('withBackdrop');
} else {
getBackgroundContainer().classList.remove('withBackdrop');
}
}
let hasInternalBackdrop;
function internalBackdrop(isEnabled) {
hasInternalBackdrop = isEnabled;
setBackgroundContainerBackgroundEnabled();
}
let hasExternalBackdrop;
export function externalBackdrop(isEnabled) {
hasExternalBackdrop = isEnabled;
setBackgroundContainerBackgroundEnabled();
}
let currentLoadingBackdrop;
function setBackdropImage(url) {
if (currentLoadingBackdrop) {
currentLoadingBackdrop.destroy();
currentLoadingBackdrop = null;
}
const elem = getBackdropContainer();
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
if (existingBackdropImage.getAttribute('data-url') === url) {
img.onload = () => {
if (self.isDestroyed) {
return;
}
existingBackdropImage.classList.remove('displayingBackdropImage');
}
const instance = new Backdrop();
instance.load(url, elem, existingBackdropImage);
currentLoadingBackdrop = instance;
}
const backdropImage = document.createElement('div');
backdropImage.classList.add('backdropImage');
backdropImage.classList.add('displayingBackdropImage');
backdropImage.style.backgroundImage = `url('${url}')`;
backdropImage.setAttribute('data-url', url);
function getItemImageUrls(item, imageOptions) {
imageOptions = imageOptions || {};
backdropImage.classList.add('backdropImageFadeIn');
parent.appendChild(backdropImage);
const apiClient = ServerConnections.getApiClient(item.ServerId);
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
return item.BackdropImageTags.map((imgTag, index) => {
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
type: 'Backdrop',
tag: imgTag,
maxWidth: dom.getScreenWidth(),
index: index
}));
if (!enableAnimation()) {
if (existingBackdropImage && existingBackdropImage.parentNode) {
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
}
internalBackdrop(true);
return;
}
const onAnimationComplete = () => {
dom.removeEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
once: true
});
if (backdropImage === self.currentAnimatingElement) {
self.currentAnimatingElement = null;
}
if (existingBackdropImage && existingBackdropImage.parentNode) {
existingBackdropImage.parentNode.removeChild(existingBackdropImage);
}
};
dom.addEventListener(backdropImage, dom.whichAnimationEvent(), onAnimationComplete, {
once: true
});
}
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
return item.ParentBackdropImageTags.map((imgTag, index) => {
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
type: 'Backdrop',
tag: imgTag,
maxWidth: dom.getScreenWidth(),
index: index
}));
});
}
return [];
}
function getImageUrls(items, imageOptions) {
const list = [];
const onImg = img => {
list.push(img);
internalBackdrop(true);
};
for (let i = 0, length = items.length; i < length; i++) {
const itemImages = getItemImageUrls(items[i], imageOptions);
itemImages.forEach(onImg);
}
return list;
img.src = url;
}
function enabled() {
return userSettings.enableBackdrops();
}
let rotationInterval;
let currentRotatingImages = [];
let currentRotationIndex = -1;
export function setBackdrops(items, imageOptions, enableImageRotation) {
if (enabled()) {
const images = getImageUrls(items, imageOptions);
if (images.length) {
startRotation(images, enableImageRotation);
} else {
clearBackdrop();
}
cancelAnimation() {
const elem = this.currentAnimatingElement;
if (elem) {
elem.classList.remove('backdropImageFadeIn');
this.currentAnimatingElement = null;
}
}
function startRotation(images, enableImageRotation) {
if (isEqual(images, currentRotatingImages)) {
destroy() {
this.isDestroyed = true;
this.cancelAnimation();
}
}
let backdropContainer;
function getBackdropContainer() {
if (!backdropContainer) {
backdropContainer = document.querySelector('.backdropContainer');
}
if (!backdropContainer) {
backdropContainer = document.createElement('div');
backdropContainer.classList.add('backdropContainer');
document.body.insertBefore(backdropContainer, document.body.firstChild);
}
return backdropContainer;
}
export function clearBackdrop(clearAll) {
clearRotation();
if (currentLoadingBackdrop) {
currentLoadingBackdrop.destroy();
currentLoadingBackdrop = null;
}
const elem = getBackdropContainer();
elem.innerHTML = '';
if (clearAll) {
hasExternalBackdrop = false;
}
internalBackdrop(false);
}
let backgroundContainer;
function getBackgroundContainer() {
if (!backgroundContainer) {
backgroundContainer = document.querySelector('.backgroundContainer');
}
return backgroundContainer;
}
function setBackgroundContainerBackgroundEnabled() {
if (hasInternalBackdrop || hasExternalBackdrop) {
getBackgroundContainer().classList.add('withBackdrop');
} else {
getBackgroundContainer().classList.remove('withBackdrop');
}
}
let hasInternalBackdrop;
function internalBackdrop(isEnabled) {
hasInternalBackdrop = isEnabled;
setBackgroundContainerBackgroundEnabled();
}
let hasExternalBackdrop;
export function externalBackdrop(isEnabled) {
hasExternalBackdrop = isEnabled;
setBackgroundContainerBackgroundEnabled();
}
let currentLoadingBackdrop;
function setBackdropImage(url) {
if (currentLoadingBackdrop) {
currentLoadingBackdrop.destroy();
currentLoadingBackdrop = null;
}
const elem = getBackdropContainer();
const existingBackdropImage = elem.querySelector('.displayingBackdropImage');
if (existingBackdropImage && existingBackdropImage.getAttribute('data-url') === url) {
if (existingBackdropImage.getAttribute('data-url') === url) {
return;
}
clearRotation();
currentRotatingImages = images;
currentRotationIndex = -1;
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
rotationInterval = setInterval(onRotationInterval, 24000);
}
onRotationInterval();
existingBackdropImage.classList.remove('displayingBackdropImage');
}
function onRotationInterval() {
if (playbackManager.isPlayingLocally(['Video'])) {
return;
}
const instance = new Backdrop();
instance.load(url, elem, existingBackdropImage);
currentLoadingBackdrop = instance;
}
let newIndex = currentRotationIndex + 1;
if (newIndex >= currentRotatingImages.length) {
newIndex = 0;
}
function getItemImageUrls(item, imageOptions) {
imageOptions = imageOptions || {};
currentRotationIndex = newIndex;
setBackdropImage(currentRotatingImages[newIndex]);
const apiClient = ServerConnections.getApiClient(item.ServerId);
if (item.BackdropImageTags && item.BackdropImageTags.length > 0) {
return item.BackdropImageTags.map((imgTag, index) => {
return apiClient.getScaledImageUrl(item.BackdropItemId || item.Id, Object.assign(imageOptions, {
type: 'Backdrop',
tag: imgTag,
maxWidth: dom.getScreenWidth(),
index: index
}));
});
}
function clearRotation() {
const interval = rotationInterval;
if (interval) {
clearInterval(interval);
}
rotationInterval = null;
currentRotatingImages = [];
currentRotationIndex = -1;
if (item.ParentBackdropItemId && item.ParentBackdropImageTags && item.ParentBackdropImageTags.length) {
return item.ParentBackdropImageTags.map((imgTag, index) => {
return apiClient.getScaledImageUrl(item.ParentBackdropItemId, Object.assign(imageOptions, {
type: 'Backdrop',
tag: imgTag,
maxWidth: dom.getScreenWidth(),
index: index
}));
});
}
export function setBackdrop(url, imageOptions) {
if (url && typeof url !== 'string') {
url = getImageUrls([url], imageOptions)[0];
}
return [];
}
if (url) {
clearRotation();
setBackdropImage(url);
function getImageUrls(items, imageOptions) {
const list = [];
const onImg = img => {
list.push(img);
};
for (let i = 0, length = items.length; i < length; i++) {
const itemImages = getItemImageUrls(items[i], imageOptions);
itemImages.forEach(onImg);
}
return list;
}
function enabled() {
return userSettings.enableBackdrops();
}
let rotationInterval;
let currentRotatingImages = [];
let currentRotationIndex = -1;
export function setBackdrops(items, imageOptions, enableImageRotation) {
if (enabled()) {
const images = getImageUrls(items, imageOptions);
if (images.length) {
startRotation(images, enableImageRotation);
} else {
clearBackdrop();
}
}
}
/* eslint-enable indent */
function startRotation(images, enableImageRotation) {
if (isEqual(images, currentRotatingImages)) {
return;
}
clearRotation();
currentRotatingImages = images;
currentRotationIndex = -1;
if (images.length > 1 && enableImageRotation !== false && enableRotation()) {
rotationInterval = setInterval(onRotationInterval, 24000);
}
onRotationInterval();
}
function onRotationInterval() {
if (playbackManager.isPlayingLocally(['Video'])) {
return;
}
let newIndex = currentRotationIndex + 1;
if (newIndex >= currentRotatingImages.length) {
newIndex = 0;
}
currentRotationIndex = newIndex;
setBackdropImage(currentRotatingImages[newIndex]);
}
function clearRotation() {
const interval = rotationInterval;
if (interval) {
clearInterval(interval);
}
rotationInterval = null;
currentRotatingImages = [];
currentRotationIndex = -1;
}
export function setBackdrop(url, imageOptions) {
if (url && typeof url !== 'string') {
url = getImageUrls([url], imageOptions)[0];
}
if (url) {
clearRotation();
setBackdropImage(url);
} else {
clearBackdrop();
}
}
/**
* @enum TransparencyLevel

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for building cards from item data.
@ -12,123 +11,121 @@ import layoutManager from '../layoutManager';
import browser from '../../scripts/browser';
import ServerConnections from '../ServerConnections';
const enableFocusTransform = !browser.slow && !browser.edge;
const enableFocusTransform = !browser.slow && !browser.edge;
function buildChapterCardsHtml(item, chapters, options) {
// TODO move card creation code to Card component
function buildChapterCardsHtml(item, chapters, options) {
// TODO move card creation code to Card component
let className = 'card itemAction chapterCard';
let className = 'card itemAction chapterCard';
if (layoutManager.tv) {
className += ' show-focus';
if (layoutManager.tv) {
className += ' show-focus';
if (enableFocusTransform) {
className += ' show-animation';
}
if (enableFocusTransform) {
className += ' show-animation';
}
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
const videoStream = mediaStreams.filter(({ Type }) => {
return Type === 'Video';
})[0] || {};
let shape = (options.backdropShape || 'backdrop');
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
shape = (options.squareShape || 'square');
}
className += ` ${shape}Card`;
if (options.block || options.rows) {
className += ' block';
}
let html = '';
let itemsInRow = 0;
const apiClient = ServerConnections.getApiClient(item.ServerId);
for (let i = 0, length = chapters.length; i < length; i++) {
if (options.rows && itemsInRow === 0) {
html += '<div class="cardColumn">';
}
const chapter = chapters[i];
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
itemsInRow++;
if (options.rows && itemsInRow >= options.rows) {
itemsInRow = 0;
html += '</div>';
}
}
return html;
}
function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
if (ImageTag) {
return apiClient.getScaledImageUrl(Id, {
const mediaStreams = ((item.MediaSources || [])[0] || {}).MediaStreams || [];
const videoStream = mediaStreams.filter(({ Type }) => {
return Type === 'Video';
})[0] || {};
maxWidth: maxWidth,
tag: ImageTag,
type: 'Chapter',
index
});
}
let shape = (options.backdropShape || 'backdrop');
return null;
if (videoStream.Width && videoStream.Height && (videoStream.Width / videoStream.Height) <= 1.2) {
shape = (options.squareShape || 'square');
}
function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
className += ` ${shape}Card`;
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
if (coverImage) {
cardImageContainerClass += ' coveredImage';
}
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
if (!imgUrl) {
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
}
let nameHtml = '';
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
const cardBoxCssClass = 'cardBox';
const cardScalableClass = 'cardScalable';
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
if (options.block || options.rows) {
className += ' block';
}
export function buildChapterCards(item, chapters, options) {
if (options.parentContainer) {
// Abort if the container has been disposed
if (!document.body.contains(options.parentContainer)) {
return;
}
let html = '';
let itemsInRow = 0;
if (chapters.length) {
options.parentContainer.classList.remove('hide');
} else {
options.parentContainer.classList.add('hide');
return;
}
const apiClient = ServerConnections.getApiClient(item.ServerId);
for (let i = 0, length = chapters.length; i < length; i++) {
if (options.rows && itemsInRow === 0) {
html += '<div class="cardColumn">';
}
const html = buildChapterCardsHtml(item, chapters, options);
const chapter = chapters[i];
options.itemsContainer.innerHTML = html;
html += buildChapterCard(item, apiClient, chapter, i, options, className, shape);
itemsInRow++;
imageLoader.lazyChildren(options.itemsContainer);
if (options.rows && itemsInRow >= options.rows) {
itemsInRow = 0;
html += '</div>';
}
}
/* eslint-enable indent */
return html;
}
function getImgUrl({ Id }, { ImageTag }, index, maxWidth, apiClient) {
if (ImageTag) {
return apiClient.getScaledImageUrl(Id, {
maxWidth: maxWidth,
tag: ImageTag,
type: 'Chapter',
index
});
}
return null;
}
function buildChapterCard(item, apiClient, chapter, index, { width, coverImage }, className, shape) {
const imgUrl = getImgUrl(item, chapter, index, width || 400, apiClient);
let cardImageContainerClass = 'cardContent cardContent-shadow cardImageContainer chapterCardImageContainer';
if (coverImage) {
cardImageContainerClass += ' coveredImage';
}
const dataAttributes = ` data-action="play" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-positionticks="${chapter.StartPositionTicks}"`;
let cardImageContainer = imgUrl ? (`<div class="${cardImageContainerClass} lazy" data-src="${imgUrl}">`) : (`<div class="${cardImageContainerClass}">`);
if (!imgUrl) {
cardImageContainer += '<span class="material-icons cardImageIcon local_movies" aria-hidden="true"></span>';
}
let nameHtml = '';
nameHtml += `<div class="cardText">${escapeHtml(chapter.Name)}</div>`;
nameHtml += `<div class="cardText">${datetime.getDisplayRunningTime(chapter.StartPositionTicks)}</div>`;
const cardBoxCssClass = 'cardBox';
const cardScalableClass = 'cardScalable';
return `<button type="button" class="${className}"${dataAttributes}><div class="${cardBoxCssClass}"><div class="${cardScalableClass}"><div class="cardPadder-${shape}"></div>${cardImageContainer}</div><div class="innerCardFooter">${nameHtml}</div></div></div></button>`;
}
export function buildChapterCards(item, chapters, options) {
if (options.parentContainer) {
// Abort if the container has been disposed
if (!document.body.contains(options.parentContainer)) {
return;
}
if (chapters.length) {
options.parentContainer.classList.remove('hide');
} else {
options.parentContainer.classList.add('hide');
return;
}
}
const html = buildChapterCardsHtml(item, chapters, options);
options.itemsContainer.innerHTML = html;
imageLoader.lazyChildren(options.itemsContainer);
}
export default {
buildChapterCards: buildChapterCards

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for building cards from item data.
@ -7,20 +6,18 @@
import cardBuilder from './cardBuilder';
export function buildPeopleCards(items, options) {
options = Object.assign(options || {}, {
cardLayout: false,
centerText: true,
showTitle: true,
cardFooterAside: 'none',
showPersonRoleOrType: true,
cardCssClass: 'personCard',
defaultCardImageIcon: 'person'
});
cardBuilder.buildCards(items, options);
}
/* eslint-enable indent */
export function buildPeopleCards(items, options) {
options = Object.assign(options || {}, {
cardLayout: false,
centerText: true,
showTitle: true,
cardFooterAside: 'none',
showPersonRoleOrType: true,
cardCssClass: 'personCard',
defaultCardImageIcon: 'person'
});
cardBuilder.buildCards(items, options);
}
export default {
buildPeopleCards: buildPeopleCards

View file

@ -16,254 +16,251 @@ import '../../styles/flexstyles.scss';
import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
/* eslint-disable indent */
let currentServerId;
let currentServerId;
function onSubmit(e) {
loading.show();
function onSubmit(e) {
loading.show();
const panel = dom.parentWithClass(this, 'dialog');
const panel = dom.parentWithClass(this, 'dialog');
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
const collectionId = panel.querySelector('#selectCollectionToAddTo').value;
const apiClient = ServerConnections.getApiClient(currentServerId);
const apiClient = ServerConnections.getApiClient(currentServerId);
if (collectionId) {
addToCollection(apiClient, panel, collectionId);
} else {
createCollection(apiClient, panel);
}
e.preventDefault();
return false;
if (collectionId) {
addToCollection(apiClient, panel, collectionId);
} else {
createCollection(apiClient, panel);
}
function createCollection(apiClient, dlg) {
const url = apiClient.getUrl('Collections', {
e.preventDefault();
return false;
}
Name: dlg.querySelector('#txtNewCollectionName').value,
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
});
function createCollection(apiClient, dlg) {
const url = apiClient.getUrl('Collections', {
apiClient.ajax({
type: 'POST',
url: url,
dataType: 'json'
Name: dlg.querySelector('#txtNewCollectionName').value,
IsLocked: !dlg.querySelector('#chkEnableInternetMetadata').checked,
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
});
}).then(result => {
loading.hide();
apiClient.ajax({
type: 'POST',
url: url,
dataType: 'json'
const id = result.Id;
}).then(result => {
loading.hide();
dlg.submitted = true;
dialogHelper.close(dlg);
redirectToCollection(apiClient, id);
});
}
const id = result.Id;
function redirectToCollection(apiClient, id) {
appRouter.showItem(id, apiClient.serverId());
}
dlg.submitted = true;
dialogHelper.close(dlg);
redirectToCollection(apiClient, id);
});
}
function addToCollection(apiClient, dlg, id) {
const url = apiClient.getUrl(`Collections/${id}/Items`, {
function redirectToCollection(apiClient, id) {
appRouter.showItem(id, apiClient.serverId());
}
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
});
function addToCollection(apiClient, dlg, id) {
const url = apiClient.getUrl(`Collections/${id}/Items`, {
apiClient.ajax({
type: 'POST',
url: url
Ids: dlg.querySelector('.fldSelectedItemIds').value || ''
});
}).then(() => {
loading.hide();
apiClient.ajax({
type: 'POST',
url: url
dlg.submitted = true;
dialogHelper.close(dlg);
}).then(() => {
loading.hide();
toast(globalize.translate('MessageItemsAdded'));
});
}
dlg.submitted = true;
dialogHelper.close(dlg);
function triggerChange(select) {
select.dispatchEvent(new CustomEvent('change', {}));
}
toast(globalize.translate('MessageItemsAdded'));
});
}
function populateCollections(panel) {
loading.show();
function triggerChange(select) {
select.dispatchEvent(new CustomEvent('change', {}));
}
const select = panel.querySelector('#selectCollectionToAddTo');
function populateCollections(panel) {
loading.show();
panel.querySelector('.newCollectionInfo').classList.add('hide');
const select = panel.querySelector('#selectCollectionToAddTo');
const options = {
panel.querySelector('.newCollectionInfo').classList.add('hide');
Recursive: true,
IncludeItemTypes: 'BoxSet',
SortBy: 'SortName',
EnableTotalRecordCount: false
};
const options = {
const apiClient = ServerConnections.getApiClient(currentServerId);
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = '';
Recursive: true,
IncludeItemTypes: 'BoxSet',
SortBy: 'SortName',
EnableTotalRecordCount: false
};
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += result.Items.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
select.innerHTML = html;
select.value = '';
triggerChange(select);
loading.hide();
});
}
function getEditorHtml() {
const apiClient = ServerConnections.getApiClient(currentServerId);
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = '';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form class="newCollectionForm" style="margin:auto;">';
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += '<div>';
html += globalize.translate('NewCollectionHelp');
html += '</div>';
html += '<div class="fldSelectCollection">';
html += '<br/>';
html += '<br/>';
html += '<div class="selectContainer">';
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
html += '</div>';
html += '</div>';
html += '<div class="newCollectionInfo">';
html += '<div class="inputContainer">';
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
html += '</div>';
html += '<label class="checkboxContainer">';
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
html += '</label>';
// newCollectionInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
return html;
}
function initEditor(content, items) {
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
if (this.value) {
content.querySelector('.newCollectionInfo').classList.add('hide');
content.querySelector('#txtNewCollectionName').removeAttribute('required');
} else {
content.querySelector('.newCollectionInfo').classList.remove('hide');
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
}
html += result.Items.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
content.querySelector('form').addEventListener('submit', onSubmit);
select.innerHTML = html;
select.value = '';
triggerChange(select);
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
loading.hide();
});
}
if (items.length) {
content.querySelector('.fldSelectCollection').classList.remove('hide');
populateCollections(content);
function getEditorHtml() {
let html = '';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form class="newCollectionForm" style="margin:auto;">';
html += '<div>';
html += globalize.translate('NewCollectionHelp');
html += '</div>';
html += '<div class="fldSelectCollection">';
html += '<br/>';
html += '<br/>';
html += '<div class="selectContainer">';
html += `<select is="emby-select" label="${globalize.translate('LabelCollection')}" id="selectCollectionToAddTo" autofocus></select>`;
html += '</div>';
html += '</div>';
html += '<div class="newCollectionInfo">';
html += '<div class="inputContainer">';
html += `<input is="emby-input" type="text" id="txtNewCollectionName" required="required" label="${globalize.translate('LabelName')}" />`;
html += `<div class="fieldDescription">${globalize.translate('NewCollectionNameExample')}</div>`;
html += '</div>';
html += '<label class="checkboxContainer">';
html += '<input is="emby-checkbox" type="checkbox" id="chkEnableInternetMetadata" />';
html += `<span>${globalize.translate('SearchForCollectionInternetMetadata')}</span>`;
html += '</label>';
// newCollectionInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('ButtonOk')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
return html;
}
function initEditor(content, items) {
content.querySelector('#selectCollectionToAddTo').addEventListener('change', function () {
if (this.value) {
content.querySelector('.newCollectionInfo').classList.add('hide');
content.querySelector('#txtNewCollectionName').removeAttribute('required');
} else {
content.querySelector('.fldSelectCollection').classList.add('hide');
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
selectCollectionToAddTo.innerHTML = '';
selectCollectionToAddTo.value = '';
triggerChange(selectCollectionToAddTo);
content.querySelector('.newCollectionInfo').classList.remove('hide');
content.querySelector('#txtNewCollectionName').setAttribute('required', 'required');
}
}
});
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
content.querySelector('form').addEventListener('submit', onSubmit);
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
if (items.length) {
content.querySelector('.fldSelectCollection').classList.remove('hide');
populateCollections(content);
} else {
content.querySelector('.fldSelectCollection').classList.add('hide');
const selectCollectionToAddTo = content.querySelector('#selectCollectionToAddTo');
selectCollectionToAddTo.innerHTML = '';
selectCollectionToAddTo.value = '';
triggerChange(selectCollectionToAddTo);
}
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
}
class CollectionEditor {
show(options) {
const items = options.items || {};
currentServerId = options.serverId;
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
html += '<div class="formDialogHeader">';
html += `<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">';
html += title;
html += '</h3>';
html += '</div>';
html += getEditorHtml();
dlg.innerHTML = html;
initEditor(dlg, items);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
return dialogHelper.open(dlg).then(() => {
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (dlg.submitted) {
return Promise.resolve();
}
return Promise.reject();
});
}
}
class CollectionEditor {
show(options) {
const items = options.items || {};
currentServerId = options.serverId;
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
const title = items.length ? globalize.translate('HeaderAddToCollection') : globalize.translate('NewCollection');
html += '<div class="formDialogHeader">';
html += `<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">';
html += title;
html += '</h3>';
html += '</div>';
html += getEditorHtml();
dlg.innerHTML = html;
initEditor(dlg, items);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
return dialogHelper.open(dlg).then(() => {
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (dlg.submitted) {
return Promise.resolve();
}
return Promise.reject();
});
}
}
/* eslint-enable indent */
export default CollectionEditor;

View file

@ -13,129 +13,126 @@ import '../formdialog.scss';
import '../../styles/flexstyles.scss';
import template from './dialog.template.html';
/* eslint-disable indent */
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
const dialogOptions = {
removeOnClose: true,
scrollY: false,
...options.dialogOptions
};
function showDialog(options = { dialogOptions: {}, buttons: [] }) {
const dialogOptions = {
removeOnClose: true,
scrollY: false,
...options.dialogOptions
};
const enableTvLayout = layoutManager.tv;
const enableTvLayout = layoutManager.tv;
if (enableTvLayout) {
dialogOptions.size = 'fullscreen';
}
if (enableTvLayout) {
dialogOptions.size = 'fullscreen';
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template, 'core');
dlg.classList.add('align-items-center');
dlg.classList.add('justify-content-center');
const formDialogContent = dlg.querySelector('.formDialogContent');
formDialogContent.classList.add('no-grow');
if (enableTvLayout) {
formDialogContent.style['max-width'] = '50%';
formDialogContent.style['max-height'] = '60%';
scrollHelper.centerFocus.on(formDialogContent, false);
} else {
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
dlg.classList.add('dialog-fullscreen-lowres');
}
if (options.title) {
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
} else {
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
}
const displayText = options.html || options.text || '';
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
if (!displayText) {
dlg.querySelector('.dialogContentInner').classList.add('hide');
}
let i;
let length;
let html = '';
let hasDescriptions = false;
for (i = 0, length = options.buttons.length; i < length; i++) {
const item = options.buttons[i];
const autoFocus = i === 0 ? ' autofocus' : '';
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
if (item.type) {
buttonClass += ` button-${item.type}`;
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template, 'core');
dlg.classList.add('align-items-center');
dlg.classList.add('justify-content-center');
const formDialogContent = dlg.querySelector('.formDialogContent');
formDialogContent.classList.add('no-grow');
if (enableTvLayout) {
formDialogContent.style['max-width'] = '50%';
formDialogContent.style['max-height'] = '60%';
scrollHelper.centerFocus.on(formDialogContent, false);
} else {
formDialogContent.style.maxWidth = `${Math.min((options.buttons.length * 150) + 200, dom.getWindowSize().innerWidth - 50)}px`;
dlg.classList.add('dialog-fullscreen-lowres');
if (item.description) {
hasDescriptions = true;
}
if (options.title) {
dlg.querySelector('.formDialogHeaderTitle').innerText = options.title || '';
} else {
dlg.querySelector('.formDialogHeaderTitle').classList.add('hide');
}
const displayText = options.html || options.text || '';
dlg.querySelector('.text').innerHTML = DOMPurify.sanitize(displayText);
if (!displayText) {
dlg.querySelector('.dialogContentInner').classList.add('hide');
}
let i;
let length;
let html = '';
let hasDescriptions = false;
for (i = 0, length = options.buttons.length; i < length; i++) {
const item = options.buttons[i];
const autoFocus = i === 0 ? ' autofocus' : '';
let buttonClass = 'btnOption raised formDialogFooterItem formDialogFooterItem-autosize';
if (item.type) {
buttonClass += ` button-${item.type}`;
}
if (item.description) {
hasDescriptions = true;
}
if (hasDescriptions) {
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
}
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
if (item.description) {
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
}
}
dlg.querySelector('.formDialogFooter').innerHTML = html;
if (hasDescriptions) {
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
buttonClass += ' formDialogFooterItem-vertical formDialogFooterItem-nomarginbottom';
}
let dialogResult;
function onButtonClick() {
dialogResult = this.getAttribute('data-id');
dialogHelper.close(dlg);
html += `<button is="emby-button" type="button" class="${buttonClass}" data-id="${item.id}"${autoFocus}>${escapeHtml(item.name)}</button>`;
if (item.description) {
html += `<div class="formDialogFooterItem formDialogFooterItem-autosize fieldDescription" style="margin-top:.25em!important;margin-bottom:1.25em!important;">${item.description}</div>`;
}
const buttons = dlg.querySelectorAll('.btnOption');
for (i = 0, length = buttons.length; i < length; i++) {
buttons[i].addEventListener('click', onButtonClick);
}
return dialogHelper.open(dlg).then(() => {
if (enableTvLayout) {
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
}
if (dialogResult) {
return dialogResult;
} else {
return Promise.reject();
}
});
}
export function show(text, title) {
let options;
if (typeof text === 'string') {
options = {
title: title,
text: text
};
dlg.querySelector('.formDialogFooter').innerHTML = html;
if (hasDescriptions) {
dlg.querySelector('.formDialogFooter').classList.add('formDialogFooter-vertical');
}
let dialogResult;
function onButtonClick() {
dialogResult = this.getAttribute('data-id');
dialogHelper.close(dlg);
}
const buttons = dlg.querySelectorAll('.btnOption');
for (i = 0, length = buttons.length; i < length; i++) {
buttons[i].addEventListener('click', onButtonClick);
}
return dialogHelper.open(dlg).then(() => {
if (enableTvLayout) {
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
}
if (dialogResult) {
return dialogResult;
} else {
options = text;
return Promise.reject();
}
});
}
return showDialog(options);
export function show(text, title) {
let options;
if (typeof text === 'string') {
options = {
title: title,
text: text
};
} else {
options = text;
}
/* eslint-enable indent */
return showDialog(options);
}
export default {
show: show
};

View file

@ -9,506 +9,501 @@ import dom from '../../scripts/dom';
import './dialoghelper.scss';
import '../../styles/scrollstyles.scss';
/* eslint-disable indent */
let globalOnOpenCallback;
let globalOnOpenCallback;
function enableAnimation() {
// too slow
if (browser.tv) {
return false;
}
return browser.supportsCssAnimation();
function enableAnimation() {
// too slow
if (browser.tv) {
return false;
}
function removeCenterFocus(dlg) {
if (layoutManager.tv) {
if (dlg.classList.contains('scrollX')) {
centerFocus(dlg, true, false);
} else if (dlg.classList.contains('smoothScrollY')) {
centerFocus(dlg, false, false);
}
return browser.supportsCssAnimation();
}
function removeCenterFocus(dlg) {
if (layoutManager.tv) {
if (dlg.classList.contains('scrollX')) {
centerFocus(dlg, true, false);
} else if (dlg.classList.contains('smoothScrollY')) {
centerFocus(dlg, false, false);
}
}
}
function tryRemoveElement(elem) {
const parentNode = elem.parentNode;
if (parentNode) {
// Seeing crashes in edge webview
try {
parentNode.removeChild(elem);
} catch (err) {
console.error('[dialogHelper] error removing dialog element: ' + err);
}
}
}
function DialogHashHandler(dlg, hash, resolve) {
const self = this;
self.originalUrl = window.location.href;
const activeElement = document.activeElement;
let removeScrollLockOnClose = false;
let unlisten;
function onHashChange({ location }) {
const dialogs = location.state?.dialogs || [];
const shouldClose = !dialogs.includes(hash);
if ((shouldClose || !isOpened(dlg)) && unlisten) {
unlisten();
unlisten = null;
}
if (shouldClose) {
close(dlg);
}
}
function tryRemoveElement(elem) {
const parentNode = elem.parentNode;
if (parentNode) {
// Seeing crashes in edge webview
try {
parentNode.removeChild(elem);
} catch (err) {
console.error('[dialogHelper] error removing dialog element: ' + err);
}
}
}
function DialogHashHandler(dlg, hash, resolve) {
const self = this;
self.originalUrl = window.location.href;
const activeElement = document.activeElement;
let removeScrollLockOnClose = false;
let unlisten;
function onHashChange({ location }) {
const dialogs = location.state?.dialogs || [];
const shouldClose = !dialogs.includes(hash);
if ((shouldClose || !isOpened(dlg)) && unlisten) {
unlisten();
unlisten = null;
}
if (shouldClose) {
close(dlg);
}
function finishClose() {
if (unlisten) {
unlisten();
unlisten = null;
}
function finishClose() {
if (unlisten) {
unlisten();
unlisten = null;
}
dlg.dispatchEvent(new CustomEvent('close', {
bubbles: false,
cancelable: false
}));
resolve({
element: dlg
});
}
function onBackCommand(e) {
if (e.detail.command === 'back') {
e.preventDefault();
e.stopPropagation();
close(dlg);
}
}
function onDialogClosed() {
if (!isHistoryEnabled(dlg)) {
inputManager.off(dlg, onBackCommand);
}
if (unlisten) {
unlisten();
unlisten = null;
}
removeBackdrop(dlg);
dlg.classList.remove('opened');
if (removeScrollLockOnClose) {
document.body.classList.remove('noScroll');
}
if (isHistoryEnabled(dlg)) {
const state = history.location.state || {};
if (state.dialogs?.length > 0) {
if (state.dialogs[state.dialogs.length - 1] === hash) {
unlisten = history.listen(finishClose);
history.back();
} else if (state.dialogs.includes(hash)) {
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
unlisten = history.listen(finishClose);
// Remove the closed dialog hash from the history state
history.replace(
`${history.location.pathname}${history.location.search}`,
{
...state,
dialogs: state.dialogs.filter(dialog => dialog !== hash)
}
);
}
}
}
if (layoutManager.tv) {
focusManager.focus(activeElement);
}
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
removeCenterFocus(dlg);
const dialogContainer = dlg.dialogContainer;
if (dialogContainer) {
tryRemoveElement(dialogContainer);
dlg.dialogContainer = null;
} else {
tryRemoveElement(dlg);
}
}
if (!unlisten) {
finishClose();
}
}
dlg.addEventListener('_close', onDialogClosed);
const center = !dlg.classList.contains('dialog-fixedSize');
if (center) {
dlg.classList.add('centeredDialog');
}
dlg.classList.remove('hide');
addBackdropOverlay(dlg);
dlg.classList.add('opened');
dlg.dispatchEvent(new CustomEvent('open', {
dlg.dispatchEvent(new CustomEvent('close', {
bubbles: false,
cancelable: false
}));
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
document.body.classList.add('noScroll');
removeScrollLockOnClose = true;
resolve({
element: dlg
});
}
function onBackCommand(e) {
if (e.detail.command === 'back') {
e.preventDefault();
e.stopPropagation();
close(dlg);
}
}
function onDialogClosed() {
if (!isHistoryEnabled(dlg)) {
inputManager.off(dlg, onBackCommand);
}
animateDialogOpen(dlg);
if (unlisten) {
unlisten();
unlisten = null;
}
removeBackdrop(dlg);
dlg.classList.remove('opened');
if (removeScrollLockOnClose) {
document.body.classList.remove('noScroll');
}
if (isHistoryEnabled(dlg)) {
const state = history.location.state || {};
const dialogs = state.dialogs || [];
// Add new dialog to the list of open dialogs
dialogs.push(hash);
if (state.dialogs?.length > 0) {
if (state.dialogs[state.dialogs.length - 1] === hash) {
unlisten = history.listen(finishClose);
history.back();
} else if (state.dialogs.includes(hash)) {
console.warn('[dialogHelper] dialog "%s" was closed, but is not the last dialog opened', hash);
history.push(
`${history.location.pathname}${history.location.search}`,
{
...state,
dialogs
unlisten = history.listen(finishClose);
// Remove the closed dialog hash from the history state
history.replace(
`${history.location.pathname}${history.location.search}`,
{
...state,
dialogs: state.dialogs.filter(dialog => dialog !== hash)
}
);
}
);
unlisten = history.listen(onHashChange);
} else {
inputManager.on(dlg, onBackCommand);
}
}
function addBackdropOverlay(dlg) {
const backdrop = document.createElement('div');
backdrop.classList.add('dialogBackdrop');
const backdropParent = dlg.dialogContainer || dlg;
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
dlg.backdrop = backdrop;
// trigger reflow or the backdrop will not animate
void backdrop.offsetWidth;
backdrop.classList.add('dialogBackdropOpened');
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
if (e.target === dlg.dialogContainer) {
close(dlg);
}
}, {
passive: true
});
}
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
if (e.target === dlg.dialogContainer) {
// Close the application dialog menu
close(dlg);
// Prevent the default browser context menu from appearing
e.preventDefault();
if (layoutManager.tv) {
focusManager.focus(activeElement);
}
if (toBoolean(dlg.getAttribute('data-removeonclose'), true)) {
removeCenterFocus(dlg);
const dialogContainer = dlg.dialogContainer;
if (dialogContainer) {
tryRemoveElement(dialogContainer);
dlg.dialogContainer = null;
} else {
tryRemoveElement(dlg);
}
});
}
function isHistoryEnabled(dlg) {
return dlg.getAttribute('data-history') === 'true';
}
export function open(dlg) {
if (globalOnOpenCallback) {
globalOnOpenCallback(dlg);
}
const parent = dlg.parentNode;
if (parent) {
parent.removeChild(dlg);
if (!unlisten) {
finishClose();
}
const dialogContainer = document.createElement('div');
dialogContainer.classList.add('dialogContainer');
dialogContainer.appendChild(dlg);
dlg.dialogContainer = dialogContainer;
document.body.appendChild(dialogContainer);
return new Promise((resolve) => {
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
});
}
function isOpened(dlg) {
//return dlg.opened;
return !dlg.classList.contains('hide');
dlg.addEventListener('_close', onDialogClosed);
const center = !dlg.classList.contains('dialog-fixedSize');
if (center) {
dlg.classList.add('centeredDialog');
}
export function close(dlg) {
if (!dlg.classList.contains('hide')) {
dlg.dispatchEvent(new CustomEvent('closing', {
dlg.classList.remove('hide');
addBackdropOverlay(dlg);
dlg.classList.add('opened');
dlg.dispatchEvent(new CustomEvent('open', {
bubbles: false,
cancelable: false
}));
if (dlg.getAttribute('data-lockscroll') === 'true' && !document.body.classList.contains('noScroll')) {
document.body.classList.add('noScroll');
removeScrollLockOnClose = true;
}
animateDialogOpen(dlg);
if (isHistoryEnabled(dlg)) {
const state = history.location.state || {};
const dialogs = state.dialogs || [];
// Add new dialog to the list of open dialogs
dialogs.push(hash);
history.push(
`${history.location.pathname}${history.location.search}`,
{
...state,
dialogs
}
);
unlisten = history.listen(onHashChange);
} else {
inputManager.on(dlg, onBackCommand);
}
}
function addBackdropOverlay(dlg) {
const backdrop = document.createElement('div');
backdrop.classList.add('dialogBackdrop');
const backdropParent = dlg.dialogContainer || dlg;
backdropParent.parentNode.insertBefore(backdrop, backdropParent);
dlg.backdrop = backdrop;
// trigger reflow or the backdrop will not animate
void backdrop.offsetWidth;
backdrop.classList.add('dialogBackdropOpened');
dom.addEventListener((dlg.dialogContainer || backdrop), 'click', e => {
if (e.target === dlg.dialogContainer) {
close(dlg);
}
}, {
passive: true
});
dom.addEventListener((dlg.dialogContainer || backdrop), 'contextmenu', e => {
if (e.target === dlg.dialogContainer) {
// Close the application dialog menu
close(dlg);
// Prevent the default browser context menu from appearing
e.preventDefault();
}
});
}
function isHistoryEnabled(dlg) {
return dlg.getAttribute('data-history') === 'true';
}
export function open(dlg) {
if (globalOnOpenCallback) {
globalOnOpenCallback(dlg);
}
const parent = dlg.parentNode;
if (parent) {
parent.removeChild(dlg);
}
const dialogContainer = document.createElement('div');
dialogContainer.classList.add('dialogContainer');
dialogContainer.appendChild(dlg);
dlg.dialogContainer = dialogContainer;
document.body.appendChild(dialogContainer);
return new Promise((resolve) => {
new DialogHashHandler(dlg, `dlg${new Date().getTime()}`, resolve);
});
}
function isOpened(dlg) {
return !dlg.classList.contains('hide');
}
export function close(dlg) {
if (!dlg.classList.contains('hide')) {
dlg.dispatchEvent(new CustomEvent('closing', {
bubbles: false,
cancelable: false
}));
const onAnimationFinish = () => {
focusManager.popScope(dlg);
dlg.classList.add('hide');
dlg.dispatchEvent(new CustomEvent('_close', {
bubbles: false,
cancelable: false
}));
};
const onAnimationFinish = () => {
focusManager.popScope(dlg);
dlg.classList.add('hide');
dlg.dispatchEvent(new CustomEvent('_close', {
bubbles: false,
cancelable: false
}));
};
animateDialogClose(dlg, onAnimationFinish);
}
animateDialogClose(dlg, onAnimationFinish);
}
}
const getAnimationEndHandler = (dlg, callback) => function handler() {
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
callback();
const getAnimationEndHandler = (dlg, callback) => function handler() {
dom.removeEventListener(dlg, dom.whichAnimationEvent(), handler, { once: true });
callback();
};
function animateDialogOpen(dlg) {
const onAnimationFinish = () => {
focusManager.pushScope(dlg);
if (dlg.getAttribute('data-autofocus') === 'true') {
focusManager.autoFocus(dlg);
}
if (document.activeElement && !dlg.contains(document.activeElement)) {
// Blur foreign element to prevent triggering of an action from the previous scope
document.activeElement.blur();
}
};
function animateDialogOpen(dlg) {
const onAnimationFinish = () => {
focusManager.pushScope(dlg);
if (enableAnimation()) {
dom.addEventListener(
dlg,
dom.whichAnimationEvent(),
getAnimationEndHandler(dlg, onAnimationFinish),
{ once: true });
if (dlg.getAttribute('data-autofocus') === 'true') {
focusManager.autoFocus(dlg);
}
return;
}
if (document.activeElement && !dlg.contains(document.activeElement)) {
// Blur foreign element to prevent triggering of an action from the previous scope
document.activeElement.blur();
}
};
onAnimationFinish();
}
if (enableAnimation()) {
dom.addEventListener(
dlg,
dom.whichAnimationEvent(),
getAnimationEndHandler(dlg, onAnimationFinish),
{ once: true });
function animateDialogClose(dlg, onAnimationFinish) {
if (enableAnimation()) {
let animated = true;
switch (dlg.animationConfig.exit.name) {
case 'fadeout':
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
case 'scaledown':
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
case 'slidedown':
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
default:
animated = false;
break;
}
dom.addEventListener(
dlg,
dom.whichAnimationEvent(),
getAnimationEndHandler(dlg, onAnimationFinish),
{ once: true });
if (animated) {
return;
}
onAnimationFinish();
}
function animateDialogClose(dlg, onAnimationFinish) {
if (enableAnimation()) {
let animated = true;
onAnimationFinish();
}
switch (dlg.animationConfig.exit.name) {
case 'fadeout':
dlg.style.animation = `fadeout ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
case 'scaledown':
dlg.style.animation = `scaledown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
case 'slidedown':
dlg.style.animation = `slidedown ${dlg.animationConfig.exit.timing.duration}ms ease-out normal both`;
break;
default:
animated = false;
break;
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
function shouldLockDocumentScroll(options) {
if (options.lockScroll != null) {
return options.lockScroll;
}
if (options.size === 'fullscreen') {
return true;
}
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
return false;
}
if (options.size) {
return true;
}
return browser.touch;
}
function removeBackdrop(dlg) {
const backdrop = dlg.backdrop;
if (!backdrop) {
return;
}
dlg.backdrop = null;
const onAnimationFinish = () => {
tryRemoveElement(backdrop);
};
if (enableAnimation()) {
backdrop.classList.remove('dialogBackdropOpened');
// this is not firing animationend
setTimeout(onAnimationFinish, 300);
return;
}
onAnimationFinish();
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
}
export function createDialog(options = {}) {
// If there's no native dialog support, use a plain div
// Also not working well in samsung tizen browser, content inside not clickable
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
const dlg = document.createElement('div');
// Add an id so we can access the dialog element
if (options.id) {
dlg.id = options.id;
}
dlg.classList.add('focuscontainer');
dlg.classList.add('hide');
if (shouldLockDocumentScroll(options)) {
dlg.setAttribute('data-lockscroll', 'true');
}
if (options.enableHistory !== false) {
dlg.setAttribute('data-history', 'true');
}
// without this safari will scroll the background instead of the dialog contents
// but not needed here since this is already on top of an existing dialog
// but skip it in IE because it's causing the entire browser to hang
// Also have to disable for firefox because it's causing select elements to not be clickable
if (options.modal !== false) {
dlg.setAttribute('modal', 'modal');
}
if (options.autoFocus !== false) {
dlg.setAttribute('data-autofocus', 'true');
}
const defaultEntryAnimation = 'scaleup';
const defaultExitAnimation = 'scaledown';
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
const exitAnimation = options.exitAnimation || defaultExitAnimation;
// If it's not fullscreen then lower the default animation speed to make it open really fast
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
dlg.animationConfig = {
// scale up
'entry': {
name: entryAnimation,
timing: {
duration: entryAnimationDuration,
easing: 'ease-out'
}
dom.addEventListener(
dlg,
dom.whichAnimationEvent(),
getAnimationEndHandler(dlg, onAnimationFinish),
{ once: true });
if (animated) {
return;
},
// fade out
'exit': {
name: exitAnimation,
timing: {
duration: exitAnimationDuration,
easing: 'ease-out',
fill: 'both'
}
}
};
onAnimationFinish();
dlg.classList.add('dialog');
if (options.scrollX) {
dlg.classList.add('scrollX');
dlg.classList.add('smoothScrollX');
if (layoutManager.tv) {
centerFocus(dlg, true, true);
}
} else if (options.scrollY !== false) {
dlg.classList.add('smoothScrollY');
if (layoutManager.tv) {
centerFocus(dlg, false, true);
}
}
const supportsOverscrollBehavior = 'overscroll-behavior-y' in document.body.style;
function shouldLockDocumentScroll(options) {
if (options.lockScroll != null) {
return options.lockScroll;
}
if (options.size === 'fullscreen') {
return true;
}
if (supportsOverscrollBehavior && (options.size || !browser.touch)) {
return false;
}
if (options.size) {
return true;
}
return browser.touch;
if (options.removeOnClose) {
dlg.setAttribute('data-removeonclose', 'true');
}
function removeBackdrop(dlg) {
const backdrop = dlg.backdrop;
if (!backdrop) {
return;
}
dlg.backdrop = null;
const onAnimationFinish = () => {
tryRemoveElement(backdrop);
};
if (enableAnimation()) {
backdrop.classList.remove('dialogBackdropOpened');
// this is not firing animationend
setTimeout(onAnimationFinish, 300);
return;
}
onAnimationFinish();
if (options.size) {
dlg.classList.add('dialog-fixedSize');
dlg.classList.add(`dialog-${options.size}`);
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
if (enableAnimation()) {
switch (dlg.animationConfig.entry.name) {
case 'fadein':
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
break;
case 'scaleup':
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
break;
case 'slideup':
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
break;
case 'slidedown':
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
break;
default:
break;
}
}
export function createDialog(options = {}) {
// If there's no native dialog support, use a plain div
// Also not working well in samsung tizen browser, content inside not clickable
// Just go ahead and always use a plain div because we're seeing issues overlaying absoltutely positioned content over a modal dialog
const dlg = document.createElement('div');
return dlg;
}
// Add an id so we can access the dialog element
if (options.id) {
dlg.id = options.id;
}
dlg.classList.add('focuscontainer');
dlg.classList.add('hide');
if (shouldLockDocumentScroll(options)) {
dlg.setAttribute('data-lockscroll', 'true');
}
if (options.enableHistory !== false) {
dlg.setAttribute('data-history', 'true');
}
// without this safari will scroll the background instead of the dialog contents
// but not needed here since this is already on top of an existing dialog
// but skip it in IE because it's causing the entire browser to hang
// Also have to disable for firefox because it's causing select elements to not be clickable
if (options.modal !== false) {
dlg.setAttribute('modal', 'modal');
}
if (options.autoFocus !== false) {
dlg.setAttribute('data-autofocus', 'true');
}
const defaultEntryAnimation = 'scaleup';
const defaultExitAnimation = 'scaledown';
const entryAnimation = options.entryAnimation || defaultEntryAnimation;
const exitAnimation = options.exitAnimation || defaultExitAnimation;
// If it's not fullscreen then lower the default animation speed to make it open really fast
const entryAnimationDuration = options.entryAnimationDuration || (options.size !== 'fullscreen' ? 180 : 280);
const exitAnimationDuration = options.exitAnimationDuration || (options.size !== 'fullscreen' ? 120 : 220);
dlg.animationConfig = {
// scale up
'entry': {
name: entryAnimation,
timing: {
duration: entryAnimationDuration,
easing: 'ease-out'
}
},
// fade out
'exit': {
name: exitAnimation,
timing: {
duration: exitAnimationDuration,
easing: 'ease-out',
fill: 'both'
}
}
};
dlg.classList.add('dialog');
if (options.scrollX) {
dlg.classList.add('scrollX');
dlg.classList.add('smoothScrollX');
if (layoutManager.tv) {
centerFocus(dlg, true, true);
}
} else if (options.scrollY !== false) {
dlg.classList.add('smoothScrollY');
if (layoutManager.tv) {
centerFocus(dlg, false, true);
}
}
if (options.removeOnClose) {
dlg.setAttribute('data-removeonclose', 'true');
}
if (options.size) {
dlg.classList.add('dialog-fixedSize');
dlg.classList.add(`dialog-${options.size}`);
}
if (enableAnimation()) {
switch (dlg.animationConfig.entry.name) {
case 'fadein':
dlg.style.animation = `fadein ${entryAnimationDuration}ms ease-out normal`;
break;
case 'scaleup':
dlg.style.animation = `scaleup ${entryAnimationDuration}ms ease-out normal both`;
break;
case 'slideup':
dlg.style.animation = `slideup ${entryAnimationDuration}ms ease-out normal`;
break;
case 'slidedown':
dlg.style.animation = `slidedown ${entryAnimationDuration}ms ease-out normal`;
break;
default:
break;
}
}
return dlg;
}
export function setOnOpen(val) {
globalOnOpenCallback = val;
}
/* eslint-enable indent */
export function setOnOpen(val) {
globalOnOpenCallback = val;
}
export default {
open: open,

View file

@ -18,238 +18,235 @@ import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
import template from './displaySettings.template.html';
/* eslint-disable indent */
function fillThemes(select, selectedTheme) {
skinManager.getThemes().then(themes => {
select.innerHTML = themes.map(t => {
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
}).join('');
// get default theme
const defaultTheme = themes.find(theme => theme.default);
// set the current theme
select.value = selectedTheme || defaultTheme.id;
});
}
function loadScreensavers(context, userSettings) {
const selectScreensaver = context.querySelector('.selectScreensaver');
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
return {
name: plugin.name,
value: plugin.id
};
});
options.unshift({
name: globalize.translate('None'),
value: 'none'
});
selectScreensaver.innerHTML = options.map(o => {
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
function fillThemes(select, selectedTheme) {
skinManager.getThemes().then(themes => {
select.innerHTML = themes.map(t => {
return `<option value="${t.id}">${escapeHtml(t.name)}</option>`;
}).join('');
selectScreensaver.value = userSettings.screensaver();
// get default theme
const defaultTheme = themes.find(theme => theme.default);
if (!selectScreensaver.value) {
// TODO: set the default instead of none
selectScreensaver.value = 'none';
}
// set the current theme
select.value = selectedTheme || defaultTheme.id;
});
}
function loadScreensavers(context, userSettings) {
const selectScreensaver = context.querySelector('.selectScreensaver');
const options = pluginManager.ofType(PluginType.Screensaver).map(plugin => {
return {
name: plugin.name,
value: plugin.id
};
});
options.unshift({
name: globalize.translate('None'),
value: 'none'
});
selectScreensaver.innerHTML = options.map(o => {
return `<option value="${o.value}">${escapeHtml(o.name)}</option>`;
}).join('');
selectScreensaver.value = userSettings.screensaver();
if (!selectScreensaver.value) {
// TODO: set the default instead of none
selectScreensaver.value = 'none';
}
}
function showOrHideMissingEpisodesField(context) {
if (browser.tizen || browser.web0s) {
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
return;
}
function showOrHideMissingEpisodesField(context) {
if (browser.tizen || browser.web0s) {
context.querySelector('.fldDisplayMissingEpisodes').classList.add('hide');
return;
}
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
}
context.querySelector('.fldDisplayMissingEpisodes').classList.remove('hide');
function loadForm(context, user, userSettings) {
if (appHost.supports('displaylanguage')) {
context.querySelector('.languageSection').classList.remove('hide');
} else {
context.querySelector('.languageSection').classList.add('hide');
}
function loadForm(context, user, userSettings) {
if (appHost.supports('displaylanguage')) {
context.querySelector('.languageSection').classList.remove('hide');
} else {
context.querySelector('.languageSection').classList.add('hide');
}
if (appHost.supports('displaymode')) {
context.querySelector('.fldDisplayMode').classList.remove('hide');
} else {
context.querySelector('.fldDisplayMode').classList.add('hide');
}
if (appHost.supports('externallinks')) {
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
} else {
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
}
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
if (appHost.supports('screensaver')) {
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
} else {
context.querySelector('.selectScreensaverContainer').classList.add('hide');
}
if (datetime.supportsLocalization()) {
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
} else {
context.querySelector('.fldDateTimeLocale').classList.add('hide');
}
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
loadScreensavers(context, userSettings);
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
context.querySelector('#selectLanguage').value = userSettings.language() || '';
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
showOrHideMissingEpisodesField(context);
loading.hide();
if (appHost.supports('displaymode')) {
context.querySelector('.fldDisplayMode').classList.remove('hide');
} else {
context.querySelector('.fldDisplayMode').classList.add('hide');
}
function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
if (appHost.supports('displaylanguage')) {
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
}
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
if (user.Id === apiClient.getCurrentUserId()) {
skinManager.setTheme(userSettingsInstance.theme());
}
layoutManager.setLayout(context.querySelector('.selectLayout').value);
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
if (appHost.supports('externallinks')) {
context.querySelector('.learnHowToContributeContainer').classList.remove('hide');
} else {
context.querySelector('.learnHowToContributeContainer').classList.add('hide');
}
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
context.querySelector('.selectDashboardThemeContainer').classList.toggle('hide', !user.Policy.IsAdministrator);
if (appHost.supports('screensaver')) {
context.querySelector('.selectScreensaverContainer').classList.remove('hide');
} else {
context.querySelector('.selectScreensaverContainer').classList.add('hide');
}
if (datetime.supportsLocalization()) {
context.querySelector('.fldDateTimeLocale').classList.remove('hide');
} else {
context.querySelector('.fldDateTimeLocale').classList.add('hide');
}
fillThemes(context.querySelector('#selectTheme'), userSettings.theme());
fillThemes(context.querySelector('#selectDashboardTheme'), userSettings.dashboardTheme());
loadScreensavers(context, userSettings);
context.querySelector('.chkDisplayMissingEpisodes').checked = user.Configuration.DisplayMissingEpisodes || false;
context.querySelector('#chkThemeSong').checked = userSettings.enableThemeSongs();
context.querySelector('#chkThemeVideo').checked = userSettings.enableThemeVideos();
context.querySelector('#chkFadein').checked = userSettings.enableFastFadein();
context.querySelector('#chkBlurhash').checked = userSettings.enableBlurhash();
context.querySelector('#chkBackdrops').checked = userSettings.enableBackdrops();
context.querySelector('#chkDetailsBanner').checked = userSettings.detailsBanner();
context.querySelector('#chkDisableCustomCss').checked = userSettings.disableCustomCss();
context.querySelector('#txtLocalCustomCss').value = userSettings.customCss();
context.querySelector('#selectLanguage').value = userSettings.language() || '';
context.querySelector('.selectDateTimeLocale').value = userSettings.dateTimeLocale() || '';
context.querySelector('#txtLibraryPageSize').value = userSettings.libraryPageSize();
context.querySelector('#txtMaxDaysForNextUp').value = userSettings.maxDaysForNextUp();
context.querySelector('#chkRewatchingNextUp').checked = userSettings.enableRewatchingInNextUp();
context.querySelector('#chkUseEpisodeImagesInNextUp').checked = userSettings.useEpisodeImagesInNextUpAndResume();
context.querySelector('.selectLayout').value = layoutManager.getSavedLayout() || '';
showOrHideMissingEpisodesField(context);
loading.hide();
}
function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.DisplayMissingEpisodes = context.querySelector('.chkDisplayMissingEpisodes').checked;
if (appHost.supports('displaylanguage')) {
userSettingsInstance.language(context.querySelector('#selectLanguage').value);
}
userSettingsInstance.dateTimeLocale(context.querySelector('.selectDateTimeLocale').value);
userSettingsInstance.enableThemeSongs(context.querySelector('#chkThemeSong').checked);
userSettingsInstance.enableThemeVideos(context.querySelector('#chkThemeVideo').checked);
userSettingsInstance.theme(context.querySelector('#selectTheme').value);
userSettingsInstance.dashboardTheme(context.querySelector('#selectDashboardTheme').value);
userSettingsInstance.screensaver(context.querySelector('.selectScreensaver').value);
userSettingsInstance.libraryPageSize(context.querySelector('#txtLibraryPageSize').value);
userSettingsInstance.maxDaysForNextUp(context.querySelector('#txtMaxDaysForNextUp').value);
userSettingsInstance.enableRewatchingInNextUp(context.querySelector('#chkRewatchingNextUp').checked);
userSettingsInstance.useEpisodeImagesInNextUpAndResume(context.querySelector('#chkUseEpisodeImagesInNextUp').checked);
userSettingsInstance.enableFastFadein(context.querySelector('#chkFadein').checked);
userSettingsInstance.enableBlurhash(context.querySelector('#chkBlurhash').checked);
userSettingsInstance.enableBackdrops(context.querySelector('#chkBackdrops').checked);
userSettingsInstance.detailsBanner(context.querySelector('#chkDetailsBanner').checked);
userSettingsInstance.disableCustomCss(context.querySelector('#chkDisableCustomCss').checked);
userSettingsInstance.customCss(context.querySelector('#txtLocalCustomCss').value);
if (user.Id === apiClient.getCurrentUserId()) {
skinManager.setTheme(userSettingsInstance.theme());
}
layoutManager.setLayout(context.querySelector('.selectLayout').value);
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
}
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
loading.show();
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
function embed(options, self) {
options.element.innerHTML = globalize.translateHtml(template, 'core');
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
self.loadData(options.autoFocus);
}
class DisplaySettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData(autoFocus) {
const self = this;
const context = self.options.element;
loading.show();
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
function embed(options, self) {
options.element.innerHTML = globalize.translateHtml(template, 'core');
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
self.loadData(options.autoFocus);
}
class DisplaySettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData(autoFocus) {
const self = this;
const context = self.options.element;
loading.show();
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
return apiClient.getUser(userId).then(user => {
return userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
loadForm(context, user, userSettings);
if (autoFocus) {
focusManager.autoFocus(context);
}
});
return apiClient.getUser(userId).then(user => {
return userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
loadForm(context, user, userSettings);
if (autoFocus) {
focusManager.autoFocus(context);
}
});
}
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
});
}
/* eslint-enable indent */
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
}
export default DisplaySettings;

View file

@ -9,236 +9,232 @@ import { getParameterByName } from '../utils/url.ts';
import '../styles/scrollstyles.scss';
import '../elements/emby-itemscontainer/emby-itemscontainer';
/* eslint-disable indent */
function enableScrollX() {
return !layoutManager.desktop;
}
function enableScrollX() {
return !layoutManager.desktop;
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
function getPosterShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getSections() {
return [{
name: 'Movies',
types: 'Movie',
id: 'favoriteMovies',
shape: getPosterShape(),
showTitle: false,
overlayPlayButton: true
}, {
name: 'Shows',
types: 'Series',
id: 'favoriteShows',
shape: getPosterShape(),
showTitle: false,
overlayPlayButton: true
}, {
name: 'Episodes',
types: 'Episode',
id: 'favoriteEpisode',
shape: getThumbShape(),
preferThumb: false,
showTitle: true,
showParentTitle: true,
overlayPlayButton: true,
overlayText: false,
centerText: true
}, {
name: 'Videos',
types: 'Video,MusicVideo',
id: 'favoriteVideos',
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
overlayPlayButton: true,
overlayText: false,
centerText: true
}, {
name: 'Artists',
types: 'MusicArtist',
id: 'favoriteArtists',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: false,
centerText: true,
overlayPlayButton: true,
coverImage: true
}, {
name: 'Albums',
types: 'MusicAlbum',
id: 'favoriteAlbums',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: true,
centerText: true,
overlayPlayButton: true,
coverImage: true
}, {
name: 'Songs',
types: 'Audio',
id: 'favoriteSongs',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: true,
centerText: true,
overlayMoreButton: true,
action: 'instantmix',
coverImage: true
}];
}
function loadSection(elem, userId, topParentId, section, isSingleSection) {
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'SortName',
SortOrder: 'Ascending',
Filters: 'IsFavorite',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
CollapseBoxSetItems: false,
ExcludeLocationTypes: 'Virtual',
EnableTotalRecordCount: false
};
if (topParentId) {
options.ParentId = topParentId;
}
function getThumbShape() {
return enableScrollX() ? 'overflowBackdrop' : 'backdrop';
}
if (!isSingleSection) {
options.Limit = 6;
function getPosterShape() {
return enableScrollX() ? 'overflowPortrait' : 'portrait';
}
function getSquareShape() {
return enableScrollX() ? 'overflowSquare' : 'square';
}
function getSections() {
return [{
name: 'Movies',
types: 'Movie',
id: 'favoriteMovies',
shape: getPosterShape(),
showTitle: false,
overlayPlayButton: true
}, {
name: 'Shows',
types: 'Series',
id: 'favoriteShows',
shape: getPosterShape(),
showTitle: false,
overlayPlayButton: true
}, {
name: 'Episodes',
types: 'Episode',
id: 'favoriteEpisode',
shape: getThumbShape(),
preferThumb: false,
showTitle: true,
showParentTitle: true,
overlayPlayButton: true,
overlayText: false,
centerText: true
}, {
name: 'Videos',
types: 'Video,MusicVideo',
id: 'favoriteVideos',
shape: getThumbShape(),
preferThumb: true,
showTitle: true,
overlayPlayButton: true,
overlayText: false,
centerText: true
}, {
name: 'Artists',
types: 'MusicArtist',
id: 'favoriteArtists',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: false,
centerText: true,
overlayPlayButton: true,
coverImage: true
}, {
name: 'Albums',
types: 'MusicAlbum',
id: 'favoriteAlbums',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: true,
centerText: true,
overlayPlayButton: true,
coverImage: true
}, {
name: 'Songs',
types: 'Audio',
id: 'favoriteSongs',
shape: getSquareShape(),
preferThumb: false,
showTitle: true,
overlayText: false,
showParentTitle: true,
centerText: true,
overlayMoreButton: true,
action: 'instantmix',
coverImage: true
}];
}
function loadSection(elem, userId, topParentId, section, isSingleSection) {
const screenWidth = dom.getWindowSize().innerWidth;
const options = {
SortBy: 'SortName',
SortOrder: 'Ascending',
Filters: 'IsFavorite',
Recursive: true,
Fields: 'PrimaryImageAspectRatio,BasicSyncInfo',
CollapseBoxSetItems: false,
ExcludeLocationTypes: 'Virtual',
EnableTotalRecordCount: false
};
if (topParentId) {
options.ParentId = topParentId;
if (enableScrollX()) {
options.Limit = 20;
} else if (screenWidth >= 1920) {
options.Limit = 10;
} else if (screenWidth >= 1440) {
options.Limit = 8;
}
}
if (!isSingleSection) {
options.Limit = 6;
let promise;
if (section.types === 'MusicArtist') {
promise = ApiClient.getArtists(userId, options);
} else {
options.IncludeItemTypes = section.types;
promise = ApiClient.getItems(userId, options);
}
return promise.then(function (result) {
let html = '';
if (result.Items.length) {
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate(section.name);
html += '</h2>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
}
html += '</div>';
if (enableScrollX()) {
options.Limit = 20;
} else if (screenWidth >= 1920) {
options.Limit = 10;
} else if (screenWidth >= 1440) {
options.Limit = 8;
}
}
let promise;
if (section.types === 'MusicArtist') {
promise = ApiClient.getArtists(userId, options);
} else {
options.IncludeItemTypes = section.types;
promise = ApiClient.getItems(userId, options);
}
return promise.then(function (result) {
let html = '';
if (result.Items.length) {
html += '<div class="sectionTitleContainer sectionTitleContainer-cards padded-left">';
if (!layoutManager.tv && options.Limit && result.Items.length >= options.Limit) {
html += '<a is="emby-linkbutton" href="' + ('#/list.html?serverId=' + ApiClient.serverId() + '&type=' + section.types + '&IsFavorite=true') + '" class="more button-flat button-flat-mini sectionTitleTextButton">';
html += '<h2 class="sectionTitle sectionTitle-cards">';
html += globalize.translate(section.name);
html += '</h2>';
html += '<span class="material-icons chevron_right" aria-hidden="true"></span>';
html += '</a>';
} else {
html += '<h2 class="sectionTitle sectionTitle-cards">' + globalize.translate(section.name) + '</h2>';
let scrollXClass = 'scrollX hiddenScrollX';
if (layoutManager.tv) {
scrollXClass += ' smoothScrollX';
}
html += '</div>';
if (enableScrollX()) {
let scrollXClass = 'scrollX hiddenScrollX';
if (layoutManager.tv) {
scrollXClass += ' smoothScrollX';
}
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
}
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
cardLayout = false;
html += cardBuilder.getCardsHtml(result.Items, {
preferThumb: section.preferThumb,
shape: section.shape,
centerText: section.centerText && !cardLayout,
overlayText: section.overlayText !== false,
showTitle: section.showTitle,
showParentTitle: section.showParentTitle,
scalable: true,
coverImage: section.coverImage,
overlayPlayButton: section.overlayPlayButton,
overlayMoreButton: section.overlayMoreButton && !cardLayout,
action: section.action,
allowBottomPadding: !enableScrollX(),
cardLayout: cardLayout
});
html += '</div>';
html += '<div is="emby-itemscontainer" class="itemsContainer ' + scrollXClass + ' padded-left padded-right">';
} else {
html += '<div is="emby-itemscontainer" class="itemsContainer vertical-wrap padded-left padded-right">';
}
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
let cardLayout = appHost.preferVisualCards && section.autoCardLayout && section.showTitle;
cardLayout = false;
html += cardBuilder.getCardsHtml(result.Items, {
preferThumb: section.preferThumb,
shape: section.shape,
centerText: section.centerText && !cardLayout,
overlayText: section.overlayText !== false,
showTitle: section.showTitle,
showParentTitle: section.showParentTitle,
scalable: true,
coverImage: section.coverImage,
overlayPlayButton: section.overlayPlayButton,
overlayMoreButton: section.overlayMoreButton && !cardLayout,
action: section.action,
allowBottomPadding: !enableScrollX(),
cardLayout: cardLayout
});
html += '</div>';
}
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
});
}
export function loadSections(page, userId, topParentId, types) {
loading.show();
let sections = getSections();
const sectionid = getParameterByName('sectionid');
if (sectionid) {
sections = sections.filter(function (s) {
return s.id === sectionid;
});
}
export function loadSections(page, userId, topParentId, types) {
loading.show();
let sections = getSections();
const sectionid = getParameterByName('sectionid');
if (types) {
sections = sections.filter(function (s) {
return types.indexOf(s.id) !== -1;
});
}
if (sectionid) {
sections = sections.filter(function (s) {
return s.id === sectionid;
});
}
let elem = page.querySelector('.favoriteSections');
if (types) {
sections = sections.filter(function (s) {
return types.indexOf(s.id) !== -1;
});
}
let elem = page.querySelector('.favoriteSections');
if (!elem.innerHTML) {
let html = '';
for (let i = 0, length = sections.length; i < length; i++) {
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
}
elem.innerHTML = html;
}
const promises = [];
if (!elem.innerHTML) {
let html = '';
for (let i = 0, length = sections.length; i < length; i++) {
const section = sections[i];
elem = page.querySelector('.section' + section.id);
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
html += '<div class="verticalSection section' + sections[i].id + '"></div>';
}
Promise.all(promises).then(function () {
loading.hide();
});
elem.innerHTML = html;
}
const promises = [];
for (let i = 0, length = sections.length; i < length; i++) {
const section = sections[i];
elem = page.querySelector('.section' + section.id);
promises.push(loadSection(elem, userId, topParentId, section, sections.length === 1));
}
Promise.all(promises).then(function () {
loading.hide();
});
}
export default {
render: loadSections
};
/* eslint-enable indent */

View file

@ -1,110 +1,108 @@
/* eslint-disable indent */
export function getFetchPromise(request) {
const headers = request.headers || {};
export function getFetchPromise(request) {
const headers = request.headers || {};
if (request.dataType === 'json') {
headers.accept = 'application/json';
}
const fetchRequest = {
headers: headers,
method: request.type,
credentials: 'same-origin'
};
let contentType = request.contentType;
if (request.data) {
if (typeof request.data === 'string') {
fetchRequest.body = request.data;
} else {
fetchRequest.body = paramsToString(request.data);
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
}
}
if (contentType) {
headers['Content-Type'] = contentType;
}
let url = request.url;
if (request.query) {
const paramString = paramsToString(request.query);
if (paramString) {
url += `?${paramString}`;
}
}
if (!request.timeout) {
return fetch(url, fetchRequest);
}
return fetchWithTimeout(url, fetchRequest, request.timeout);
if (request.dataType === 'json') {
headers.accept = 'application/json';
}
function fetchWithTimeout(url, options, timeoutMs) {
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
const fetchRequest = {
headers: headers,
method: request.type,
credentials: 'same-origin'
};
return new Promise(function (resolve, reject) {
const timeout = setTimeout(reject, timeoutMs);
let contentType = request.contentType;
options = options || {};
options.credentials = 'same-origin';
if (request.data) {
if (typeof request.data === 'string') {
fetchRequest.body = request.data;
} else {
fetchRequest.body = paramsToString(request.data);
fetch(url, options).then(function (response) {
clearTimeout(timeout);
contentType = contentType || 'application/x-www-form-urlencoded; charset=UTF-8';
}
}
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
if (contentType) {
headers['Content-Type'] = contentType;
}
resolve(response);
}, function (error) {
clearTimeout(timeout);
let url = request.url;
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
if (request.query) {
const paramString = paramsToString(request.query);
if (paramString) {
url += `?${paramString}`;
}
}
reject(error);
});
if (!request.timeout) {
return fetch(url, fetchRequest);
}
return fetchWithTimeout(url, fetchRequest, request.timeout);
}
function fetchWithTimeout(url, options, timeoutMs) {
console.debug(`fetchWithTimeout: timeoutMs: ${timeoutMs}, url: ${url}`);
return new Promise(function (resolve, reject) {
const timeout = setTimeout(reject, timeoutMs);
options = options || {};
options.credentials = 'same-origin';
fetch(url, options).then(function (response) {
clearTimeout(timeout);
console.debug(`fetchWithTimeout: succeeded connecting to url: ${url}`);
resolve(response);
}, function (error) {
clearTimeout(timeout);
console.debug(`fetchWithTimeout: timed out connecting to url: ${url}`);
reject(error);
});
}
});
}
/**
/**
* @param params {Record<string, string | number | boolean>}
* @returns {string} Query string
*/
function paramsToString(params) {
return Object.entries(params)
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
function paramsToString(params) {
return Object.entries(params)
.filter(([, v]) => v !== null && v !== undefined && v !== '')
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}
export function ajax(request) {
if (!request) {
throw new Error('Request cannot be null');
}
export function ajax(request) {
if (!request) {
throw new Error('Request cannot be null');
}
request.headers = request.headers || {};
request.headers = request.headers || {};
console.debug(`requesting url: ${request.url}`);
console.debug(`requesting url: ${request.url}`);
return getFetchPromise(request).then(function (response) {
console.debug(`response status: ${response.status}, url: ${request.url}`);
if (response.status < 400) {
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
return response.json();
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
return response.text();
} else {
return response;
}
return getFetchPromise(request).then(function (response) {
console.debug(`response status: ${response.status}, url: ${request.url}`);
if (response.status < 400) {
if (request.dataType === 'json' || request.headers.accept === 'application/json') {
return response.json();
} else if (request.dataType === 'text' || (response.headers.get('Content-Type') || '').toLowerCase().startsWith('text/')) {
return response.text();
} else {
return Promise.reject(response);
return response;
}
}, function (err) {
console.error(`request failed to url: ${request.url}`);
throw err;
});
}
/* eslint-enable indent */
} else {
return Promise.reject(response);
}
}, function (err) {
console.error(`request failed to url: ${request.url}`);
throw err;
});
}

View file

@ -8,417 +8,414 @@ import './style.scss';
import ServerConnections from '../ServerConnections';
import template from './filterdialog.template.html';
/* eslint-disable indent */
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
const elem = context.querySelector(selector);
if (items.length) {
elem.classList.remove('hide');
} else {
elem.classList.add('hide');
}
let html = '';
html += '<div class="checkboxList">';
html += items.map(function (filter) {
let itemHtml = '';
const checkedHtml = isCheckedFn(filter) ? 'checked' : '';
itemHtml += '<label>';
itemHtml += `<input is="emby-checkbox" type="checkbox" ${checkedHtml} data-filter="${filter}" class="${cssClass}"/>`;
itemHtml += `<span>${filter}</span>`;
itemHtml += '</label>';
return itemHtml;
}).join('');
html += '</div>';
elem.querySelector('.filterOptions').innerHTML = html;
function renderOptions(context, selector, cssClass, items, isCheckedFn) {
const elem = context.querySelector(selector);
if (items.length) {
elem.classList.remove('hide');
} else {
elem.classList.add('hide');
}
let html = '';
html += '<div class="checkboxList">';
html += items.map(function (filter) {
let itemHtml = '';
const checkedHtml = isCheckedFn(filter) ? 'checked' : '';
itemHtml += '<label>';
itemHtml += `<input is="emby-checkbox" type="checkbox" ${checkedHtml} data-filter="${filter}" class="${cssClass}"/>`;
itemHtml += `<span>${filter}</span>`;
itemHtml += '</label>';
return itemHtml;
}).join('');
html += '</div>';
elem.querySelector('.filterOptions').innerHTML = html;
}
function renderFilters(context, result, query) {
renderOptions(context, '.genreFilters', 'chkGenreFilter', result.Genres, function (i) {
const delimeter = '|';
return (delimeter + (query.Genres || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.officialRatingFilters', 'chkOfficialRatingFilter', result.OfficialRatings, function (i) {
const delimeter = '|';
return (delimeter + (query.OfficialRatings || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.tagFilters', 'chkTagFilter', result.Tags, function (i) {
const delimeter = '|';
return (delimeter + (query.Tags || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.yearFilters', 'chkYearFilter', result.Years, function (i) {
const delimeter = ',';
return (delimeter + (query.Years || '') + delimeter).includes(delimeter + i + delimeter);
});
}
function renderFilters(context, result, query) {
renderOptions(context, '.genreFilters', 'chkGenreFilter', result.Genres, function (i) {
const delimeter = '|';
return (delimeter + (query.Genres || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.officialRatingFilters', 'chkOfficialRatingFilter', result.OfficialRatings, function (i) {
const delimeter = '|';
return (delimeter + (query.OfficialRatings || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.tagFilters', 'chkTagFilter', result.Tags, function (i) {
const delimeter = '|';
return (delimeter + (query.Tags || '') + delimeter).includes(delimeter + i + delimeter);
});
renderOptions(context, '.yearFilters', 'chkYearFilter', result.Years, function (i) {
const delimeter = ',';
return (delimeter + (query.Years || '') + delimeter).includes(delimeter + i + delimeter);
});
}
function loadDynamicFilters(context, apiClient, userId, itemQuery) {
return apiClient.getJSON(apiClient.getUrl('Items/Filters', {
UserId: userId,
ParentId: itemQuery.ParentId,
IncludeItemTypes: itemQuery.IncludeItemTypes
})).then(function (result) {
renderFilters(context, result, itemQuery);
});
}
function loadDynamicFilters(context, apiClient, userId, itemQuery) {
return apiClient.getJSON(apiClient.getUrl('Items/Filters', {
UserId: userId,
ParentId: itemQuery.ParentId,
IncludeItemTypes: itemQuery.IncludeItemTypes
})).then(function (result) {
renderFilters(context, result, itemQuery);
});
}
/**
/**
* @param context {HTMLDivElement} Dialog
* @param options {any} Options
*/
function updateFilterControls(context, options) {
const query = options.query;
function updateFilterControls(context, options) {
const query = options.query;
if (options.mode === 'livetvchannels') {
context.querySelector('.chkFavorite').checked = query.IsFavorite === true;
if (options.mode === 'livetvchannels') {
context.querySelector('.chkFavorite').checked = query.IsFavorite === true;
} else {
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
const filters = `,${query.Filters || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
}
}
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
const filters = `,${query.VideoTypes || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
}
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
context.querySelector('#chkThemeVideo').checked = query.HasThemeVideo === true;
context.querySelector('#chkSpecialFeature').checked = query.HasSpecialFeature === true;
context.querySelector('#chkSpecialEpisode').checked = query.ParentIndexNumber === 0;
context.querySelector('#chkMissingEpisode').checked = query.IsMissing === true;
context.querySelector('#chkFutureEpisode').checked = query.IsUnaired === true;
for (const elem of context.querySelectorAll('.chkStatus')) {
const filters = `,${query.SeriesStatus || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
}
}
/**
* @param instance {FilterDialog} An instance of FilterDialog
*/
function triggerChange(instance) {
Events.trigger(instance, 'filterchange');
}
function setVisibility(context, options) {
if (options.mode === 'livetvchannels' || options.mode === 'albums' || options.mode === 'artists' || options.mode === 'albumartists' || options.mode === 'songs') {
hideByClass(context, 'videoStandard');
}
if (enableDynamicFilters(options.mode)) {
context.querySelector('.genreFilters').classList.remove('hide');
context.querySelector('.officialRatingFilters').classList.remove('hide');
context.querySelector('.tagFilters').classList.remove('hide');
context.querySelector('.yearFilters').classList.remove('hide');
}
if (options.mode === 'movies' || options.mode === 'episodes') {
context.querySelector('.videoTypeFilters').classList.remove('hide');
}
if (options.mode === 'movies' || options.mode === 'series' || options.mode === 'episodes') {
context.querySelector('.features').classList.remove('hide');
}
if (options.mode === 'series') {
context.querySelector('.seriesStatus').classList.remove('hide');
}
if (options.mode === 'episodes') {
showByClass(context, 'episodeFilter');
}
}
function showByClass(context, className) {
for (const elem of context.querySelectorAll(`.${className}`)) {
elem.classList.remove('hide');
}
}
function hideByClass(context, className) {
for (const elem of context.querySelectorAll(`.${className}`)) {
elem.classList.add('hide');
}
}
function enableDynamicFilters(mode) {
return mode === 'movies' || mode === 'series' || mode === 'albums' || mode === 'albumartists' || mode === 'artists' || mode === 'songs' || mode === 'episodes';
}
class FilterDialog {
constructor(options) {
/**
* @private
*/
this.options = options;
}
/**
* @private
*/
onFavoriteChange(elem) {
const query = this.options.query;
query.StartIndex = 0;
query.IsFavorite = !!elem.checked || null;
triggerChange(this);
}
/**
* @private
*/
onStandardFilterChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.Filters || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
query.StartIndex = 0;
query.Filters = filters;
triggerChange(this);
}
/**
* @private
*/
onVideoTypeFilterChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.VideoTypes || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
query.StartIndex = 0;
query.VideoTypes = filters;
triggerChange(this);
}
/**
* @private
*/
onStatusChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.SeriesStatus || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
query.SeriesStatus = filters;
query.StartIndex = 0;
triggerChange(this);
}
/**
* @param context {HTMLDivElement} The dialog
*/
bindEvents(context) {
const query = this.options.query;
if (this.options.mode === 'livetvchannels') {
for (const elem of context.querySelectorAll('.chkFavorite')) {
elem.addEventListener('change', () => this.onFavoriteChange(elem));
}
} else {
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
const filters = `,${query.Filters || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
elem.addEventListener('change', () => this.onStandardFilterChange(elem));
}
}
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
const filters = `,${query.VideoTypes || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
elem.addEventListener('change', () => this.onVideoTypeFilterChange(elem));
}
context.querySelector('.chk3DFilter').checked = query.Is3D === true;
context.querySelector('.chkHDFilter').checked = query.IsHD === true;
context.querySelector('.chk4KFilter').checked = query.Is4K === true;
context.querySelector('.chkSDFilter').checked = query.IsHD === false;
context.querySelector('#chkSubtitle').checked = query.HasSubtitles === true;
context.querySelector('#chkTrailer').checked = query.HasTrailer === true;
context.querySelector('#chkThemeSong').checked = query.HasThemeSong === true;
context.querySelector('#chkThemeVideo').checked = query.HasThemeVideo === true;
context.querySelector('#chkSpecialFeature').checked = query.HasSpecialFeature === true;
context.querySelector('#chkSpecialEpisode').checked = query.ParentIndexNumber === 0;
context.querySelector('#chkMissingEpisode').checked = query.IsMissing === true;
context.querySelector('#chkFutureEpisode').checked = query.IsUnaired === true;
for (const elem of context.querySelectorAll('.chkStatus')) {
const filters = `,${query.SeriesStatus || ''}`;
const filterName = elem.getAttribute('data-filter');
elem.checked = filters.includes(`,${filterName}`);
}
}
/**
* @param instance {FilterDialog} An instance of FilterDialog
*/
function triggerChange(instance) {
Events.trigger(instance, 'filterchange');
}
function setVisibility(context, options) {
if (options.mode === 'livetvchannels' || options.mode === 'albums' || options.mode === 'artists' || options.mode === 'albumartists' || options.mode === 'songs') {
hideByClass(context, 'videoStandard');
}
if (enableDynamicFilters(options.mode)) {
context.querySelector('.genreFilters').classList.remove('hide');
context.querySelector('.officialRatingFilters').classList.remove('hide');
context.querySelector('.tagFilters').classList.remove('hide');
context.querySelector('.yearFilters').classList.remove('hide');
}
if (options.mode === 'movies' || options.mode === 'episodes') {
context.querySelector('.videoTypeFilters').classList.remove('hide');
}
if (options.mode === 'movies' || options.mode === 'series' || options.mode === 'episodes') {
context.querySelector('.features').classList.remove('hide');
}
if (options.mode === 'series') {
context.querySelector('.seriesStatus').classList.remove('hide');
}
if (options.mode === 'episodes') {
showByClass(context, 'episodeFilter');
}
}
function showByClass(context, className) {
for (const elem of context.querySelectorAll(`.${className}`)) {
elem.classList.remove('hide');
}
}
function hideByClass(context, className) {
for (const elem of context.querySelectorAll(`.${className}`)) {
elem.classList.add('hide');
}
}
function enableDynamicFilters(mode) {
return mode === 'movies' || mode === 'series' || mode === 'albums' || mode === 'albumartists' || mode === 'artists' || mode === 'songs' || mode === 'episodes';
}
class FilterDialog {
constructor(options) {
/**
* @private
*/
this.options = options;
}
/**
* @private
*/
onFavoriteChange(elem) {
const query = this.options.query;
const chk3DFilter = context.querySelector('.chk3DFilter');
chk3DFilter.addEventListener('change', () => {
query.StartIndex = 0;
query.IsFavorite = !!elem.checked || null;
query.Is3D = chk3DFilter.checked ? true : null;
triggerChange(this);
}
/**
* @private
*/
onStandardFilterChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.Filters || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
});
const chk4KFilter = context.querySelector('.chk4KFilter');
chk4KFilter.addEventListener('change', () => {
query.StartIndex = 0;
query.Filters = filters;
query.Is4K = chk4KFilter.checked ? true : null;
triggerChange(this);
}
/**
* @private
*/
onVideoTypeFilterChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.VideoTypes || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
});
const chkHDFilter = context.querySelector('.chkHDFilter');
const chkSDFilter = context.querySelector('.chkSDFilter');
chkHDFilter.addEventListener('change', () => {
query.StartIndex = 0;
query.VideoTypes = filters;
triggerChange(this);
}
/**
* @private
*/
onStatusChange(elem) {
const query = this.options.query;
const filterName = elem.getAttribute('data-filter');
let filters = query.SeriesStatus || '';
filters = (`,${filters}`).replace(`,${filterName}`, '').substring(1);
if (elem.checked) {
filters = filters ? `${filters},${filterName}` : filterName;
}
query.SeriesStatus = filters;
query.StartIndex = 0;
triggerChange(this);
}
/**
* @param context {HTMLDivElement} The dialog
*/
bindEvents(context) {
const query = this.options.query;
if (this.options.mode === 'livetvchannels') {
for (const elem of context.querySelectorAll('.chkFavorite')) {
elem.addEventListener('change', () => this.onFavoriteChange(elem));
}
if (chkHDFilter.checked) {
chkSDFilter.checked = false;
query.IsHD = true;
} else {
for (const elem of context.querySelectorAll('.chkStandardFilter')) {
elem.addEventListener('change', () => this.onStandardFilterChange(elem));
}
query.IsHD = null;
}
for (const elem of context.querySelectorAll('.chkVideoTypeFilter')) {
elem.addEventListener('change', () => this.onVideoTypeFilterChange(elem));
triggerChange(this);
});
chkSDFilter.addEventListener('change', () => {
query.StartIndex = 0;
if (chkSDFilter.checked) {
chkHDFilter.checked = false;
query.IsHD = false;
} else {
query.IsHD = null;
}
const chk3DFilter = context.querySelector('.chk3DFilter');
chk3DFilter.addEventListener('change', () => {
query.StartIndex = 0;
query.Is3D = chk3DFilter.checked ? true : null;
triggerChange(this);
});
const chk4KFilter = context.querySelector('.chk4KFilter');
chk4KFilter.addEventListener('change', () => {
query.StartIndex = 0;
query.Is4K = chk4KFilter.checked ? true : null;
triggerChange(this);
});
const chkHDFilter = context.querySelector('.chkHDFilter');
const chkSDFilter = context.querySelector('.chkSDFilter');
chkHDFilter.addEventListener('change', () => {
query.StartIndex = 0;
if (chkHDFilter.checked) {
chkSDFilter.checked = false;
query.IsHD = true;
} else {
query.IsHD = null;
}
triggerChange(this);
});
chkSDFilter.addEventListener('change', () => {
query.StartIndex = 0;
if (chkSDFilter.checked) {
chkHDFilter.checked = false;
query.IsHD = false;
} else {
query.IsHD = null;
}
triggerChange(this);
});
for (const elem of context.querySelectorAll('.chkStatus')) {
elem.addEventListener('change', () => this.onStatusChange(elem));
}
const chkTrailer = context.querySelector('#chkTrailer');
chkTrailer.addEventListener('change', () => {
query.StartIndex = 0;
query.HasTrailer = chkTrailer.checked ? true : null;
triggerChange(this);
});
const chkThemeSong = context.querySelector('#chkThemeSong');
chkThemeSong.addEventListener('change', () => {
query.StartIndex = 0;
query.HasThemeSong = chkThemeSong.checked ? true : null;
triggerChange(this);
});
const chkSpecialFeature = context.querySelector('#chkSpecialFeature');
chkSpecialFeature.addEventListener('change', () => {
query.StartIndex = 0;
query.HasSpecialFeature = chkSpecialFeature.checked ? true : null;
triggerChange(this);
});
const chkThemeVideo = context.querySelector('#chkThemeVideo');
chkThemeVideo.addEventListener('change', () => {
query.StartIndex = 0;
query.HasThemeVideo = chkThemeVideo.checked ? true : null;
triggerChange(this);
});
const chkMissingEpisode = context.querySelector('#chkMissingEpisode');
chkMissingEpisode.addEventListener('change', () => {
query.StartIndex = 0;
query.IsMissing = !!chkMissingEpisode.checked;
triggerChange(this);
});
const chkSpecialEpisode = context.querySelector('#chkSpecialEpisode');
chkSpecialEpisode.addEventListener('change', () => {
query.StartIndex = 0;
query.ParentIndexNumber = chkSpecialEpisode.checked ? 0 : null;
triggerChange(this);
});
const chkFutureEpisode = context.querySelector('#chkFutureEpisode');
chkFutureEpisode.addEventListener('change', () => {
query.StartIndex = 0;
if (chkFutureEpisode.checked) {
query.IsUnaired = true;
query.IsVirtualUnaired = null;
} else {
query.IsUnaired = null;
query.IsVirtualUnaired = false;
}
triggerChange(this);
});
const chkSubtitle = context.querySelector('#chkSubtitle');
chkSubtitle.addEventListener('change', () => {
query.StartIndex = 0;
query.HasSubtitles = chkSubtitle.checked ? true : null;
triggerChange(this);
});
context.addEventListener('change', (e) => {
const chkGenreFilter = dom.parentWithClass(e.target, 'chkGenreFilter');
if (chkGenreFilter) {
const filterName = chkGenreFilter.getAttribute('data-filter');
let filters = query.Genres || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkGenreFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.Genres = filters;
triggerChange(this);
return;
}
const chkTagFilter = dom.parentWithClass(e.target, 'chkTagFilter');
if (chkTagFilter) {
const filterName = chkTagFilter.getAttribute('data-filter');
let filters = query.Tags || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkTagFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.Tags = filters;
triggerChange(this);
return;
}
const chkYearFilter = dom.parentWithClass(e.target, 'chkYearFilter');
if (chkYearFilter) {
const filterName = chkYearFilter.getAttribute('data-filter');
let filters = query.Years || '';
const delimiter = ',';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkYearFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.Years = filters;
triggerChange(this);
return;
}
const chkOfficialRatingFilter = dom.parentWithClass(e.target, 'chkOfficialRatingFilter');
if (chkOfficialRatingFilter) {
const filterName = chkOfficialRatingFilter.getAttribute('data-filter');
let filters = query.OfficialRatings || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkOfficialRatingFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.OfficialRatings = filters;
triggerChange(this);
}
});
triggerChange(this);
});
for (const elem of context.querySelectorAll('.chkStatus')) {
elem.addEventListener('change', () => this.onStatusChange(elem));
}
show() {
return new Promise((resolve) => {
const dlg = dialogHelper.createDialog({
removeOnClose: true,
modal: false
});
dlg.classList.add('ui-body-a');
dlg.classList.add('background-theme-a');
dlg.classList.add('formDialog');
dlg.classList.add('filterDialog');
dlg.innerHTML = globalize.translateHtml(template);
setVisibility(dlg, this.options);
dialogHelper.open(dlg);
dlg.addEventListener('close', resolve);
updateFilterControls(dlg, this.options);
this.bindEvents(dlg);
if (enableDynamicFilters(this.options.mode)) {
dlg.classList.add('dynamicFilterDialog');
const apiClient = ServerConnections.getApiClient(this.options.serverId);
loadDynamicFilters(dlg, apiClient, apiClient.getCurrentUserId(), this.options.query);
const chkTrailer = context.querySelector('#chkTrailer');
chkTrailer.addEventListener('change', () => {
query.StartIndex = 0;
query.HasTrailer = chkTrailer.checked ? true : null;
triggerChange(this);
});
const chkThemeSong = context.querySelector('#chkThemeSong');
chkThemeSong.addEventListener('change', () => {
query.StartIndex = 0;
query.HasThemeSong = chkThemeSong.checked ? true : null;
triggerChange(this);
});
const chkSpecialFeature = context.querySelector('#chkSpecialFeature');
chkSpecialFeature.addEventListener('change', () => {
query.StartIndex = 0;
query.HasSpecialFeature = chkSpecialFeature.checked ? true : null;
triggerChange(this);
});
const chkThemeVideo = context.querySelector('#chkThemeVideo');
chkThemeVideo.addEventListener('change', () => {
query.StartIndex = 0;
query.HasThemeVideo = chkThemeVideo.checked ? true : null;
triggerChange(this);
});
const chkMissingEpisode = context.querySelector('#chkMissingEpisode');
chkMissingEpisode.addEventListener('change', () => {
query.StartIndex = 0;
query.IsMissing = !!chkMissingEpisode.checked;
triggerChange(this);
});
const chkSpecialEpisode = context.querySelector('#chkSpecialEpisode');
chkSpecialEpisode.addEventListener('change', () => {
query.StartIndex = 0;
query.ParentIndexNumber = chkSpecialEpisode.checked ? 0 : null;
triggerChange(this);
});
const chkFutureEpisode = context.querySelector('#chkFutureEpisode');
chkFutureEpisode.addEventListener('change', () => {
query.StartIndex = 0;
if (chkFutureEpisode.checked) {
query.IsUnaired = true;
query.IsVirtualUnaired = null;
} else {
query.IsUnaired = null;
query.IsVirtualUnaired = false;
}
triggerChange(this);
});
const chkSubtitle = context.querySelector('#chkSubtitle');
chkSubtitle.addEventListener('change', () => {
query.StartIndex = 0;
query.HasSubtitles = chkSubtitle.checked ? true : null;
triggerChange(this);
});
context.addEventListener('change', (e) => {
const chkGenreFilter = dom.parentWithClass(e.target, 'chkGenreFilter');
if (chkGenreFilter) {
const filterName = chkGenreFilter.getAttribute('data-filter');
let filters = query.Genres || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkGenreFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
});
}
query.StartIndex = 0;
query.Genres = filters;
triggerChange(this);
return;
}
const chkTagFilter = dom.parentWithClass(e.target, 'chkTagFilter');
if (chkTagFilter) {
const filterName = chkTagFilter.getAttribute('data-filter');
let filters = query.Tags || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkTagFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.Tags = filters;
triggerChange(this);
return;
}
const chkYearFilter = dom.parentWithClass(e.target, 'chkYearFilter');
if (chkYearFilter) {
const filterName = chkYearFilter.getAttribute('data-filter');
let filters = query.Years || '';
const delimiter = ',';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkYearFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.Years = filters;
triggerChange(this);
return;
}
const chkOfficialRatingFilter = dom.parentWithClass(e.target, 'chkOfficialRatingFilter');
if (chkOfficialRatingFilter) {
const filterName = chkOfficialRatingFilter.getAttribute('data-filter');
let filters = query.OfficialRatings || '';
const delimiter = '|';
filters = (delimiter + filters).replace(delimiter + filterName, '').substring(1);
if (chkOfficialRatingFilter.checked) {
filters = filters ? (filters + delimiter + filterName) : filterName;
}
query.StartIndex = 0;
query.OfficialRatings = filters;
triggerChange(this);
}
});
}
/* eslint-enable indent */
show() {
return new Promise((resolve) => {
const dlg = dialogHelper.createDialog({
removeOnClose: true,
modal: false
});
dlg.classList.add('ui-body-a');
dlg.classList.add('background-theme-a');
dlg.classList.add('formDialog');
dlg.classList.add('filterDialog');
dlg.innerHTML = globalize.translateHtml(template);
setVisibility(dlg, this.options);
dialogHelper.open(dlg);
dlg.addEventListener('close', resolve);
updateFilterControls(dlg, this.options);
this.bindEvents(dlg);
if (enableDynamicFilters(this.options.mode)) {
dlg.classList.add('dynamicFilterDialog');
const apiClient = ServerConnections.getApiClient(this.options.serverId);
loadDynamicFilters(dlg, apiClient, apiClient.getCurrentUserId(), this.options.query);
}
});
}
}
export default FilterDialog;

View file

@ -297,10 +297,8 @@ class FilterMenu {
}
if (submitted) {
//if (!options.onChange) {
saveValues(dlg, options.settings, options.settingsKey, options.setfilters);
return resolve();
//}
}
return resolve();
});

View file

@ -1,470 +1,466 @@
/* eslint-disable indent */
import dom from '../scripts/dom';
import scrollManager from './scrollManager';
const scopes = [];
function pushScope(elem) {
scopes.push(elem);
}
const scopes = [];
function pushScope(elem) {
scopes.push(elem);
}
function popScope() {
if (scopes.length) {
scopes.length -= 1;
function popScope() {
if (scopes.length) {
scopes.length -= 1;
}
}
function autoFocus(view, defaultToFirst, findAutoFocusElement) {
let element;
if (findAutoFocusElement !== false) {
element = view.querySelector('*[autofocus]');
if (element) {
focus(element);
return element;
}
}
function autoFocus(view, defaultToFirst, findAutoFocusElement) {
let element;
if (findAutoFocusElement !== false) {
element = view.querySelector('*[autofocus]');
if (element) {
focus(element);
return element;
}
}
if (defaultToFirst !== false) {
element = getFocusableElements(view, 1, 'noautofocus')[0];
if (defaultToFirst !== false) {
element = getFocusableElements(view, 1, 'noautofocus')[0];
if (element) {
focus(element);
return element;
}
}
return null;
}
function focus(element) {
try {
element.focus({
preventScroll: scrollManager.isEnabled()
});
} catch (err) {
console.error('Error in focusManager.autoFocus: ' + err);
if (element) {
focus(element);
return element;
}
}
const focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
const focusableContainerTagNames = ['BODY', 'DIALOG'];
const focusableQuery = focusableTagNames.map(function (t) {
if (t === 'INPUT') {
t += ':not([type="range"]):not([type="file"])';
}
return t + ':not([tabindex="-1"]):not(:disabled)';
}).join(',') + ',.focusable';
return null;
}
function isFocusable(elem) {
return focusableTagNames.indexOf(elem.tagName) !== -1
function focus(element) {
try {
element.focus({
preventScroll: scrollManager.isEnabled()
});
} catch (err) {
console.error('Error in focusManager.autoFocus: ' + err);
}
}
const focusableTagNames = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A'];
const focusableContainerTagNames = ['BODY', 'DIALOG'];
const focusableQuery = focusableTagNames.map(function (t) {
if (t === 'INPUT') {
t += ':not([type="range"]):not([type="file"])';
}
return t + ':not([tabindex="-1"]):not(:disabled)';
}).join(',') + ',.focusable';
function isFocusable(elem) {
return focusableTagNames.indexOf(elem.tagName) !== -1
|| (elem.classList?.contains('focusable'));
}
function normalizeFocusable(elem, originalElement) {
if (elem) {
const tagName = elem.tagName;
if (!tagName || tagName === 'HTML' || tagName === 'BODY') {
elem = originalElement;
}
}
function normalizeFocusable(elem, originalElement) {
if (elem) {
const tagName = elem.tagName;
if (!tagName || tagName === 'HTML' || tagName === 'BODY') {
elem = originalElement;
}
return elem;
}
function focusableParent(elem) {
const originalElement = elem;
while (!isFocusable(elem)) {
const parent = elem.parentNode;
if (!parent) {
return normalizeFocusable(elem, originalElement);
}
return elem;
elem = parent;
}
function focusableParent(elem) {
const originalElement = elem;
return normalizeFocusable(elem, originalElement);
}
while (!isFocusable(elem)) {
const parent = elem.parentNode;
if (!parent) {
return normalizeFocusable(elem, originalElement);
}
elem = parent;
}
return normalizeFocusable(elem, originalElement);
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusableInternal(elem) {
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
return elem.offsetParent !== null;
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusable(elem) {
if (elem.disabled) {
return false;
}
if (elem.getAttribute('tabindex') === '-1') {
return false;
}
if (elem.tagName === 'INPUT') {
const type = elem.type;
if (type === 'range') {
return false;
}
if (type === 'file') {
return false;
}
}
return isCurrentlyFocusableInternal(elem);
}
function getDefaultScope() {
return scopes[0] || document.body;
}
function getFocusableElements(parent, limit, excludeClass) {
const elems = (parent || getDefaultScope()).querySelectorAll(focusableQuery);
const focusableElements = [];
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
if (excludeClass && elem.classList.contains(excludeClass)) {
continue;
}
if (isCurrentlyFocusableInternal(elem)) {
focusableElements.push(elem);
if (limit && focusableElements.length >= limit) {
break;
}
}
}
return focusableElements;
}
function isFocusContainer(elem, direction) {
if (focusableContainerTagNames.indexOf(elem.tagName) !== -1) {
return true;
}
const classList = elem.classList;
if (classList.contains('focuscontainer')) {
return true;
}
if (direction === 0) {
if (classList.contains('focuscontainer-x')) {
return true;
}
if (classList.contains('focuscontainer-left')) {
return true;
}
} else if (direction === 1) {
if (classList.contains('focuscontainer-x')) {
return true;
}
if (classList.contains('focuscontainer-right')) {
return true;
}
} else if (direction === 2) {
if (classList.contains('focuscontainer-y')) {
return true;
}
} else if (direction === 3) {
if (classList.contains('focuscontainer-y')) {
return true;
}
if (classList.contains('focuscontainer-down')) {
return true;
}
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusableInternal(elem) {
// http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom
return elem.offsetParent !== null;
}
// Determines if a focusable element can be focused at a given point in time
function isCurrentlyFocusable(elem) {
if (elem.disabled) {
return false;
}
function getFocusContainer(elem, direction) {
while (!isFocusContainer(elem, direction)) {
elem = elem.parentNode;
if (elem.getAttribute('tabindex') === '-1') {
return false;
}
if (!elem) {
return getDefaultScope();
if (elem.tagName === 'INPUT') {
const type = elem.type;
if (type === 'range') {
return false;
}
if (type === 'file') {
return false;
}
}
return isCurrentlyFocusableInternal(elem);
}
function getDefaultScope() {
return scopes[0] || document.body;
}
function getFocusableElements(parent, limit, excludeClass) {
const elems = (parent || getDefaultScope()).querySelectorAll(focusableQuery);
const focusableElements = [];
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
if (excludeClass && elem.classList.contains(excludeClass)) {
continue;
}
if (isCurrentlyFocusableInternal(elem)) {
focusableElements.push(elem);
if (limit && focusableElements.length >= limit) {
break;
}
}
return elem;
}
function getOffset(elem) {
let box;
return focusableElements;
}
// Support: BlackBerry 5, iOS 3 (original iPhone)
// If we don't have gBCR, just use 0,0 rather than error
if (elem.getBoundingClientRect) {
box = elem.getBoundingClientRect();
} else {
box = {
top: 0,
left: 0,
width: 0,
height: 0
};
}
if (box.right === null) {
// Create a new object because some browsers will throw an error when trying to set data onto the Rect object
const newBox = {
top: box.top,
left: box.left,
width: box.width,
height: box.height
};
box = newBox;
box.right = box.left + box.width;
box.bottom = box.top + box.height;
}
return box;
function isFocusContainer(elem, direction) {
if (focusableContainerTagNames.indexOf(elem.tagName) !== -1) {
return true;
}
function nav(activeElement, direction, container, focusableElements) {
activeElement = activeElement || document.activeElement;
const classList = elem.classList;
if (classList.contains('focuscontainer')) {
return true;
}
if (direction === 0) {
if (classList.contains('focuscontainer-x')) {
return true;
}
if (classList.contains('focuscontainer-left')) {
return true;
}
} else if (direction === 1) {
if (classList.contains('focuscontainer-x')) {
return true;
}
if (classList.contains('focuscontainer-right')) {
return true;
}
} else if (direction === 2) {
if (classList.contains('focuscontainer-y')) {
return true;
}
} else if (direction === 3) {
if (classList.contains('focuscontainer-y')) {
return true;
}
if (classList.contains('focuscontainer-down')) {
return true;
}
}
return false;
}
function getFocusContainer(elem, direction) {
while (!isFocusContainer(elem, direction)) {
elem = elem.parentNode;
if (!elem) {
return getDefaultScope();
}
}
return elem;
}
function getOffset(elem) {
let box;
// Support: BlackBerry 5, iOS 3 (original iPhone)
// If we don't have gBCR, just use 0,0 rather than error
if (elem.getBoundingClientRect) {
box = elem.getBoundingClientRect();
} else {
box = {
top: 0,
left: 0,
width: 0,
height: 0
};
}
if (box.right === null) {
// Create a new object because some browsers will throw an error when trying to set data onto the Rect object
const newBox = {
top: box.top,
left: box.left,
width: box.width,
height: box.height
};
box = newBox;
box.right = box.left + box.width;
box.bottom = box.top + box.height;
}
return box;
}
function nav(activeElement, direction, container, focusableElements) {
activeElement = activeElement || document.activeElement;
if (activeElement) {
activeElement = focusableParent(activeElement);
}
container = container || (activeElement ? getFocusContainer(activeElement, direction) : getDefaultScope());
if (!activeElement) {
autoFocus(container, true, false);
return;
}
const focusableContainer = dom.parentWithClass(activeElement, 'focusable');
const rect = getOffset(activeElement);
// Get elements and work out x/y points
const point1x = parseFloat(rect.left) || 0;
const point1y = parseFloat(rect.top) || 0;
const point2x = parseFloat(point1x + rect.width - 1) || point1x;
const point2y = parseFloat(point1y + rect.height - 1) || point1y;
const sourceMidX = rect.left + (rect.width / 2);
const sourceMidY = rect.top + (rect.height / 2);
const focusable = focusableElements || container.querySelectorAll(focusableQuery);
const maxDistance = Infinity;
let minDistance = maxDistance;
let nearestElement;
for (let i = 0, length = focusable.length; i < length; i++) {
const curr = focusable[i];
if (curr === activeElement) {
continue;
}
// Don't refocus into the same container
if (curr === focusableContainer) {
continue;
}
const elementRect = getOffset(curr);
// not currently visible
if (!elementRect.width && !elementRect.height) {
continue;
}
switch (direction) {
case 0:
// left
if (elementRect.left >= rect.left) {
continue;
}
if (elementRect.right === rect.right) {
continue;
}
break;
case 1:
// right
if (elementRect.right <= rect.right) {
continue;
}
if (elementRect.left === rect.left) {
continue;
}
break;
case 2:
// up
if (elementRect.top >= rect.top) {
continue;
}
if (elementRect.bottom >= rect.bottom) {
continue;
}
break;
case 3:
// down
if (elementRect.bottom <= rect.bottom) {
continue;
}
if (elementRect.top <= rect.top) {
continue;
}
break;
default:
break;
}
const x = elementRect.left;
const y = elementRect.top;
const x2 = x + elementRect.width - 1;
const y2 = y + elementRect.height - 1;
const intersectX = intersects(point1x, point2x, x, x2);
const intersectY = intersects(point1y, point2y, y, y2);
const midX = elementRect.left + (elementRect.width / 2);
const midY = elementRect.top + (elementRect.height / 2);
let distX;
let distY;
switch (direction) {
case 0:
// left
distX = Math.abs(point1x - Math.min(point1x, x2));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 1:
// right
distX = Math.abs(point2x - Math.max(point2x, x));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 2:
// up
distY = Math.abs(point1y - Math.min(point1y, y2));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
case 3:
// down
distY = Math.abs(point2y - Math.max(point2y, y));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
default:
break;
}
const dist = Math.sqrt(distX * distX + distY * distY);
if (dist < minDistance) {
nearestElement = curr;
minDistance = dist;
}
}
if (nearestElement) {
// See if there's a focusable container, and if so, send the focus command to that
if (activeElement) {
activeElement = focusableParent(activeElement);
}
container = container || (activeElement ? getFocusContainer(activeElement, direction) : getDefaultScope());
if (!activeElement) {
autoFocus(container, true, false);
return;
}
const focusableContainer = dom.parentWithClass(activeElement, 'focusable');
const rect = getOffset(activeElement);
// Get elements and work out x/y points
const point1x = parseFloat(rect.left) || 0;
const point1y = parseFloat(rect.top) || 0;
const point2x = parseFloat(point1x + rect.width - 1) || point1x;
const point2y = parseFloat(point1y + rect.height - 1) || point1y;
const sourceMidX = rect.left + (rect.width / 2);
const sourceMidY = rect.top + (rect.height / 2);
const focusable = focusableElements || container.querySelectorAll(focusableQuery);
const maxDistance = Infinity;
let minDistance = maxDistance;
let nearestElement;
for (let i = 0, length = focusable.length; i < length; i++) {
const curr = focusable[i];
if (curr === activeElement) {
continue;
}
// Don't refocus into the same container
if (curr === focusableContainer) {
continue;
}
const elementRect = getOffset(curr);
// not currently visible
if (!elementRect.width && !elementRect.height) {
continue;
}
switch (direction) {
case 0:
// left
if (elementRect.left >= rect.left) {
continue;
}
if (elementRect.right === rect.right) {
continue;
}
break;
case 1:
// right
if (elementRect.right <= rect.right) {
continue;
}
if (elementRect.left === rect.left) {
continue;
}
break;
case 2:
// up
if (elementRect.top >= rect.top) {
continue;
}
if (elementRect.bottom >= rect.bottom) {
continue;
}
break;
case 3:
// down
if (elementRect.bottom <= rect.bottom) {
continue;
}
if (elementRect.top <= rect.top) {
continue;
}
break;
default:
break;
}
const x = elementRect.left;
const y = elementRect.top;
const x2 = x + elementRect.width - 1;
const y2 = y + elementRect.height - 1;
const intersectX = intersects(point1x, point2x, x, x2);
const intersectY = intersects(point1y, point2y, y, y2);
const midX = elementRect.left + (elementRect.width / 2);
const midY = elementRect.top + (elementRect.height / 2);
let distX;
let distY;
switch (direction) {
case 0:
// left
distX = Math.abs(point1x - Math.min(point1x, x2));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 1:
// right
distX = Math.abs(point2x - Math.max(point2x, x));
distY = intersectY ? 0 : Math.abs(sourceMidY - midY);
break;
case 2:
// up
distY = Math.abs(point1y - Math.min(point1y, y2));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
case 3:
// down
distY = Math.abs(point2y - Math.max(point2y, y));
distX = intersectX ? 0 : Math.abs(sourceMidX - midX);
break;
default:
break;
}
const dist = Math.sqrt(distX * distX + distY * distY);
if (dist < minDistance) {
nearestElement = curr;
minDistance = dist;
}
}
if (nearestElement) {
// See if there's a focusable container, and if so, send the focus command to that
if (activeElement) {
const nearestElementFocusableParent = dom.parentWithClass(nearestElement, 'focusable');
if (nearestElementFocusableParent
const nearestElementFocusableParent = dom.parentWithClass(nearestElement, 'focusable');
if (nearestElementFocusableParent
&& nearestElementFocusableParent !== nearestElement
&& focusableContainer !== nearestElementFocusableParent
) {
nearestElement = nearestElementFocusableParent;
}
}
focus(nearestElement);
}
}
function intersectsInternal(a1, a2, b1, b2) {
return (b1 >= a1 && b1 <= a2) || (b2 >= a1 && b2 <= a2);
}
function intersects(a1, a2, b1, b2) {
return intersectsInternal(a1, a2, b1, b2) || intersectsInternal(b1, b2, a1, a2);
}
function sendText(text) {
const elem = document.activeElement;
elem.value = text;
}
function focusFirst(container, focusableSelector) {
const elems = container.querySelectorAll(focusableSelector);
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
if (isCurrentlyFocusableInternal(elem)) {
focus(elem);
break;
) {
nearestElement = nearestElementFocusableParent;
}
}
focus(nearestElement);
}
}
function focusLast(container, focusableSelector) {
const elems = [].slice.call(container.querySelectorAll(focusableSelector), 0).reverse();
function intersectsInternal(a1, a2, b1, b2) {
return (b1 >= a1 && b1 <= a2) || (b2 >= a1 && b2 <= a2);
}
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
function intersects(a1, a2, b1, b2) {
return intersectsInternal(a1, a2, b1, b2) || intersectsInternal(b1, b2, a1, a2);
}
if (isCurrentlyFocusableInternal(elem)) {
focus(elem);
break;
}
function sendText(text) {
const elem = document.activeElement;
elem.value = text;
}
function focusFirst(container, focusableSelector) {
const elems = container.querySelectorAll(focusableSelector);
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
if (isCurrentlyFocusableInternal(elem)) {
focus(elem);
break;
}
}
}
function focusLast(container, focusableSelector) {
const elems = [].slice.call(container.querySelectorAll(focusableSelector), 0).reverse();
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[i];
if (isCurrentlyFocusableInternal(elem)) {
focus(elem);
break;
}
}
}
function moveFocus(sourceElement, container, focusableSelector, offset) {
const elems = container.querySelectorAll(focusableSelector);
const list = [];
let i;
let length;
let elem;
for (i = 0, length = elems.length; i < length; i++) {
elem = elems[i];
if (isCurrentlyFocusableInternal(elem)) {
list.push(elem);
}
}
function moveFocus(sourceElement, container, focusableSelector, offset) {
const elems = container.querySelectorAll(focusableSelector);
const list = [];
let i;
let length;
let elem;
let currentIndex = -1;
for (i = 0, length = elems.length; i < length; i++) {
elem = elems[i];
for (i = 0, length = list.length; i < length; i++) {
elem = list[i];
if (isCurrentlyFocusableInternal(elem)) {
list.push(elem);
}
}
let currentIndex = -1;
for (i = 0, length = list.length; i < length; i++) {
elem = list[i];
if (sourceElement === elem || elem.contains(sourceElement)) {
currentIndex = i;
break;
}
}
if (currentIndex === -1) {
return;
}
let newIndex = currentIndex + offset;
newIndex = Math.max(0, newIndex);
newIndex = Math.min(newIndex, list.length - 1);
const newElem = list[newIndex];
if (newElem) {
focus(newElem);
if (sourceElement === elem || elem.contains(sourceElement)) {
currentIndex = i;
break;
}
}
/* eslint-enable indent */
if (currentIndex === -1) {
return;
}
let newIndex = currentIndex + offset;
newIndex = Math.max(0, newIndex);
newIndex = Math.min(newIndex, list.length - 1);
const newElem = list[newIndex];
if (newElem) {
focus(newElem);
}
}
export default {
autoFocus: autoFocus,

View file

@ -1,47 +1,43 @@
/* eslint-disable indent */
import dom from '../scripts/dom';
import { appRouter } from './appRouter';
import Dashboard from '../utils/dashboard';
import ServerConnections from './ServerConnections';
function onGroupedCardClick(e, card) {
const itemId = card.getAttribute('data-id');
const serverId = card.getAttribute('data-serverid');
const apiClient = ServerConnections.getApiClient(serverId);
const userId = apiClient.getCurrentUserId();
const playedIndicator = card.querySelector('.playedIndicator');
const playedIndicatorHtml = playedIndicator ? playedIndicator.innerHTML : null;
const options = {
Limit: parseInt(playedIndicatorHtml || '10', 10),
Fields: 'PrimaryImageAspectRatio,DateCreated',
ParentId: itemId,
GroupItems: false
};
const actionableParent = dom.parentWithTag(e.target, ['A', 'BUTTON', 'INPUT']);
function onGroupedCardClick(e, card) {
const itemId = card.getAttribute('data-id');
const serverId = card.getAttribute('data-serverid');
const apiClient = ServerConnections.getApiClient(serverId);
const userId = apiClient.getCurrentUserId();
const playedIndicator = card.querySelector('.playedIndicator');
const playedIndicatorHtml = playedIndicator ? playedIndicator.innerHTML : null;
const options = {
Limit: parseInt(playedIndicatorHtml || '10', 10),
Fields: 'PrimaryImageAspectRatio,DateCreated',
ParentId: itemId,
GroupItems: false
};
const actionableParent = dom.parentWithTag(e.target, ['A', 'BUTTON', 'INPUT']);
if (!actionableParent || actionableParent.classList.contains('cardContent')) {
apiClient.getJSON(apiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
if (items.length === 1) {
appRouter.showItem(items[0]);
return;
}
if (!actionableParent || actionableParent.classList.contains('cardContent')) {
apiClient.getJSON(apiClient.getUrl('Users/' + userId + '/Items/Latest', options)).then(function (items) {
if (items.length === 1) {
appRouter.showItem(items[0]);
return;
}
const url = 'details?id=' + itemId + '&serverId=' + serverId;
Dashboard.navigate(url);
});
e.stopPropagation();
e.preventDefault();
return false;
}
const url = 'details?id=' + itemId + '&serverId=' + serverId;
Dashboard.navigate(url);
});
e.stopPropagation();
e.preventDefault();
return false;
}
}
export default function onItemsContainerClick(e) {
const groupedCard = dom.parentWithClass(e.target, 'groupedCard');
export default function onItemsContainerClick(e) {
const groupedCard = dom.parentWithClass(e.target, 'groupedCard');
if (groupedCard) {
onGroupedCardClick(e, groupedCard);
}
if (groupedCard) {
onGroupedCardClick(e, groupedCard);
}
/* eslint-enable indent */
}

View file

@ -14,498 +14,494 @@ import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
import template from './homeScreenSettings.template.html';
/* eslint-disable indent */
const numConfigurableSections = 7;
const numConfigurableSections = 7;
function renderViews(page, user, result) {
let folderHtml = '';
function renderViews(page, user, result) {
let folderHtml = '';
folderHtml += '<div class="checkboxList">';
folderHtml += result.map(i => {
let currentHtml = '';
folderHtml += '<div class="checkboxList">';
folderHtml += result.map(i => {
let currentHtml = '';
const id = `chkGroupFolder${i.Id}`;
const id = `chkGroupFolder${i.Id}`;
const isChecked = user.Configuration.GroupedFolders.includes(i.Id);
const isChecked = user.Configuration.GroupedFolders.includes(i.Id);
const checkedHtml = isChecked ? ' checked="checked"' : '';
const checkedHtml = isChecked ? ' checked="checked"' : '';
currentHtml += '<label>';
currentHtml += `<input type="checkbox" is="emby-checkbox" class="chkGroupFolder" data-folderid="${i.Id}" id="${id}"${checkedHtml}/>`;
currentHtml += `<span>${escapeHtml(i.Name)}</span>`;
currentHtml += '</label>';
currentHtml += '<label>';
currentHtml += `<input type="checkbox" is="emby-checkbox" class="chkGroupFolder" data-folderid="${i.Id}" id="${id}"${checkedHtml}/>`;
currentHtml += `<span>${escapeHtml(i.Name)}</span>`;
currentHtml += '</label>';
return currentHtml;
}).join('');
return currentHtml;
}).join('');
folderHtml += '</div>';
folderHtml += '</div>';
page.querySelector('.folderGroupList').innerHTML = folderHtml;
}
page.querySelector('.folderGroupList').innerHTML = folderHtml;
function getLandingScreenOptions(type) {
const list = [];
if (type === 'movies') {
list.push({
name: globalize.translate('Movies'),
value: 'movies',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('Trailers'),
value: 'trailers'
});
list.push({
name: globalize.translate('Favorites'),
value: 'favorites'
});
list.push({
name: globalize.translate('Collections'),
value: 'collections'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
} else if (type === 'tvshows') {
list.push({
name: globalize.translate('Shows'),
value: 'shows',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('TabUpcoming'),
value: 'upcoming'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
list.push({
name: globalize.translate('TabNetworks'),
value: 'networks'
});
list.push({
name: globalize.translate('Episodes'),
value: 'episodes'
});
} else if (type === 'music') {
list.push({
name: globalize.translate('Albums'),
value: 'albums',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('HeaderAlbumArtists'),
value: 'albumartists'
});
list.push({
name: globalize.translate('Artists'),
value: 'artists'
});
list.push({
name: globalize.translate('Playlists'),
value: 'playlists'
});
list.push({
name: globalize.translate('Songs'),
value: 'songs'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
} else if (type === 'livetv') {
list.push({
name: globalize.translate('Programs'),
value: 'programs',
isDefault: true
});
list.push({
name: globalize.translate('Guide'),
value: 'guide'
});
list.push({
name: globalize.translate('Channels'),
value: 'channels'
});
list.push({
name: globalize.translate('Recordings'),
value: 'recordings'
});
list.push({
name: globalize.translate('Schedule'),
value: 'schedule'
});
list.push({
name: globalize.translate('Series'),
value: 'series'
});
}
function getLandingScreenOptions(type) {
const list = [];
return list;
}
if (type === 'movies') {
list.push({
name: globalize.translate('Movies'),
value: 'movies',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('Trailers'),
value: 'trailers'
});
list.push({
name: globalize.translate('Favorites'),
value: 'favorites'
});
list.push({
name: globalize.translate('Collections'),
value: 'collections'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
} else if (type === 'tvshows') {
list.push({
name: globalize.translate('Shows'),
value: 'shows',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('TabUpcoming'),
value: 'upcoming'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
list.push({
name: globalize.translate('TabNetworks'),
value: 'networks'
});
list.push({
name: globalize.translate('Episodes'),
value: 'episodes'
});
} else if (type === 'music') {
list.push({
name: globalize.translate('Albums'),
value: 'albums',
isDefault: true
});
list.push({
name: globalize.translate('Suggestions'),
value: 'suggestions'
});
list.push({
name: globalize.translate('HeaderAlbumArtists'),
value: 'albumartists'
});
list.push({
name: globalize.translate('Artists'),
value: 'artists'
});
list.push({
name: globalize.translate('Playlists'),
value: 'playlists'
});
list.push({
name: globalize.translate('Songs'),
value: 'songs'
});
list.push({
name: globalize.translate('Genres'),
value: 'genres'
});
} else if (type === 'livetv') {
list.push({
name: globalize.translate('Programs'),
value: 'programs',
isDefault: true
});
list.push({
name: globalize.translate('Guide'),
value: 'guide'
});
list.push({
name: globalize.translate('Channels'),
value: 'channels'
});
list.push({
name: globalize.translate('Recordings'),
value: 'recordings'
});
list.push({
name: globalize.translate('Schedule'),
value: 'schedule'
});
list.push({
name: globalize.translate('Series'),
value: 'series'
});
function getLandingScreenOptionsHtml(type, userValue) {
return getLandingScreenOptions(type).map(o => {
const selected = userValue === o.value || (o.isDefault && !userValue);
const selectedHtml = selected ? ' selected' : '';
const optionValue = o.isDefault ? '' : o.value;
return `<option value="${optionValue}"${selectedHtml}>${escapeHtml(o.name)}</option>`;
}).join('');
}
function renderViewOrder(context, user, result) {
let html = '';
html += result.Items.map((view) => {
let currentHtml = '';
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
currentHtml += '<div class="listItemBody">';
currentHtml += '<div>';
currentHtml += escapeHtml(view.Name);
currentHtml += '</div>';
currentHtml += '</div>';
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
currentHtml += '</div>';
return currentHtml;
}).join('');
context.querySelector('.viewOrderList').innerHTML = html;
}
function updateHomeSectionValues(context, userSettings) {
for (let i = 1; i <= 7; i++) {
const select = context.querySelector(`#selectHomeSection${i}`);
const defaultValue = homeSections.getDefaultSection(i - 1);
const option = select.querySelector(`option[value=${defaultValue}]`) || select.querySelector('option[value=""]');
const userValue = userSettings.get(`homesection${i - 1}`);
option.value = '';
if (userValue === defaultValue || !userValue) {
select.value = '';
} else {
select.value = userValue;
}
return list;
}
function getLandingScreenOptionsHtml(type, userValue) {
return getLandingScreenOptions(type).map(o => {
const selected = userValue === o.value || (o.isDefault && !userValue);
const selectedHtml = selected ? ' selected' : '';
const optionValue = o.isDefault ? '' : o.value;
context.querySelector('.selectTVHomeScreen').value = userSettings.get('tvhome') || '';
}
return `<option value="${optionValue}"${selectedHtml}>${escapeHtml(o.name)}</option>`;
}).join('');
function getPerLibrarySettingsHtml(item, user, userSettings) {
let html = '';
let isChecked;
if (item.Type === 'Channel' || item.CollectionType === 'boxsets' || item.CollectionType === 'playlists') {
isChecked = !(user.Configuration.MyMediaExcludes || []).includes(item.Id);
html += '<div>';
html += '<label>';
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInMyMedia" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
html += `<span>${globalize.translate('DisplayInMyMedia')}</span>`;
html += '</label>';
html += '</div>';
}
function renderViewOrder(context, user, result) {
let html = '';
html += result.Items.map((view) => {
let currentHtml = '';
currentHtml += `<div class="listItem viewItem" data-viewid="${view.Id}">`;
currentHtml += '<span class="material-icons listItemIcon folder_open" aria-hidden="true"></span>';
currentHtml += '<div class="listItemBody">';
currentHtml += '<div>';
currentHtml += escapeHtml(view.Name);
currentHtml += '</div>';
currentHtml += '</div>';
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemUp btnViewItemMove autoSize" title="${globalize.translate('Up')}"><span class="material-icons keyboard_arrow_up" aria-hidden="true"></span></button>`;
currentHtml += `<button type="button" is="paper-icon-button-light" class="btnViewItemDown btnViewItemMove autoSize" title="${globalize.translate('Down')}"><span class="material-icons keyboard_arrow_down" aria-hidden="true"></span></button>`;
currentHtml += '</div>';
return currentHtml;
}).join('');
context.querySelector('.viewOrderList').innerHTML = html;
const excludeFromLatest = ['playlists', 'livetv', 'boxsets', 'channels'];
if (!excludeFromLatest.includes(item.CollectionType || '')) {
isChecked = !user.Configuration.LatestItemsExcludes.includes(item.Id);
html += '<label class="fldIncludeInLatest">';
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInLatest" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
html += `<span>${globalize.translate('DisplayInOtherHomeScreenSections')}</span>`;
html += '</label>';
}
function updateHomeSectionValues(context, userSettings) {
for (let i = 1; i <= 7; i++) {
const select = context.querySelector(`#selectHomeSection${i}`);
const defaultValue = homeSections.getDefaultSection(i - 1);
if (html) {
html = `<div class="checkboxListContainer">${html}</div>`;
}
const option = select.querySelector(`option[value=${defaultValue}]`) || select.querySelector('option[value=""]');
if (item.CollectionType === 'movies' || item.CollectionType === 'tvshows' || item.CollectionType === 'music' || item.CollectionType === 'livetv') {
const idForLanding = item.CollectionType === 'livetv' ? item.CollectionType : item.Id;
html += '<div class="selectContainer">';
html += `<select is="emby-select" class="selectLanding" data-folderid="${idForLanding}" label="${globalize.translate('LabelDefaultScreen')}">`;
const userValue = userSettings.get(`homesection${i - 1}`);
const userValue = userSettings.get(`landing-${idForLanding}`);
option.value = '';
html += getLandingScreenOptionsHtml(item.CollectionType, userValue);
if (userValue === defaultValue || !userValue) {
select.value = '';
html += '</select>';
html += '</div>';
}
if (html) {
let prefix = '';
prefix += '<div class="verticalSection">';
prefix += '<h2 class="sectionTitle">';
prefix += escapeHtml(item.Name);
prefix += '</h2>';
html = prefix + html;
html += '</div>';
}
return html;
}
function renderPerLibrarySettings(context, user, userViews, userSettings) {
const elem = context.querySelector('.perLibrarySettings');
let html = '';
for (let i = 0, length = userViews.length; i < length; i++) {
html += getPerLibrarySettingsHtml(userViews[i], user, userSettings);
}
elem.innerHTML = html;
}
function loadForm(context, user, userSettings, apiClient) {
context.querySelector('.chkHidePlayedFromLatest').checked = user.Configuration.HidePlayedInLatest || false;
updateHomeSectionValues(context, userSettings);
const promise1 = apiClient.getUserViews({ IncludeHidden: true }, user.Id);
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
Promise.all([promise1, promise2]).then(responses => {
renderViewOrder(context, user, responses[0]);
renderPerLibrarySettings(context, user, responses[0].Items, userSettings);
renderViews(context, user, responses[1]);
loading.hide();
});
}
function onSectionOrderListClick(e) {
const target = dom.parentWithClass(e.target, 'btnViewItemMove');
if (target) {
const viewItem = dom.parentWithClass(target, 'viewItem');
if (viewItem) {
if (target.classList.contains('btnViewItemDown')) {
const next = viewItem.nextSibling;
if (next) {
viewItem.parentNode.removeChild(viewItem);
next.parentNode.insertBefore(viewItem, next.nextSibling);
focusManager.focus(e.target);
}
} else {
select.value = userValue;
const prev = viewItem.previousSibling;
if (prev) {
viewItem.parentNode.removeChild(viewItem);
prev.parentNode.insertBefore(viewItem, prev);
focusManager.focus(e.target);
}
}
}
}
}
context.querySelector('.selectTVHomeScreen').value = userSettings.get('tvhome') || '';
function getCheckboxItems(selector, context, isChecked) {
const inputs = context.querySelectorAll(selector);
const list = [];
for (let i = 0, length = inputs.length; i < length; i++) {
if (inputs[i].checked === isChecked) {
list.push(inputs[i]);
}
}
function getPerLibrarySettingsHtml(item, user, userSettings) {
let html = '';
return list;
}
let isChecked;
function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.HidePlayedInLatest = context.querySelector('.chkHidePlayedFromLatest').checked;
if (item.Type === 'Channel' || item.CollectionType === 'boxsets' || item.CollectionType === 'playlists') {
isChecked = !(user.Configuration.MyMediaExcludes || []).includes(item.Id);
html += '<div>';
html += '<label>';
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInMyMedia" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
html += `<span>${globalize.translate('DisplayInMyMedia')}</span>`;
html += '</label>';
html += '</div>';
}
user.Configuration.LatestItemsExcludes = getCheckboxItems('.chkIncludeInLatest', context, false).map(i => {
return i.getAttribute('data-folderid');
});
const excludeFromLatest = ['playlists', 'livetv', 'boxsets', 'channels'];
if (!excludeFromLatest.includes(item.CollectionType || '')) {
isChecked = !user.Configuration.LatestItemsExcludes.includes(item.Id);
html += '<label class="fldIncludeInLatest">';
html += `<input type="checkbox" is="emby-checkbox" class="chkIncludeInLatest" data-folderid="${item.Id}"${isChecked ? ' checked="checked"' : ''}/>`;
html += `<span>${globalize.translate('DisplayInOtherHomeScreenSections')}</span>`;
html += '</label>';
}
user.Configuration.MyMediaExcludes = getCheckboxItems('.chkIncludeInMyMedia', context, false).map(i => {
return i.getAttribute('data-folderid');
});
if (html) {
html = `<div class="checkboxListContainer">${html}</div>`;
}
user.Configuration.GroupedFolders = getCheckboxItems('.chkGroupFolder', context, true).map(i => {
return i.getAttribute('data-folderid');
});
if (item.CollectionType === 'movies' || item.CollectionType === 'tvshows' || item.CollectionType === 'music' || item.CollectionType === 'livetv') {
const idForLanding = item.CollectionType === 'livetv' ? item.CollectionType : item.Id;
html += '<div class="selectContainer">';
html += `<select is="emby-select" class="selectLanding" data-folderid="${idForLanding}" label="${globalize.translate('LabelDefaultScreen')}">`;
const userValue = userSettings.get(`landing-${idForLanding}`);
html += getLandingScreenOptionsHtml(item.CollectionType, userValue);
html += '</select>';
html += '</div>';
}
if (html) {
let prefix = '';
prefix += '<div class="verticalSection">';
prefix += '<h2 class="sectionTitle">';
prefix += escapeHtml(item.Name);
prefix += '</h2>';
html = prefix + html;
html += '</div>';
}
return html;
const viewItems = context.querySelectorAll('.viewItem');
const orderedViews = [];
let i;
let length;
for (i = 0, length = viewItems.length; i < length; i++) {
orderedViews.push(viewItems[i].getAttribute('data-viewid'));
}
function renderPerLibrarySettings(context, user, userViews, userSettings) {
const elem = context.querySelector('.perLibrarySettings');
let html = '';
user.Configuration.OrderedViews = orderedViews;
for (let i = 0, length = userViews.length; i < length; i++) {
html += getPerLibrarySettingsHtml(userViews[i], user, userSettings);
}
userSettingsInstance.set('tvhome', context.querySelector('.selectTVHomeScreen').value);
elem.innerHTML = html;
userSettingsInstance.set('homesection0', context.querySelector('#selectHomeSection1').value);
userSettingsInstance.set('homesection1', context.querySelector('#selectHomeSection2').value);
userSettingsInstance.set('homesection2', context.querySelector('#selectHomeSection3').value);
userSettingsInstance.set('homesection3', context.querySelector('#selectHomeSection4').value);
userSettingsInstance.set('homesection4', context.querySelector('#selectHomeSection5').value);
userSettingsInstance.set('homesection5', context.querySelector('#selectHomeSection6').value);
userSettingsInstance.set('homesection6', context.querySelector('#selectHomeSection7').value);
const selectLandings = context.querySelectorAll('.selectLanding');
for (i = 0, length = selectLandings.length; i < length; i++) {
const selectLanding = selectLandings[i];
userSettingsInstance.set(`landing-${selectLanding.getAttribute('data-folderid')}`, selectLanding.value);
}
function loadForm(context, user, userSettings, apiClient) {
context.querySelector('.chkHidePlayedFromLatest').checked = user.Configuration.HidePlayedInLatest || false;
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
}
updateHomeSectionValues(context, userSettings);
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
loading.show();
const promise1 = apiClient.getUserViews({ IncludeHidden: true }, user.Id);
const promise2 = apiClient.getJSON(apiClient.getUrl(`Users/${user.Id}/GroupingOptions`));
Promise.all([promise1, promise2]).then(responses => {
renderViewOrder(context, user, responses[0]);
renderPerLibrarySettings(context, user, responses[0].Items, userSettings);
renderViews(context, user, responses[1]);
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
function onChange(e) {
const chkIncludeInMyMedia = dom.parentWithClass(e.target, 'chkIncludeInMyMedia');
if (!chkIncludeInMyMedia) {
return;
}
function onSectionOrderListClick(e) {
const target = dom.parentWithClass(e.target, 'btnViewItemMove');
if (target) {
const viewItem = dom.parentWithClass(target, 'viewItem');
if (viewItem) {
if (target.classList.contains('btnViewItemDown')) {
const next = viewItem.nextSibling;
if (next) {
viewItem.parentNode.removeChild(viewItem);
next.parentNode.insertBefore(viewItem, next.nextSibling);
focusManager.focus(e.target);
}
} else {
const prev = viewItem.previousSibling;
if (prev) {
viewItem.parentNode.removeChild(viewItem);
prev.parentNode.insertBefore(viewItem, prev);
focusManager.focus(e.target);
}
}
}
const section = dom.parentWithClass(chkIncludeInMyMedia, 'verticalSection');
const fldIncludeInLatest = section.querySelector('.fldIncludeInLatest');
if (fldIncludeInLatest) {
if (chkIncludeInMyMedia.checked) {
fldIncludeInLatest.classList.remove('hide');
} else {
fldIncludeInLatest.classList.add('hide');
}
}
}
function getCheckboxItems(selector, context, isChecked) {
const inputs = context.querySelectorAll(selector);
const list = [];
for (let i = 0, length = inputs.length; i < length; i++) {
if (inputs[i].checked === isChecked) {
list.push(inputs[i]);
}
}
return list;
function embed(options, self) {
let workingTemplate = template;
for (let i = 1; i <= numConfigurableSections; i++) {
workingTemplate = workingTemplate.replace(`{section${i}label}`, globalize.translate('LabelHomeScreenSectionValue', i));
}
function saveUser(context, user, userSettingsInstance, apiClient) {
user.Configuration.HidePlayedInLatest = context.querySelector('.chkHidePlayedFromLatest').checked;
options.element.innerHTML = globalize.translateHtml(workingTemplate, 'core');
user.Configuration.LatestItemsExcludes = getCheckboxItems('.chkIncludeInLatest', context, false).map(i => {
return i.getAttribute('data-folderid');
});
options.element.querySelector('.viewOrderList').addEventListener('click', onSectionOrderListClick);
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
options.element.addEventListener('change', onChange);
user.Configuration.MyMediaExcludes = getCheckboxItems('.chkIncludeInMyMedia', context, false).map(i => {
return i.getAttribute('data-folderid');
});
user.Configuration.GroupedFolders = getCheckboxItems('.chkGroupFolder', context, true).map(i => {
return i.getAttribute('data-folderid');
});
const viewItems = context.querySelectorAll('.viewItem');
const orderedViews = [];
let i;
let length;
for (i = 0, length = viewItems.length; i < length; i++) {
orderedViews.push(viewItems[i].getAttribute('data-viewid'));
}
user.Configuration.OrderedViews = orderedViews;
userSettingsInstance.set('tvhome', context.querySelector('.selectTVHomeScreen').value);
userSettingsInstance.set('homesection0', context.querySelector('#selectHomeSection1').value);
userSettingsInstance.set('homesection1', context.querySelector('#selectHomeSection2').value);
userSettingsInstance.set('homesection2', context.querySelector('#selectHomeSection3').value);
userSettingsInstance.set('homesection3', context.querySelector('#selectHomeSection4').value);
userSettingsInstance.set('homesection4', context.querySelector('#selectHomeSection5').value);
userSettingsInstance.set('homesection5', context.querySelector('#selectHomeSection6').value);
userSettingsInstance.set('homesection6', context.querySelector('#selectHomeSection7').value);
const selectLandings = context.querySelectorAll('.selectLanding');
for (i = 0, length = selectLandings.length; i < length; i++) {
const selectLanding = selectLandings[i];
userSettingsInstance.set(`landing-${selectLanding.getAttribute('data-folderid')}`, selectLanding.value);
}
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
if (layoutManager.tv) {
options.element.querySelector('.selectTVHomeScreenContainer').classList.remove('hide');
} else {
options.element.querySelector('.selectTVHomeScreenContainer').classList.add('hide');
}
self.loadData(options.autoFocus);
}
class HomeScreenSettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData(autoFocus) {
const self = this;
const context = self.options.element;
loading.show();
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
apiClient.getUser(userId).then(user => {
userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
loadForm(context, user, userSettings, apiClient);
function onChange(e) {
const chkIncludeInMyMedia = dom.parentWithClass(e.target, 'chkIncludeInMyMedia');
if (!chkIncludeInMyMedia) {
return;
}
const section = dom.parentWithClass(chkIncludeInMyMedia, 'verticalSection');
const fldIncludeInLatest = section.querySelector('.fldIncludeInLatest');
if (fldIncludeInLatest) {
if (chkIncludeInMyMedia.checked) {
fldIncludeInLatest.classList.remove('hide');
} else {
fldIncludeInLatest.classList.add('hide');
}
}
}
function embed(options, self) {
let workingTemplate = template;
for (let i = 1; i <= numConfigurableSections; i++) {
workingTemplate = workingTemplate.replace(`{section${i}label}`, globalize.translate('LabelHomeScreenSectionValue', i));
}
options.element.innerHTML = globalize.translateHtml(workingTemplate, 'core');
options.element.querySelector('.viewOrderList').addEventListener('click', onSectionOrderListClick);
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
options.element.addEventListener('change', onChange);
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
if (layoutManager.tv) {
options.element.querySelector('.selectTVHomeScreenContainer').classList.remove('hide');
} else {
options.element.querySelector('.selectTVHomeScreenContainer').classList.add('hide');
}
self.loadData(options.autoFocus);
}
class HomeScreenSettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData(autoFocus) {
const self = this;
const context = self.options.element;
loading.show();
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
apiClient.getUser(userId).then(user => {
userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
loadForm(context, user, userSettings, apiClient);
if (autoFocus) {
focusManager.autoFocus(context);
}
});
if (autoFocus) {
focusManager.autoFocus(context);
}
});
}
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
});
}
/* eslint-enable indent */
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
}
export default HomeScreenSettings;

File diff suppressed because it is too large Load diff

View file

@ -1,395 +1,382 @@
/* eslint-disable indent */
import appSettings from '../scripts/settings/appSettings' ;
import browser from '../scripts/browser';
import Events from '../utils/events.ts';
export function getSavedVolume() {
return appSettings.get('volume') || 1;
export function getSavedVolume() {
return appSettings.get('volume') || 1;
}
export function saveVolume(value) {
if (value) {
appSettings.set('volume', value);
}
}
export function getCrossOriginValue(mediaSource) {
if (mediaSource.IsRemote) {
return null;
}
export function saveVolume(value) {
if (value) {
appSettings.set('volume', value);
}
}
return 'anonymous';
}
export function getCrossOriginValue(mediaSource) {
if (mediaSource.IsRemote) {
return null;
}
function canPlayNativeHls() {
const media = document.createElement('video');
return 'anonymous';
}
function canPlayNativeHls() {
const media = document.createElement('video');
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
return !!(media.canPlayType('application/x-mpegURL').replace(/no/, '')
|| media.canPlayType('application/vnd.apple.mpegURL').replace(/no/, ''));
}
export function enableHlsJsPlayer(runTimeTicks, mediaType) {
if (window.MediaSource == null) {
return false;
}
export function enableHlsJsPlayer(runTimeTicks, mediaType) {
if (window.MediaSource == null) {
return false;
}
// hls.js is only in beta. needs more testing.
if (browser.iOS) {
return false;
}
// The native players on these devices support seeking live streams, no need to use hls.js here
if (browser.tizen || browser.web0s) {
return false;
}
if (canPlayNativeHls()) {
// Having trouble with chrome's native support and transcoded music
if (browser.android && mediaType === 'Audio') {
return true;
}
if (browser.edge && mediaType === 'Video') {
//return true;
}
// simple playback should use the native support
if (runTimeTicks) {
//if (!browser.edge) {
return false;
//}
}
//return false;
}
return true;
// hls.js is only in beta. needs more testing.
if (browser.iOS) {
return false;
}
let recoverDecodingErrorDate;
let recoverSwapAudioCodecDate;
export function handleHlsJsMediaError(instance, reject) {
const hlsPlayer = instance._hlsPlayer;
// The native players on these devices support seeking live streams, no need to use hls.js here
if (browser.tizen || browser.web0s) {
return false;
}
if (!hlsPlayer) {
return;
if (canPlayNativeHls()) {
// Having trouble with chrome's native support and transcoded music
if (browser.android && mediaType === 'Audio') {
return true;
}
let now = Date.now();
if (window.performance && window.performance.now) {
now = performance.now(); // eslint-disable-line compat/compat
// simple playback should use the native support
if (runTimeTicks) {
return false;
}
}
if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) {
recoverDecodingErrorDate = now;
console.debug('try to recover media Error ...');
return true;
}
let recoverDecodingErrorDate;
let recoverSwapAudioCodecDate;
export function handleHlsJsMediaError(instance, reject) {
const hlsPlayer = instance._hlsPlayer;
if (!hlsPlayer) {
return;
}
let now = Date.now();
if (window.performance && window.performance.now) {
now = performance.now(); // eslint-disable-line compat/compat
}
if (!recoverDecodingErrorDate || (now - recoverDecodingErrorDate) > 3000) {
recoverDecodingErrorDate = now;
console.debug('try to recover media Error ...');
hlsPlayer.recoverMediaError();
} else {
if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
recoverSwapAudioCodecDate = now;
console.debug('try to swap Audio Codec and recover media Error ...');
hlsPlayer.swapAudioCodec();
hlsPlayer.recoverMediaError();
} else {
if (!recoverSwapAudioCodecDate || (now - recoverSwapAudioCodecDate) > 3000) {
recoverSwapAudioCodecDate = now;
console.debug('try to swap Audio Codec and recover media Error ...');
hlsPlayer.swapAudioCodec();
hlsPlayer.recoverMediaError();
console.error('cannot recover, last media error recovery failed ...');
if (reject) {
reject();
} else {
console.error('cannot recover, last media error recovery failed ...');
if (reject) {
reject();
} else {
onErrorInternal(instance, 'mediadecodeerror');
}
onErrorInternal(instance, 'mediadecodeerror');
}
}
}
}
export function onErrorInternal(instance, type) {
// Needed for video
if (instance.destroyCustomTrack) {
instance.destroyCustomTrack(instance._mediaElement);
}
Events.trigger(instance, 'error', [
{
type: type
}
]);
export function onErrorInternal(instance, type) {
// Needed for video
if (instance.destroyCustomTrack) {
instance.destroyCustomTrack(instance._mediaElement);
}
export function isValidDuration(duration) {
return duration
Events.trigger(instance, 'error', [
{
type: type
}
]);
}
export function isValidDuration(duration) {
return duration
&& !isNaN(duration)
&& duration !== Number.POSITIVE_INFINITY
&& duration !== Number.NEGATIVE_INFINITY;
}
}
function setCurrentTimeIfNeeded(element, seconds) {
// If it's worth skipping (1 sec or less of a difference)
if (Math.abs((element.currentTime || 0) - seconds) >= 1) {
element.currentTime = seconds;
function setCurrentTimeIfNeeded(element, seconds) {
// If it's worth skipping (1 sec or less of a difference)
if (Math.abs((element.currentTime || 0) - seconds) >= 1) {
element.currentTime = seconds;
}
}
export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) {
const seconds = (ticks || 0) / 10000000;
if (seconds) {
// Appending #t=xxx to the query string doesn't seem to work with HLS
// For plain video files, not all browsers support it either
if (element.duration >= seconds) {
// media is ready, seek immediately
setCurrentTimeIfNeeded(element, seconds);
if (onMediaReady) onMediaReady();
} else {
// update video player position when media is ready to be sought
const events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata'];
const onMediaChange = function(e) {
if (element.currentTime === 0 && element.duration >= seconds) {
// seek only when video position is exactly zero,
// as this is true only if video hasn't started yet or
// user rewound to the very beginning
// (but rewinding cannot happen as the first event with media of non-empty duration)
console.debug(`seeking to ${seconds} on ${e.type} event`);
setCurrentTimeIfNeeded(element, seconds);
events.forEach(name => {
element.removeEventListener(name, onMediaChange);
});
if (onMediaReady) onMediaReady();
}
};
events.forEach(name => {
element.addEventListener(name, onMediaChange);
});
}
}
}
export function seekOnPlaybackStart(instance, element, ticks, onMediaReady) {
const seconds = (ticks || 0) / 10000000;
export function applySrc(elem, src, options) {
if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) {
return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) {
const playlist = new Windows.Media.Playback.MediaPlaybackList();
if (seconds) {
// Appending #t=xxx to the query string doesn't seem to work with HLS
// For plain video files, not all browsers support it either
if (element.duration >= seconds) {
// media is ready, seek immediately
setCurrentTimeIfNeeded(element, seconds);
if (onMediaReady) onMediaReady();
} else {
// update video player position when media is ready to be sought
const events = ['durationchange', 'loadeddata', 'play', 'loadedmetadata'];
const onMediaChange = function(e) {
if (element.currentTime === 0 && element.duration >= seconds) {
// seek only when video position is exactly zero,
// as this is true only if video hasn't started yet or
// user rewound to the very beginning
// (but rewinding cannot happen as the first event with media of non-empty duration)
console.debug(`seeking to ${seconds} on ${e.type} event`);
setCurrentTimeIfNeeded(element, seconds);
events.forEach(name => {
element.removeEventListener(name, onMediaChange);
});
if (onMediaReady) onMediaReady();
}
};
events.forEach(name => {
element.addEventListener(name, onMediaChange);
});
}
}
const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file);
const startTime = (options.playerStartPositionTicks || 0) / 10000;
playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime));
elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true });
return Promise.resolve();
});
} else {
elem.src = src;
}
export function applySrc(elem, src, options) {
if (window.Windows && options.mediaSource && options.mediaSource.IsLocal) {
return Windows.Storage.StorageFile.getFileFromPathAsync(options.url).then(function (file) {
const playlist = new Windows.Media.Playback.MediaPlaybackList();
return Promise.resolve();
}
const source1 = Windows.Media.Core.MediaSource.createFromStorageFile(file);
const startTime = (options.playerStartPositionTicks || 0) / 10000;
playlist.items.append(new Windows.Media.Playback.MediaPlaybackItem(source1, startTime));
elem.src = URL.createObjectURL(playlist, { oneTimeOnly: true });
export function resetSrc(elem) {
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute('src');
}
function onSuccessfulPlay(elem, onErrorFn) {
elem.addEventListener('error', onErrorFn);
}
export function playWithPromise(elem, onErrorFn) {
try {
return elem.play()
.catch((e) => {
const errorName = (e.name || '').toLowerCase();
// safari uses aborterror
if (errorName === 'notallowederror'
|| errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element
return Promise.resolve();
}
return Promise.reject();
})
.then(() => {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
});
} else {
elem.src = src;
}
return Promise.resolve();
} catch (err) {
console.error('error calling video.play: ' + err);
return Promise.reject();
}
}
export function resetSrc(elem) {
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute('src');
}
function onSuccessfulPlay(elem, onErrorFn) {
elem.addEventListener('error', onErrorFn);
}
export function playWithPromise(elem, onErrorFn) {
export function destroyCastPlayer(instance) {
const player = instance._castPlayer;
if (player) {
try {
return elem.play()
.catch((e) => {
const errorName = (e.name || '').toLowerCase();
// safari uses aborterror
if (errorName === 'notallowederror'
|| errorName === 'aborterror') {
// swallow this error because the user can still click the play button on the video element
return Promise.resolve();
}
return Promise.reject();
})
.then(() => {
onSuccessfulPlay(elem, onErrorFn);
return Promise.resolve();
});
player.unload();
} catch (err) {
console.error('error calling video.play: ' + err);
return Promise.reject();
console.error(err);
}
}
export function destroyCastPlayer(instance) {
const player = instance._castPlayer;
if (player) {
try {
player.unload();
} catch (err) {
console.error(err);
instance._castPlayer = null;
}
}
export function destroyHlsPlayer(instance) {
const player = instance._hlsPlayer;
if (player) {
try {
player.destroy();
} catch (err) {
console.error(err);
}
instance._hlsPlayer = null;
}
}
export function destroyFlvPlayer(instance) {
const player = instance._flvPlayer;
if (player) {
try {
player.unload();
player.detachMediaElement();
player.destroy();
} catch (err) {
console.error(err);
}
instance._flvPlayer = null;
}
}
export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) {
hls.on(Hls.Events.MANIFEST_PARSED, function () {
playWithPromise(elem, onErrorFn).then(resolve, function () {
if (reject) {
reject();
reject = null;
}
instance._castPlayer = null;
}
}
export function destroyHlsPlayer(instance) {
const player = instance._hlsPlayer;
if (player) {
try {
player.destroy();
} catch (err) {
console.error(err);
}
instance._hlsPlayer = null;
}
}
export function destroyFlvPlayer(instance) {
const player = instance._flvPlayer;
if (player) {
try {
player.unload();
player.detachMediaElement();
player.destroy();
} catch (err) {
console.error(err);
}
instance._flvPlayer = null;
}
}
export function bindEventsToHlsPlayer(instance, hls, elem, onErrorFn, resolve, reject) {
hls.on(Hls.Events.MANIFEST_PARSED, function () {
playWithPromise(elem, onErrorFn).then(resolve, function () {
if (reject) {
reject();
reject = null;
}
});
});
});
hls.on(Hls.Events.ERROR, function (event, data) {
console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false));
hls.on(Hls.Events.ERROR, function (event, data) {
console.error('HLS Error: Type: ' + data.type + ' Details: ' + (data.details || '') + ' Fatal: ' + (data.fatal || false));
// try to recover network error
if (data.type === Hls.ErrorTypes.NETWORK_ERROR
// try to recover network error
if (data.type === Hls.ErrorTypes.NETWORK_ERROR
&& data.response?.code && data.response.code >= 400
) {
console.debug('hls.js response error code: ' + data.response.code);
) {
console.debug('hls.js response error code: ' + data.response.code);
// Trigger failure differently depending on whether this is prior to start of playback, or after
hls.destroy();
// Trigger failure differently depending on whether this is prior to start of playback, or after
hls.destroy();
if (reject) {
reject('servererror');
reject = null;
} else {
onErrorInternal(instance, 'servererror');
}
return;
if (reject) {
reject('servererror');
reject = null;
} else {
onErrorInternal(instance, 'servererror');
}
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
return;
}
if (data.response && data.response.code === 0) {
// This could be a CORS error related to access control response headers
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
console.debug('hls.js response error code: ' + data.response.code);
if (data.response && data.response.code === 0) {
// This could be a CORS error related to access control response headers
// Trigger failure differently depending on whether this is prior to start of playback, or after
hls.destroy();
console.debug('hls.js response error code: ' + data.response.code);
if (reject) {
reject('network');
reject = null;
} else {
onErrorInternal(instance, 'network');
}
} else {
console.debug('fatal network error encountered, try to recover');
hls.startLoad();
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.debug('fatal media error encountered, try to recover');
handleHlsJsMediaError(instance, reject);
reject = null;
break;
default:
console.debug('Cannot recover from hls error - destroy and trigger error');
// cannot recover
// Trigger failure differently depending on whether this is prior to start of playback, or after
hls.destroy();
if (reject) {
reject();
reject('network');
reject = null;
} else {
onErrorInternal(instance, 'mediadecodeerror');
onErrorInternal(instance, 'network');
}
break;
}
} else {
console.debug('fatal network error encountered, try to recover');
hls.startLoad();
}
break;
case Hls.ErrorTypes.MEDIA_ERROR:
console.debug('fatal media error encountered, try to recover');
handleHlsJsMediaError(instance, reject);
reject = null;
break;
default:
console.debug('Cannot recover from hls error - destroy and trigger error');
// cannot recover
// Trigger failure differently depending on whether this is prior to start of playback, or after
hls.destroy();
if (reject) {
reject();
reject = null;
} else {
onErrorInternal(instance, 'mediadecodeerror');
}
break;
}
}
});
}
export function onEndedInternal(instance, elem, onErrorFn) {
elem.removeEventListener('error', onErrorFn);
resetSrc(elem);
destroyHlsPlayer(instance);
destroyFlvPlayer(instance);
destroyCastPlayer(instance);
const stopInfo = {
src: instance._currentSrc
};
Events.trigger(instance, 'stopped', [stopInfo]);
instance._currentTime = null;
instance._currentSrc = null;
instance._currentPlayOptions = null;
}
export function getBufferedRanges(instance, elem) {
const ranges = [];
const seekable = elem.buffered || [];
let offset;
const currentPlayOptions = instance._currentPlayOptions;
if (currentPlayOptions) {
offset = currentPlayOptions.transcodingOffsetTicks;
}
offset = offset || 0;
for (let i = 0, length = seekable.length; i < length; i++) {
let start = seekable.start(i);
let end = seekable.end(i);
if (!isValidDuration(start)) {
start = 0;
}
if (!isValidDuration(end)) {
end = 0;
continue;
}
ranges.push({
start: (start * 10000000) + offset,
end: (end * 10000000) + offset
});
}
export function onEndedInternal(instance, elem, onErrorFn) {
elem.removeEventListener('error', onErrorFn);
resetSrc(elem);
destroyHlsPlayer(instance);
destroyFlvPlayer(instance);
destroyCastPlayer(instance);
const stopInfo = {
src: instance._currentSrc
};
Events.trigger(instance, 'stopped', [stopInfo]);
instance._currentTime = null;
instance._currentSrc = null;
instance._currentPlayOptions = null;
}
export function getBufferedRanges(instance, elem) {
const ranges = [];
const seekable = elem.buffered || [];
let offset;
const currentPlayOptions = instance._currentPlayOptions;
if (currentPlayOptions) {
offset = currentPlayOptions.transcodingOffsetTicks;
}
offset = offset || 0;
for (let i = 0, length = seekable.length; i < length; i++) {
let start = seekable.start(i);
let end = seekable.end(i);
if (!isValidDuration(start)) {
start = 0;
}
if (!isValidDuration(end)) {
end = 0;
continue;
}
ranges.push({
start: (start * 10000000) + offset,
end: (end * 10000000) + offset
});
}
return ranges;
}
/* eslint-enable indent */
return ranges;
}

View file

@ -15,371 +15,369 @@ import '../cardbuilder/card.scss';
import ServerConnections from '../ServerConnections';
import template from './imageDownloader.template.html';
/* eslint-disable indent */
const enableFocusTransform = !browser.slow && !browser.edge;
const enableFocusTransform = !browser.slow && !browser.edge;
let currentItemId;
let currentItemType;
let currentResolve;
let currentReject;
let hasChanges = false;
let currentItemId;
let currentItemType;
let currentResolve;
let currentReject;
let hasChanges = false;
// These images can be large and we're seeing memory problems in safari
const browsableImagePageSize = browser.slow ? 6 : 30;
// These images can be large and we're seeing memory problems in safari
const browsableImagePageSize = browser.slow ? 6 : 30;
let browsableImageStartIndex = 0;
let browsableImageType = 'Primary';
let selectedProvider;
let browsableParentId;
let browsableImageStartIndex = 0;
let browsableImageType = 'Primary';
let selectedProvider;
let browsableParentId;
function getBaseRemoteOptions(page, forceCurrentItemId = false) {
const options = {};
function getBaseRemoteOptions(page, forceCurrentItemId = false) {
const options = {};
if (!forceCurrentItemId && page.querySelector('#chkShowParentImages').checked && browsableParentId) {
options.itemId = browsableParentId;
} else {
options.itemId = currentItemId;
}
return options;
if (!forceCurrentItemId && page.querySelector('#chkShowParentImages').checked && browsableParentId) {
options.itemId = browsableParentId;
} else {
options.itemId = currentItemId;
}
function reloadBrowsableImages(page, apiClient) {
loading.show();
return options;
}
const options = getBaseRemoteOptions(page);
function reloadBrowsableImages(page, apiClient) {
loading.show();
options.type = browsableImageType;
options.startIndex = browsableImageStartIndex;
options.limit = browsableImagePageSize;
options.IncludeAllLanguages = page.querySelector('#chkAllLanguages').checked;
const options = getBaseRemoteOptions(page);
const provider = selectedProvider || '';
options.type = browsableImageType;
options.startIndex = browsableImageStartIndex;
options.limit = browsableImagePageSize;
options.IncludeAllLanguages = page.querySelector('#chkAllLanguages').checked;
if (provider) {
options.ProviderName = provider;
}
const provider = selectedProvider || '';
apiClient.getAvailableRemoteImages(options).then(function (result) {
renderRemoteImages(page, apiClient, result, browsableImageType, options.startIndex, options.limit);
page.querySelector('#selectBrowsableImageType').value = browsableImageType;
const providersHtml = result.Providers.map(function (p) {
return '<option value="' + p + '">' + p + '</option>';
});
const selectImageProvider = page.querySelector('#selectImageProvider');
selectImageProvider.innerHTML = '<option value="">' + globalize.translate('All') + '</option>' + providersHtml;
selectImageProvider.value = provider;
loading.hide();
});
}
function renderRemoteImages(page, apiClient, imagesResult, imageType, startIndex, limit) {
page.querySelector('.availableImagesPaging').innerHTML = getPagingHtml(startIndex, limit, imagesResult.TotalRecordCount);
let html = '';
for (let i = 0, length = imagesResult.Images.length; i < length; i++) {
html += getRemoteImageHtml(imagesResult.Images[i], imageType);
}
const availableImagesList = page.querySelector('.availableImagesList');
availableImagesList.innerHTML = html;
imageLoader.lazyChildren(availableImagesList);
const btnNextPage = page.querySelector('.btnNextPage');
const btnPreviousPage = page.querySelector('.btnPreviousPage');
if (btnNextPage) {
btnNextPage.addEventListener('click', function () {
browsableImageStartIndex += browsableImagePageSize;
reloadBrowsableImages(page, apiClient);
});
}
if (btnPreviousPage) {
btnPreviousPage.addEventListener('click', function () {
browsableImageStartIndex -= browsableImagePageSize;
reloadBrowsableImages(page, apiClient);
});
}
}
function getPagingHtml(startIndex, limit, totalRecordCount) {
let html = '';
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
// 20 is the minimum page size
const showControls = totalRecordCount > limit;
html += '<div class="listPaging">';
html += '<span style="margin-right: 10px;">';
const startAtDisplay = totalRecordCount ? startIndex + 1 : 0;
html += globalize.translate('ListPaging', startAtDisplay, recordsEnd, totalRecordCount);
html += '</span>';
if (showControls) {
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
html += '</div>';
}
html += '</div>';
return html;
}
function downloadRemoteImage(page, apiClient, url, type, provider) {
const options = getBaseRemoteOptions(page, true);
options.Type = type;
options.ImageUrl = url;
if (provider) {
options.ProviderName = provider;
loading.show();
apiClient.downloadRemoteImage(options).then(function () {
hasChanges = true;
const dlg = dom.parentWithClass(page, 'dialog');
dialogHelper.close(dlg);
});
}
function getRemoteImageHtml(image, imageType) {
const tagName = layoutManager.tv ? 'button' : 'div';
const enableFooterButtons = !layoutManager.tv;
apiClient.getAvailableRemoteImages(options).then(function (result) {
renderRemoteImages(page, apiClient, result, browsableImageType, options.startIndex, options.limit);
// TODO move card creation code to Card component
page.querySelector('#selectBrowsableImageType').value = browsableImageType;
let html = '';
let cssClass = 'card scalableCard imageEditorCard';
const cardBoxCssClass = 'cardBox visualCardBox';
let shape;
if (imageType === 'Backdrop' || imageType === 'Art' || imageType === 'Thumb' || imageType === 'Logo') {
shape = 'backdrop';
} else if (imageType === 'Banner') {
shape = 'banner';
} else if (imageType === 'Disc') {
shape = 'square';
} else {
if (currentItemType === 'Episode') {
shape = 'backdrop';
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
shape = 'square';
} else {
shape = 'portrait';
}
}
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
if (tagName === 'button') {
cssClass += ' btnImageCard';
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
}
html += '<button type="button" class="' + cssClass + '"';
} else {
html += '<div class="' + cssClass + '"';
}
html += ' data-imageprovider="' + image.ProviderName + '" data-imageurl="' + image.Url + '" data-imagetype="' + image.Type + '"';
html += '>';
html += '<div class="' + cardBoxCssClass + '">';
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
html += '<div class="cardPadder-' + shape + '"></div>';
html += '<div class="cardContent">';
if (layoutManager.tv || !appHost.supports('externallinks')) {
html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
} else {
html += '<a is="emby-linkbutton" target="_blank" href="' + image.Url + '" class="button-link cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain"></a>';
}
html += '</div>';
html += '</div>';
// begin footer
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<div class="cardText cardTextCentered">' + image.ProviderName + '</div>';
if (image.Width || image.Height || image.Language) {
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.Width && image.Height) {
html += image.Width + ' x ' + image.Height;
if (image.Language) {
html += ' • ' + image.Language;
}
} else {
if (image.Language) {
html += image.Language;
}
}
html += '</div>';
}
if (image.CommunityRating != null) {
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.RatingType === 'Likes') {
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
} else {
if (image.CommunityRating) {
html += image.CommunityRating.toFixed(1);
if (image.VoteCount) {
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
}
} else {
html += 'Unrated';
}
}
html += '</div>';
}
if (enableFooterButtons) {
html += '<div class="cardText cardTextCentered">';
html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
html += '</div>';
}
html += '</div>';
// end footer
html += '</div>';
html += '</' + tagName + '>';
return html;
}
function reloadBrowsableImagesFirstPage(page, apiClient) {
browsableImageStartIndex = 0;
reloadBrowsableImages(page, apiClient);
}
function initEditor(page, apiClient) {
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
browsableImageType = this.value;
selectedProvider = null;
reloadBrowsableImagesFirstPage(page, apiClient);
const providersHtml = result.Providers.map(function (p) {
return '<option value="' + p + '">' + p + '</option>';
});
page.querySelector('#selectImageProvider').addEventListener('change', function () {
selectedProvider = this.value;
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#chkAllLanguages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#chkShowParentImages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.addEventListener('click', function (e) {
const btnDownloadRemoteImage = dom.parentWithClass(e.target, 'btnDownloadRemoteImage');
if (btnDownloadRemoteImage) {
const card = dom.parentWithClass(btnDownloadRemoteImage, 'card');
downloadRemoteImage(page, apiClient, card.getAttribute('data-imageurl'), card.getAttribute('data-imagetype'), card.getAttribute('data-imageprovider'));
return;
}
const btnImageCard = dom.parentWithClass(e.target, 'btnImageCard');
if (btnImageCard) {
downloadRemoteImage(page, apiClient, btnImageCard.getAttribute('data-imageurl'), btnImageCard.getAttribute('data-imagetype'), btnImageCard.getAttribute('data-imageprovider'));
}
});
}
function showEditor(itemId, serverId, itemType) {
loading.show();
const apiClient = ServerConnections.getApiClient(serverId);
currentItemId = itemId;
currentItemType = itemType;
const dialogOptions = {
removeOnClose: true
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.innerHTML = globalize.translateHtml(template, 'core');
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
if (browsableParentId) {
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
}
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed);
dialogHelper.open(dlg);
const editorContent = dlg.querySelector('.formDialogContent');
initEditor(editorContent, apiClient);
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
reloadBrowsableImages(editorContent, apiClient);
}
function onDialogClosed() {
const dlg = this;
if (layoutManager.tv) {
scrollHelper.centerFocus.off(dlg, false);
}
const selectImageProvider = page.querySelector('#selectImageProvider');
selectImageProvider.innerHTML = '<option value="">' + globalize.translate('All') + '</option>' + providersHtml;
selectImageProvider.value = provider;
loading.hide();
if (hasChanges) {
currentResolve();
});
}
function renderRemoteImages(page, apiClient, imagesResult, imageType, startIndex, limit) {
page.querySelector('.availableImagesPaging').innerHTML = getPagingHtml(startIndex, limit, imagesResult.TotalRecordCount);
let html = '';
for (let i = 0, length = imagesResult.Images.length; i < length; i++) {
html += getRemoteImageHtml(imagesResult.Images[i], imageType);
}
const availableImagesList = page.querySelector('.availableImagesList');
availableImagesList.innerHTML = html;
imageLoader.lazyChildren(availableImagesList);
const btnNextPage = page.querySelector('.btnNextPage');
const btnPreviousPage = page.querySelector('.btnPreviousPage');
if (btnNextPage) {
btnNextPage.addEventListener('click', function () {
browsableImageStartIndex += browsableImagePageSize;
reloadBrowsableImages(page, apiClient);
});
}
if (btnPreviousPage) {
btnPreviousPage.addEventListener('click', function () {
browsableImageStartIndex -= browsableImagePageSize;
reloadBrowsableImages(page, apiClient);
});
}
}
function getPagingHtml(startIndex, limit, totalRecordCount) {
let html = '';
const recordsEnd = Math.min(startIndex + limit, totalRecordCount);
// 20 is the minimum page size
const showControls = totalRecordCount > limit;
html += '<div class="listPaging">';
html += '<span style="margin-right: 10px;">';
const startAtDisplay = totalRecordCount ? startIndex + 1 : 0;
html += globalize.translate('ListPaging', startAtDisplay, recordsEnd, totalRecordCount);
html += '</span>';
if (showControls) {
html += '<div data-role="controlgroup" data-type="horizontal" style="display:inline-block;">';
html += `<button is="paper-icon-button-light" title="${globalize.translate('Previous')}" class="btnPreviousPage autoSize" ${(startIndex ? '' : 'disabled')}><span class="material-icons arrow_back" aria-hidden="true"></span></button>`;
html += `<button is="paper-icon-button-light" title="${globalize.translate('Next')}" class="btnNextPage autoSize" ${(startIndex + limit >= totalRecordCount ? 'disabled' : '')}><span class="material-icons arrow_forward" aria-hidden="true"></span></button>`;
html += '</div>';
}
html += '</div>';
return html;
}
function downloadRemoteImage(page, apiClient, url, type, provider) {
const options = getBaseRemoteOptions(page, true);
options.Type = type;
options.ImageUrl = url;
options.ProviderName = provider;
loading.show();
apiClient.downloadRemoteImage(options).then(function () {
hasChanges = true;
const dlg = dom.parentWithClass(page, 'dialog');
dialogHelper.close(dlg);
});
}
function getRemoteImageHtml(image, imageType) {
const tagName = layoutManager.tv ? 'button' : 'div';
const enableFooterButtons = !layoutManager.tv;
// TODO move card creation code to Card component
let html = '';
let cssClass = 'card scalableCard imageEditorCard';
const cardBoxCssClass = 'cardBox visualCardBox';
let shape;
if (imageType === 'Backdrop' || imageType === 'Art' || imageType === 'Thumb' || imageType === 'Logo') {
shape = 'backdrop';
} else if (imageType === 'Banner') {
shape = 'banner';
} else if (imageType === 'Disc') {
shape = 'square';
} else {
if (currentItemType === 'Episode') {
shape = 'backdrop';
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
shape = 'square';
} else {
currentReject();
shape = 'portrait';
}
}
cssClass += ' ' + shape + 'Card ' + shape + 'Card-scalable';
if (tagName === 'button') {
cssClass += ' btnImageCard';
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
}
html += '<button type="button" class="' + cssClass + '"';
} else {
html += '<div class="' + cssClass + '"';
}
html += ' data-imageprovider="' + image.ProviderName + '" data-imageurl="' + image.Url + '" data-imagetype="' + image.Type + '"';
html += '>';
html += '<div class="' + cardBoxCssClass + '">';
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
html += '<div class="cardPadder-' + shape + '"></div>';
html += '<div class="cardContent">';
if (layoutManager.tv || !appHost.supports('externallinks')) {
html += '<div class="cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain;"></div>';
} else {
html += '<a is="emby-linkbutton" target="_blank" href="' + image.Url + '" class="button-link cardImageContainer lazy" data-src="' + image.Url + '" style="background-position:center center;background-size:contain"></a>';
}
html += '</div>';
html += '</div>';
// begin footer
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<div class="cardText cardTextCentered">' + image.ProviderName + '</div>';
if (image.Width || image.Height || image.Language) {
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.Width && image.Height) {
html += image.Width + ' x ' + image.Height;
if (image.Language) {
html += ' • ' + image.Language;
}
} else {
if (image.Language) {
html += image.Language;
}
}
html += '</div>';
}
if (image.CommunityRating != null) {
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.RatingType === 'Likes') {
html += image.CommunityRating + (image.CommunityRating === 1 ? ' like' : ' likes');
} else {
if (image.CommunityRating) {
html += image.CommunityRating.toFixed(1);
if (image.VoteCount) {
html += ' • ' + image.VoteCount + (image.VoteCount === 1 ? ' vote' : ' votes');
}
} else {
html += 'Unrated';
}
}
html += '</div>';
}
if (enableFooterButtons) {
html += '<div class="cardText cardTextCentered">';
html += `<button is="paper-icon-button-light" class="btnDownloadRemoteImage autoSize" raised" title="${globalize.translate('Download')}"><span class="material-icons cloud_download" aria-hidden="true"></span></button>`;
html += '</div>';
}
html += '</div>';
// end footer
html += '</div>';
html += '</' + tagName + '>';
return html;
}
function reloadBrowsableImagesFirstPage(page, apiClient) {
browsableImageStartIndex = 0;
reloadBrowsableImages(page, apiClient);
}
function initEditor(page, apiClient) {
page.querySelector('#selectBrowsableImageType').addEventListener('change', function () {
browsableImageType = this.value;
selectedProvider = null;
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#selectImageProvider').addEventListener('change', function () {
selectedProvider = this.value;
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#chkAllLanguages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.querySelector('#chkShowParentImages').addEventListener('change', function () {
reloadBrowsableImagesFirstPage(page, apiClient);
});
page.addEventListener('click', function (e) {
const btnDownloadRemoteImage = dom.parentWithClass(e.target, 'btnDownloadRemoteImage');
if (btnDownloadRemoteImage) {
const card = dom.parentWithClass(btnDownloadRemoteImage, 'card');
downloadRemoteImage(page, apiClient, card.getAttribute('data-imageurl'), card.getAttribute('data-imagetype'), card.getAttribute('data-imageprovider'));
return;
}
const btnImageCard = dom.parentWithClass(e.target, 'btnImageCard');
if (btnImageCard) {
downloadRemoteImage(page, apiClient, btnImageCard.getAttribute('data-imageurl'), btnImageCard.getAttribute('data-imagetype'), btnImageCard.getAttribute('data-imageprovider'));
}
});
}
function showEditor(itemId, serverId, itemType) {
loading.show();
const apiClient = ServerConnections.getApiClient(serverId);
currentItemId = itemId;
currentItemType = itemType;
const dialogOptions = {
removeOnClose: true
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.innerHTML = globalize.translateHtml(template, 'core');
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
if (browsableParentId) {
dlg.querySelector('#lblShowParentImages').classList.remove('hide');
}
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed);
dialogHelper.open(dlg);
const editorContent = dlg.querySelector('.formDialogContent');
initEditor(editorContent, apiClient);
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
reloadBrowsableImages(editorContent, apiClient);
}
function onDialogClosed() {
const dlg = this;
if (layoutManager.tv) {
scrollHelper.centerFocus.off(dlg, false);
}
loading.hide();
if (hasChanges) {
currentResolve();
} else {
currentReject();
}
}
export function show(itemId, serverId, itemType, imageType, parentId) {
return new Promise(function (resolve, reject) {
currentResolve = resolve;
@ -397,4 +395,3 @@ export default {
show: show
};
/* eslint-enable indent */

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for image Options Editor.
@ -13,95 +12,95 @@ import '../../elements/emby-select/emby-select';
import '../../elements/emby-input/emby-input';
import template from './imageOptionsEditor.template.html';
function getDefaultImageConfig(itemType, type) {
return {
Type: type,
MinWidth: 0,
Limit: type === 'Primary' ? 1 : 0
};
}
function getDefaultImageConfig(itemType, type) {
return {
Type: type,
MinWidth: 0,
Limit: type === 'Primary' ? 1 : 0
};
}
function findImageOptions(imageOptions, type) {
return imageOptions.filter(i => {
return i.Type == type;
})[0];
}
function findImageOptions(imageOptions, type) {
return imageOptions.filter(i => {
return i.Type == type;
})[0];
}
function getImageConfig(options, availableOptions, imageType, itemType) {
return findImageOptions(options.ImageOptions || [], imageType) || findImageOptions(availableOptions.DefaultImageOptions || [], imageType) || getDefaultImageConfig(itemType, imageType);
}
function getImageConfig(options, availableOptions, imageType, itemType) {
return findImageOptions(options.ImageOptions || [], imageType) || findImageOptions(availableOptions.DefaultImageOptions || [], imageType) || getDefaultImageConfig(itemType, imageType);
}
function setVisibilityOfBackdrops(elem, visible) {
if (visible) {
elem.classList.remove('hide');
elem.querySelector('input').setAttribute('required', 'required');
function setVisibilityOfBackdrops(elem, visible) {
if (visible) {
elem.classList.remove('hide');
elem.querySelector('input').setAttribute('required', 'required');
} else {
elem.classList.add('hide');
elem.querySelector('input').setAttribute('required', '');
elem.querySelector('input').removeAttribute('required');
}
}
function loadValues(context, itemType, options, availableOptions) {
const supportedImageTypes = availableOptions.SupportedImageTypes || [];
setVisibilityOfBackdrops(context.querySelector('.backdropFields'), supportedImageTypes.includes('Backdrop'));
Array.prototype.forEach.call(context.querySelectorAll('.imageType'), i => {
const imageType = i.getAttribute('data-imagetype');
const container = dom.parentWithTag(i, 'LABEL');
if (!supportedImageTypes.includes(imageType)) {
container.classList.add('hide');
} else {
elem.classList.add('hide');
elem.querySelector('input').setAttribute('required', '');
elem.querySelector('input').removeAttribute('required');
container.classList.remove('hide');
}
}
function loadValues(context, itemType, options, availableOptions) {
const supportedImageTypes = availableOptions.SupportedImageTypes || [];
setVisibilityOfBackdrops(context.querySelector('.backdropFields'), supportedImageTypes.includes('Backdrop'));
Array.prototype.forEach.call(context.querySelectorAll('.imageType'), i => {
const imageType = i.getAttribute('data-imagetype');
const container = dom.parentWithTag(i, 'LABEL');
if (getImageConfig(options, availableOptions, imageType, itemType).Limit) {
i.checked = true;
} else {
i.checked = false;
}
});
const backdropConfig = getImageConfig(options, availableOptions, 'Backdrop', itemType);
context.querySelector('#txtMaxBackdrops').value = backdropConfig.Limit;
context.querySelector('#txtMinBackdropDownloadWidth').value = backdropConfig.MinWidth;
}
if (!supportedImageTypes.includes(imageType)) {
container.classList.add('hide');
} else {
container.classList.remove('hide');
}
function saveValues(context, options) {
options.ImageOptions = Array.prototype.map.call(context.querySelectorAll('.imageType:not(.hide)'), c => {
return {
Type: c.getAttribute('data-imagetype'),
Limit: c.checked ? 1 : 0,
MinWidth: 0
};
});
options.ImageOptions.push({
Type: 'Backdrop',
Limit: context.querySelector('#txtMaxBackdrops').value,
MinWidth: context.querySelector('#txtMinBackdropDownloadWidth').value
});
}
if (getImageConfig(options, availableOptions, imageType, itemType).Limit) {
i.checked = true;
} else {
i.checked = false;
}
});
const backdropConfig = getImageConfig(options, availableOptions, 'Backdrop', itemType);
context.querySelector('#txtMaxBackdrops').value = backdropConfig.Limit;
context.querySelector('#txtMinBackdropDownloadWidth').value = backdropConfig.MinWidth;
}
function saveValues(context, options) {
options.ImageOptions = Array.prototype.map.call(context.querySelectorAll('.imageType:not(.hide)'), c => {
return {
Type: c.getAttribute('data-imagetype'),
Limit: c.checked ? 1 : 0,
MinWidth: 0
};
});
options.ImageOptions.push({
Type: 'Backdrop',
Limit: context.querySelector('#txtMaxBackdrops').value,
MinWidth: context.querySelector('#txtMinBackdropDownloadWidth').value
});
}
function showEditor(itemType, options, availableOptions) {
const dlg = dialogHelper.createDialog({
size: 'small',
removeOnClose: true,
scrollY: false
});
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template);
dlg.addEventListener('close', function () {
saveValues(dlg, options);
});
loadValues(dlg, itemType, options, availableOptions);
dialogHelper.open(dlg).then(() => {
return;
}).catch(() => {
return;
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
}
function showEditor(itemType, options, availableOptions) {
const dlg = dialogHelper.createDialog({
size: 'small',
removeOnClose: true,
scrollY: false
});
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template);
dlg.addEventListener('close', function () {
saveValues(dlg, options);
});
loadValues(dlg, itemType, options, availableOptions);
dialogHelper.open(dlg).then(() => {
return;
}).catch(() => {
return;
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
}
export class editor {
constructor() {
@ -109,5 +108,4 @@ export class editor {
}
}
/* eslint-enable indent */
export default editor;

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for imageUploader.
@ -19,169 +18,168 @@ import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
import template from './imageUploader.template.html';
let currentItemId;
let currentServerId;
let currentFile;
let hasChanges = false;
let currentItemId;
let currentServerId;
let currentFile;
let hasChanges = false;
function onFileReaderError(evt) {
function onFileReaderError(evt) {
loading.hide();
switch (evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
toast(globalize.translate('MessageFileReadError'));
break;
case evt.target.error.ABORT_ERR:
break; // noop
default:
toast(globalize.translate('MessageFileReadError'));
break;
}
}
function setFiles(page, files) {
const file = files[0];
if (!file || !file.type.match('image.*')) {
page.querySelector('#imageOutput').innerHTML = '';
page.querySelector('#fldUpload').classList.add('hide');
currentFile = null;
return;
}
currentFile = file;
const reader = new FileReader();
reader.onerror = onFileReaderError;
reader.onloadstart = () => {
page.querySelector('#fldUpload').classList.add('hide');
};
reader.onabort = () => {
loading.hide();
console.debug('File read cancelled');
};
switch (evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
toast(globalize.translate('MessageFileReadError'));
break;
case evt.target.error.ABORT_ERR:
break; // noop
default:
toast(globalize.translate('MessageFileReadError'));
break;
}
// Closure to capture the file information.
reader.onload = (theFile => {
return e => {
// Render thumbnail.
const html = ['<img style="max-width:100%;max-height:100%;" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join('');
page.querySelector('#imageOutput').innerHTML = html;
page.querySelector('#dropImageText').classList.add('hide');
page.querySelector('#fldUpload').classList.remove('hide');
};
})(file);
// Read in the image file as a data URL.
reader.readAsDataURL(file);
}
function onSubmit(e) {
const file = currentFile;
if (!file) {
return false;
}
function setFiles(page, files) {
const file = files[0];
if (!file || !file.type.match('image.*')) {
page.querySelector('#imageOutput').innerHTML = '';
page.querySelector('#fldUpload').classList.add('hide');
currentFile = null;
return;
}
currentFile = file;
const reader = new FileReader();
reader.onerror = onFileReaderError;
reader.onloadstart = () => {
page.querySelector('#fldUpload').classList.add('hide');
};
reader.onabort = () => {
loading.hide();
console.debug('File read cancelled');
};
// Closure to capture the file information.
reader.onload = (theFile => {
return e => {
// Render thumbnail.
const html = ['<img style="max-width:100%;max-height:100%;" src="', e.target.result, '" title="', escape(theFile.name), '"/>'].join('');
page.querySelector('#imageOutput').innerHTML = html;
page.querySelector('#dropImageText').classList.add('hide');
page.querySelector('#fldUpload').classList.remove('hide');
};
})(file);
// Read in the image file as a data URL.
reader.readAsDataURL(file);
}
function onSubmit(e) {
const file = currentFile;
if (!file) {
return false;
}
if (!file.type.startsWith('image/')) {
toast(globalize.translate('MessageImageFileTypeAllowed'));
e.preventDefault();
return false;
}
loading.show();
const dlg = dom.parentWithClass(this, 'dialog');
const imageType = dlg.querySelector('#selectImageType').value;
if (imageType === 'None') {
toast(globalize.translate('MessageImageTypeNotSelected'));
e.preventDefault();
return false;
}
ServerConnections.getApiClient(currentServerId).uploadItemImage(currentItemId, imageType, file).then(() => {
dlg.querySelector('#uploadImage').value = '';
loading.hide();
hasChanges = true;
dialogHelper.close(dlg);
});
if (!file.type.startsWith('image/')) {
toast(globalize.translate('MessageImageFileTypeAllowed'));
e.preventDefault();
return false;
}
function initEditor(page) {
page.querySelector('form').addEventListener('submit', onSubmit);
loading.show();
page.querySelector('#uploadImage').addEventListener('change', function () {
setFiles(page, this.files);
});
const dlg = dom.parentWithClass(this, 'dialog');
page.querySelector('.btnBrowse').addEventListener('click', () => {
page.querySelector('#uploadImage').click();
});
const imageType = dlg.querySelector('#selectImageType').value;
if (imageType === 'None') {
toast(globalize.translate('MessageImageTypeNotSelected'));
e.preventDefault();
return false;
}
function showEditor(options, resolve) {
options = options || {};
ServerConnections.getApiClient(currentServerId).uploadItemImage(currentItemId, imageType, file).then(() => {
dlg.querySelector('#uploadImage').value = '';
currentItemId = options.itemId;
currentServerId = options.serverId;
loading.hide();
hasChanges = true;
dialogHelper.close(dlg);
});
const dialogOptions = {
removeOnClose: true
};
e.preventDefault();
return false;
}
function initEditor(page) {
page.querySelector('form').addEventListener('submit', onSubmit);
page.querySelector('#uploadImage').addEventListener('change', function () {
setFiles(page, this.files);
});
page.querySelector('.btnBrowse').addEventListener('click', () => {
page.querySelector('#uploadImage').click();
});
}
function showEditor(options, resolve) {
options = options || {};
currentItemId = options.itemId;
currentServerId = options.serverId;
const dialogOptions = {
removeOnClose: true
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template, 'core');
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', () => {
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
scrollHelper.centerFocus.off(dlg, false);
}
const dlg = dialogHelper.createDialog(dialogOptions);
loading.hide();
resolve(hasChanges);
});
dlg.classList.add('formDialog');
dialogHelper.open(dlg);
dlg.innerHTML = globalize.translateHtml(template, 'core');
initEditor(dlg);
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
dlg.querySelector('#selectImageType').value = options.imageType || 'Primary';
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', () => {
if (layoutManager.tv) {
scrollHelper.centerFocus.off(dlg, false);
}
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
}
loading.hide();
resolve(hasChanges);
});
export function show(options) {
return new Promise(resolve => {
hasChanges = false;
dialogHelper.open(dlg);
showEditor(options, resolve);
});
}
initEditor(dlg);
dlg.querySelector('#selectImageType').value = options.imageType || 'Primary';
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
}
export function show(options) {
return new Promise(resolve => {
hasChanges = false;
showEditor(options, resolve);
});
}
/* eslint-enable indent */
export default {
show: show
};

View file

@ -18,441 +18,439 @@ import alert from '../alert';
import confirm from '../confirm/confirm';
import template from './imageeditor.template.html';
/* eslint-disable indent */
const enableFocusTransform = !browser.slow && !browser.edge;
const enableFocusTransform = !browser.slow && !browser.edge;
let currentItem;
let hasChanges = false;
let currentItem;
let hasChanges = false;
function getBaseRemoteOptions() {
return { itemId: currentItem.Id };
}
function getBaseRemoteOptions() {
return { itemId: currentItem.Id };
function reload(page, item, focusContext) {
loading.show();
let apiClient;
if (item) {
apiClient = ServerConnections.getApiClient(item.ServerId);
reloadItem(page, item, apiClient, focusContext);
} else {
apiClient = ServerConnections.getApiClient(currentItem.ServerId);
apiClient.getItem(apiClient.getCurrentUserId(), currentItem.Id).then(function (itemToReload) {
reloadItem(page, itemToReload, apiClient, focusContext);
});
}
}
function reload(page, item, focusContext) {
loading.show();
let apiClient;
if (item) {
apiClient = ServerConnections.getApiClient(item.ServerId);
reloadItem(page, item, apiClient, focusContext);
} else {
apiClient = ServerConnections.getApiClient(currentItem.ServerId);
apiClient.getItem(apiClient.getCurrentUserId(), currentItem.Id).then(function (itemToReload) {
reloadItem(page, itemToReload, apiClient, focusContext);
});
function addListeners(container, className, eventName, fn) {
container.addEventListener(eventName, function (e) {
const elem = dom.parentWithClass(e.target, className);
if (elem) {
fn.call(elem, e);
}
}
});
}
function addListeners(container, className, eventName, fn) {
container.addEventListener(eventName, function (e) {
const elem = dom.parentWithClass(e.target, className);
if (elem) {
fn.call(elem, e);
function reloadItem(page, item, apiClient, focusContext) {
currentItem = item;
apiClient.getRemoteImageProviders(getBaseRemoteOptions()).then(function (providers) {
const btnBrowseAllImages = page.querySelectorAll('.btnBrowseAllImages');
for (let i = 0, length = btnBrowseAllImages.length; i < length; i++) {
if (providers.length) {
btnBrowseAllImages[i].classList.remove('hide');
} else {
btnBrowseAllImages[i].classList.add('hide');
}
}
apiClient.getItemImageInfos(currentItem.Id).then(function (imageInfos) {
renderStandardImages(page, apiClient, item, imageInfos, providers);
renderBackdrops(page, apiClient, item, imageInfos, providers);
loading.hide();
if (layoutManager.tv) {
focusManager.autoFocus((focusContext || page));
}
});
});
}
function getImageUrl(item, apiClient, type, index, options) {
options = options || {};
options.type = type;
options.index = index;
if (type === 'Backdrop') {
options.tag = item.BackdropImageTags[index];
} else if (type === 'Primary') {
options.tag = item.PrimaryImageTag || item.ImageTags[type];
} else {
options.tag = item.ImageTags[type];
}
function reloadItem(page, item, apiClient, focusContext) {
currentItem = item;
// For search hints
return apiClient.getScaledImageUrl(item.Id || item.ItemId, options);
}
apiClient.getRemoteImageProviders(getBaseRemoteOptions()).then(function (providers) {
const btnBrowseAllImages = page.querySelectorAll('.btnBrowseAllImages');
for (let i = 0, length = btnBrowseAllImages.length; i < length; i++) {
if (providers.length) {
btnBrowseAllImages[i].classList.remove('hide');
} else {
btnBrowseAllImages[i].classList.add('hide');
}
function getCardHtml(image, apiClient, options) {
// TODO move card creation code to Card component
let html = '';
let cssClass = 'card scalableCard imageEditorCard';
const cardBoxCssClass = 'cardBox visualCardBox';
cssClass += ' backdropCard backdropCard-scalable';
if (options.tagName === 'button') {
cssClass += ' btnImageCard';
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
}
html += '<button type="button" class="' + cssClass + '"';
} else {
html += '<div class="' + cssClass + '"';
}
html += ' data-id="' + currentItem.Id + '" data-serverid="' + apiClient.serverId() + '" data-index="' + options.index + '" data-numimages="' + options.numImages + '" data-imagetype="' + image.ImageType + '" data-providers="' + options.imageProviders.length + '"';
html += '>';
html += '<div class="' + cardBoxCssClass + '">';
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
html += '<div class="cardPadder-backdrop"></div>';
html += '<div class="cardContent">';
const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, { maxWidth: options.imageSize });
html += '<div class="cardImageContainer" style="background-image:url(\'' + imageUrl + '\');background-position:center center;background-size:contain;"></div>';
html += '</div>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<h3 class="cardText cardTextCentered" style="margin:0;">' + globalize.translate('' + image.ImageType) + '</h3>';
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.Width && image.Height) {
html += image.Width + ' X ' + image.Height;
} else {
html += '&nbsp;';
}
html += '</div>';
if (options.enableFooterButtons) {
html += '<div class="cardText cardTextCentered">';
if (image.ImageType === 'Backdrop') {
if (options.index > 0) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
}
apiClient.getItemImageInfos(currentItem.Id).then(function (imageInfos) {
renderStandardImages(page, apiClient, item, imageInfos, providers);
renderBackdrops(page, apiClient, item, imageInfos, providers);
loading.hide();
if (options.index < options.numImages - 1) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
}
} else {
if (options.imageProviders.length) {
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
}
}
if (layoutManager.tv) {
focusManager.autoFocus((focusContext || page));
}
});
});
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>';
}
function getImageUrl(item, apiClient, type, index, options) {
options = options || {};
options.type = type;
options.index = index;
html += '</div>';
html += '</div>';
html += '</' + options.tagName + '>';
return html;
}
function deleteImage(context, itemId, type, index, apiClient, enableConfirmation) {
const afterConfirm = function () {
apiClient.deleteItemImage(itemId, type, index).then(function () {
hasChanges = true;
reload(context);
});
};
if (!enableConfirmation) {
afterConfirm();
return;
}
confirm({
text: globalize.translate('ConfirmDeleteImage'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(afterConfirm);
}
function moveImage(context, apiClient, itemId, type, index, newIndex, focusContext) {
apiClient.updateItemImageIndex(itemId, type, index, newIndex).then(function () {
hasChanges = true;
reload(context, null, focusContext);
}, function () {
alert(globalize.translate('ErrorDefault'));
});
}
function renderImages(page, item, apiClient, images, imageProviders, elem) {
let html = '';
let imageSize = 300;
const windowSize = dom.getWindowSize();
if (windowSize.innerWidth >= 1280) {
imageSize = Math.round(windowSize.innerWidth / 4);
}
const tagName = layoutManager.tv ? 'button' : 'div';
const enableFooterButtons = !layoutManager.tv;
for (let i = 0, length = images.length; i < length; i++) {
const image = images[i];
const options = { index: i, numImages: length, imageProviders, imageSize, tagName, enableFooterButtons };
html += getCardHtml(image, apiClient, options);
}
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
}
function renderStandardImages(page, apiClient, item, imageInfos, imageProviders) {
const images = imageInfos.filter(function (i) {
return i.ImageType !== 'Backdrop' && i.ImageType !== 'Chapter';
});
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#images'));
}
function renderBackdrops(page, apiClient, item, imageInfos, imageProviders) {
const images = imageInfos.filter(function (i) {
return i.ImageType === 'Backdrop';
}).sort(function (a, b) {
return a.ImageIndex - b.ImageIndex;
});
if (images.length) {
page.querySelector('#backdropsContainer', page).classList.remove('hide');
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#backdrops'));
} else {
page.querySelector('#backdropsContainer', page).classList.add('hide');
}
}
function showImageDownloader(page, imageType) {
import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
ImageDownloader.show(
currentItem.Id,
currentItem.ServerId,
currentItem.Type,
imageType,
currentItem.Type == 'Season' ? currentItem.ParentId : null
).then(function () {
hasChanges = true;
reload(page);
});
});
}
function showActionSheet(context, imageCard) {
const itemId = imageCard.getAttribute('data-id');
const serverId = imageCard.getAttribute('data-serverid');
const apiClient = ServerConnections.getApiClient(serverId);
const type = imageCard.getAttribute('data-imagetype');
const index = parseInt(imageCard.getAttribute('data-index'), 10);
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
import('../actionSheet/actionSheet').then(({ default: actionSheet }) => {
const commands = [];
commands.push({
name: globalize.translate('Delete'),
id: 'delete'
});
if (type === 'Backdrop') {
options.tag = item.BackdropImageTags[index];
} else if (type === 'Primary') {
options.tag = item.PrimaryImageTag || item.ImageTags[type];
} else {
options.tag = item.ImageTags[type];
}
// For search hints
return apiClient.getScaledImageUrl(item.Id || item.ItemId, options);
}
function getCardHtml(image, apiClient, options) {
// TODO move card creation code to Card component
let html = '';
let cssClass = 'card scalableCard imageEditorCard';
const cardBoxCssClass = 'cardBox visualCardBox';
cssClass += ' backdropCard backdropCard-scalable';
if (options.tagName === 'button') {
cssClass += ' btnImageCard';
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
if (index > 0) {
commands.push({
name: globalize.translate('MoveLeft'),
id: 'moveleft'
});
}
html += '<button type="button" class="' + cssClass + '"';
} else {
html += '<div class="' + cssClass + '"';
}
html += ' data-id="' + currentItem.Id + '" data-serverid="' + apiClient.serverId() + '" data-index="' + options.index + '" data-numimages="' + options.numImages + '" data-imagetype="' + image.ImageType + '" data-providers="' + options.imageProviders.length + '"';
html += '>';
html += '<div class="' + cardBoxCssClass + '">';
html += '<div class="cardScalable visualCardBox-cardScalable" style="background-color:transparent;">';
html += '<div class="cardPadder-backdrop"></div>';
html += '<div class="cardContent">';
const imageUrl = getImageUrl(currentItem, apiClient, image.ImageType, image.ImageIndex, { maxWidth: options.imageSize });
html += '<div class="cardImageContainer" style="background-image:url(\'' + imageUrl + '\');background-position:center center;background-size:contain;"></div>';
html += '</div>';
html += '</div>';
html += '<div class="cardFooter visualCardBox-cardFooter">';
html += '<h3 class="cardText cardTextCentered" style="margin:0;">' + globalize.translate('' + image.ImageType) + '</h3>';
html += '<div class="cardText cardText-secondary cardTextCentered">';
if (image.Width && image.Height) {
html += image.Width + ' X ' + image.Height;
} else {
html += '&nbsp;';
}
html += '</div>';
if (options.enableFooterButtons) {
html += '<div class="cardText cardTextCentered">';
if (image.ImageType === 'Backdrop') {
if (options.index > 0) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex - 1) + '" title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveLeft') + '"><span class="material-icons chevron_left" aria-hidden="true"></span></button>';
}
if (options.index < options.numImages - 1) {
html += '<button type="button" is="paper-icon-button-light" class="btnMoveImage autoSize" data-imagetype="' + image.ImageType + '" data-index="' + image.ImageIndex + '" data-newindex="' + (image.ImageIndex + 1) + '" title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="autoSize" disabled title="' + globalize.translate('MoveRight') + '"><span class="material-icons chevron_right" aria-hidden="true"></span></button>';
}
} else {
if (options.imageProviders.length) {
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" class="btnSearchImages autoSize" title="' + globalize.translate('Search') + '"><span class="material-icons search" aria-hidden="true"></span></button>';
}
if (index < numImages - 1) {
commands.push({
name: globalize.translate('MoveRight'),
id: 'moveright'
});
}
html += '<button type="button" is="paper-icon-button-light" data-imagetype="' + image.ImageType + '" data-index="' + (image.ImageIndex != null ? image.ImageIndex : 'null') + '" class="btnDeleteImage autoSize" title="' + globalize.translate('Delete') + '"><span class="material-icons delete" aria-hidden="true"></span></button>';
html += '</div>';
}
html += '</div>';
html += '</div>';
html += '</' + options.tagName + '>';
return html;
}
function deleteImage(context, itemId, type, index, apiClient, enableConfirmation) {
const afterConfirm = function () {
apiClient.deleteItemImage(itemId, type, index).then(function () {
hasChanges = true;
reload(context);
if (providerCount) {
commands.push({
name: globalize.translate('Search'),
id: 'search'
});
}
actionSheet.show({
items: commands,
positionTo: imageCard
}).then(function (id) {
switch (id) {
case 'delete':
deleteImage(context, itemId, type, index, apiClient, false);
break;
case 'search':
showImageDownloader(context, type);
break;
case 'moveleft':
moveImage(context, apiClient, itemId, type, index, index - 1, dom.parentWithClass(imageCard, 'itemsContainer'));
break;
case 'moveright':
moveImage(context, apiClient, itemId, type, index, index + 1, dom.parentWithClass(imageCard, 'itemsContainer'));
break;
default:
break;
}
});
});
}
function initEditor(context, options) {
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu');
const isFileInputSupported = appHost.supports('fileinput');
for (let i = 0, length = uploadButtons.length; i < length; i++) {
if (isFileInputSupported) {
uploadButtons[i].classList.remove('hide');
} else {
uploadButtons[i].classList.add('hide');
}
}
addListeners(context, 'btnOpenUploadMenu', 'click', function () {
const imageType = this.getAttribute('data-imagetype');
import('../imageUploader/imageUploader').then(({ default: imageUploader }) => {
imageUploader.show({
theme: options.theme,
imageType: imageType,
itemId: currentItem.Id,
serverId: currentItem.ServerId
}).then(function (hasChanged) {
if (hasChanged) {
hasChanges = true;
reload(context);
}
});
});
});
addListeners(context, 'btnSearchImages', 'click', function () {
showImageDownloader(context, this.getAttribute('data-imagetype'));
});
addListeners(context, 'btnBrowseAllImages', 'click', function () {
showImageDownloader(context, this.getAttribute('data-imagetype') || 'Primary');
});
addListeners(context, 'btnImageCard', 'click', function () {
showActionSheet(context, this);
});
addListeners(context, 'btnDeleteImage', 'click', function () {
const type = this.getAttribute('data-imagetype');
let index = this.getAttribute('data-index');
index = index === 'null' ? null : parseInt(index, 10);
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
deleteImage(context, currentItem.Id, type, index, apiClient, true);
});
addListeners(context, 'btnMoveImage', 'click', function () {
const type = this.getAttribute('data-imagetype');
const index = this.getAttribute('data-index');
const newIndex = this.getAttribute('data-newindex');
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
moveImage(context, apiClient, currentItem.Id, type, index, newIndex, dom.parentWithClass(this, 'itemsContainer'));
});
}
function showEditor(options, resolve, reject) {
const itemId = options.itemId;
const serverId = options.serverId;
loading.show();
const apiClient = ServerConnections.getApiClient(serverId);
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
const dialogOptions = {
removeOnClose: true
};
if (!enableConfirmation) {
afterConfirm();
return;
}
confirm({
text: globalize.translate('ConfirmDeleteImage'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(afterConfirm);
}
function moveImage(context, apiClient, itemId, type, index, newIndex, focusContext) {
apiClient.updateItemImageIndex(itemId, type, index, newIndex).then(function () {
hasChanges = true;
reload(context, null, focusContext);
}, function () {
alert(globalize.translate('ErrorDefault'));
});
}
function renderImages(page, item, apiClient, images, imageProviders, elem) {
let html = '';
let imageSize = 300;
const windowSize = dom.getWindowSize();
if (windowSize.innerWidth >= 1280) {
imageSize = Math.round(windowSize.innerWidth / 4);
}
const tagName = layoutManager.tv ? 'button' : 'div';
const enableFooterButtons = !layoutManager.tv;
for (let i = 0, length = images.length; i < length; i++) {
const image = images[i];
const options = { index: i, numImages: length, imageProviders, imageSize, tagName, enableFooterButtons };
html += getCardHtml(image, apiClient, options);
}
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
}
function renderStandardImages(page, apiClient, item, imageInfos, imageProviders) {
const images = imageInfos.filter(function (i) {
return i.ImageType !== 'Backdrop' && i.ImageType !== 'Chapter';
});
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#images'));
}
function renderBackdrops(page, apiClient, item, imageInfos, imageProviders) {
const images = imageInfos.filter(function (i) {
return i.ImageType === 'Backdrop';
}).sort(function (a, b) {
return a.ImageIndex - b.ImageIndex;
});
if (images.length) {
page.querySelector('#backdropsContainer', page).classList.remove('hide');
renderImages(page, item, apiClient, images, imageProviders, page.querySelector('#backdrops'));
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
page.querySelector('#backdropsContainer', page).classList.add('hide');
}
}
function showImageDownloader(page, imageType) {
import('../imageDownloader/imageDownloader').then((ImageDownloader) => {
ImageDownloader.show(
currentItem.Id,
currentItem.ServerId,
currentItem.Type,
imageType,
currentItem.Type == 'Season' ? currentItem.ParentId : null
).then(function () {
hasChanges = true;
reload(page);
});
});
}
function showActionSheet(context, imageCard) {
const itemId = imageCard.getAttribute('data-id');
const serverId = imageCard.getAttribute('data-serverid');
const apiClient = ServerConnections.getApiClient(serverId);
const type = imageCard.getAttribute('data-imagetype');
const index = parseInt(imageCard.getAttribute('data-index'), 10);
const providerCount = parseInt(imageCard.getAttribute('data-providers'), 10);
const numImages = parseInt(imageCard.getAttribute('data-numimages'), 10);
import('../actionSheet/actionSheet').then(({ default: actionSheet }) => {
const commands = [];
commands.push({
name: globalize.translate('Delete'),
id: 'delete'
});
if (type === 'Backdrop') {
if (index > 0) {
commands.push({
name: globalize.translate('MoveLeft'),
id: 'moveleft'
});
}
if (index < numImages - 1) {
commands.push({
name: globalize.translate('MoveRight'),
id: 'moveright'
});
}
}
if (providerCount) {
commands.push({
name: globalize.translate('Search'),
id: 'search'
});
}
actionSheet.show({
items: commands,
positionTo: imageCard
}).then(function (id) {
switch (id) {
case 'delete':
deleteImage(context, itemId, type, index, apiClient, false);
break;
case 'search':
showImageDownloader(context, type);
break;
case 'moveleft':
moveImage(context, apiClient, itemId, type, index, index - 1, dom.parentWithClass(imageCard, 'itemsContainer'));
break;
case 'moveright':
moveImage(context, apiClient, itemId, type, index, index + 1, dom.parentWithClass(imageCard, 'itemsContainer'));
break;
default:
break;
}
});
});
}
function initEditor(context, options) {
const uploadButtons = context.querySelectorAll('.btnOpenUploadMenu');
const isFileInputSupported = appHost.supports('fileinput');
for (let i = 0, length = uploadButtons.length; i < length; i++) {
if (isFileInputSupported) {
uploadButtons[i].classList.remove('hide');
} else {
uploadButtons[i].classList.add('hide');
}
dialogOptions.size = 'small';
}
addListeners(context, 'btnOpenUploadMenu', 'click', function () {
const imageType = this.getAttribute('data-imagetype');
const dlg = dialogHelper.createDialog(dialogOptions);
import('../imageUploader/imageUploader').then(({ default: imageUploader }) => {
imageUploader.show({
dlg.classList.add('formDialog');
theme: options.theme,
imageType: imageType,
itemId: currentItem.Id,
serverId: currentItem.ServerId
dlg.innerHTML = globalize.translateHtml(template, 'core');
}).then(function (hasChanged) {
if (hasChanged) {
hasChanges = true;
reload(context);
}
});
});
});
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
addListeners(context, 'btnSearchImages', 'click', function () {
showImageDownloader(context, this.getAttribute('data-imagetype'));
});
addListeners(context, 'btnBrowseAllImages', 'click', function () {
showImageDownloader(context, this.getAttribute('data-imagetype') || 'Primary');
});
addListeners(context, 'btnImageCard', 'click', function () {
showActionSheet(context, this);
});
addListeners(context, 'btnDeleteImage', 'click', function () {
const type = this.getAttribute('data-imagetype');
let index = this.getAttribute('data-index');
index = index === 'null' ? null : parseInt(index, 10);
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
deleteImage(context, currentItem.Id, type, index, apiClient, true);
});
addListeners(context, 'btnMoveImage', 'click', function () {
const type = this.getAttribute('data-imagetype');
const index = this.getAttribute('data-index');
const newIndex = this.getAttribute('data-newindex');
const apiClient = ServerConnections.getApiClient(currentItem.ServerId);
moveImage(context, apiClient, currentItem.Id, type, index, newIndex, dom.parentWithClass(this, 'itemsContainer'));
});
}
function showEditor(options, resolve, reject) {
const itemId = options.itemId;
const serverId = options.serverId;
loading.show();
const apiClient = ServerConnections.getApiClient(serverId);
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(function (item) {
const dialogOptions = {
removeOnClose: true
};
initEditor(dlg, options);
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', function () {
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
scrollHelper.centerFocus.off(dlg, false);
}
loading.hide();
if (hasChanges) {
resolve();
} else {
dialogOptions.size = 'small';
reject();
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
dlg.innerHTML = globalize.translateHtml(template, 'core');
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg, false);
}
initEditor(dlg, options);
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', function () {
if (layoutManager.tv) {
scrollHelper.centerFocus.off(dlg, false);
}
loading.hide();
if (hasChanges) {
resolve();
} else {
reject();
}
});
dialogHelper.open(dlg);
reload(dlg, item);
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
});
}
dialogHelper.open(dlg);
reload(dlg, item);
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
});
}
export function show (options) {
return new Promise(function (resolve, reject) {
@ -465,4 +463,3 @@ export default {
show
};
/* eslint-enable indent */

View file

@ -17,237 +17,235 @@ worker.addEventListener(
}
}
);
/* eslint-disable indent */
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
if (!source) {
return;
}
fillImageElement(elem, source);
export function lazyImage(elem, source = elem.getAttribute('data-src')) {
if (!source) {
return;
}
function drawBlurhash(target, pixels, width, height) {
const canvas = document.createElement('canvas');
canvas.setAttribute('aria-hidden', 'true');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(width, height);
fillImageElement(elem, source);
}
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
function drawBlurhash(target, pixels, width, height) {
const canvas = document.createElement('canvas');
canvas.setAttribute('aria-hidden', 'true');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imgData = ctx.createImageData(width, height);
requestAnimationFrame(() => {
// This class is just an utility class, so users can customize the canvas using their own CSS.
canvas.classList.add('blurhash-canvas');
imgData.data.set(pixels);
ctx.putImageData(imgData, 0, 0);
target.parentNode.insertBefore(canvas, target);
target.classList.add('blurhashed');
target.removeAttribute('data-blurhash');
requestAnimationFrame(() => {
// This class is just an utility class, so users can customize the canvas using their own CSS.
canvas.classList.add('blurhash-canvas');
target.parentNode.insertBefore(canvas, target);
target.classList.add('blurhashed');
target.removeAttribute('data-blurhash');
});
}
function itemBlurhashing(target, hash) {
try {
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
// Lower values had more visible pixelation
const width = 20;
const height = 20;
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
targetDic[hash].push(target);
worker.postMessage({
hash,
width,
height
});
} catch (err) {
console.error(err);
target.classList.add('non-blurhashable');
return;
}
}
export function fillImage(entry) {
if (!entry) {
throw new Error('entry cannot be null');
}
const target = entry.target;
let source = undefined;
if (target) {
source = target.getAttribute('data-src');
} else {
source = entry;
}
function itemBlurhashing(target, hash) {
try {
// Although the default values recommended by Blurhash developers is 32x32, a size of 20x20 seems to be the sweet spot for us,
// improving the performance and reducing the memory usage, while retaining almost full blur quality.
// Lower values had more visible pixelation
const width = 20;
const height = 20;
targetDic[hash] = (targetDic[hash] || []).filter(item => item !== target);
targetDic[hash].push(target);
worker.postMessage({
hash,
width,
height
});
} catch (err) {
console.error(err);
target.classList.add('non-blurhashable');
return;
if (entry.isIntersecting) {
if (source) {
fillImageElement(target, source);
}
} else if (!source) {
emptyImageElement(target);
}
}
export function fillImage(entry) {
if (!entry) {
throw new Error('entry cannot be null');
}
const target = entry.target;
let source = undefined;
if (target) {
source = target.getAttribute('data-src');
} else {
source = entry;
}
if (entry.isIntersecting) {
if (source) {
fillImageElement(target, source);
}
} else if (!source) {
emptyImageElement(target);
}
}
function onAnimationEnd(event) {
const elem = event.target;
requestAnimationFrame(() => {
const canvas = elem.previousSibling;
if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
canvas.classList.add('lazy-hidden');
}
// HACK: Hide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
});
elem.removeEventListener('animationend', onAnimationEnd);
}
function fillImageElement(elem, url) {
if (url === undefined) {
throw new TypeError('url cannot be undefined');
}
const preloaderImg = new Image();
preloaderImg.src = url;
elem.classList.add('lazy-hidden');
elem.addEventListener('animationend', onAnimationEnd);
preloaderImg.addEventListener('load', () => {
requestAnimationFrame(() => {
if (elem.tagName !== 'IMG') {
elem.style.backgroundImage = "url('" + url + "')";
} else {
elem.setAttribute('src', url);
}
elem.removeAttribute('data-src');
if (userSettings.enableFastFadein()) {
elem.classList.add('lazy-image-fadein-fast');
} else {
elem.classList.add('lazy-image-fadein');
}
elem.classList.remove('lazy-hidden');
});
});
}
function emptyImageElement(elem) {
elem.removeEventListener('animationend', onAnimationEnd);
function onAnimationEnd(event) {
const elem = event.target;
requestAnimationFrame(() => {
const canvas = elem.previousSibling;
if (canvas?.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
if (elem.classList.contains('blurhashed') && canvas?.tagName === 'CANVAS') {
canvas.classList.add('lazy-hidden');
}
// HACK: Unhide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
// HACK: Hide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.add('lazy-hidden-children');
});
elem.removeEventListener('animationend', onAnimationEnd);
}
let url;
if (elem.tagName !== 'IMG') {
url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, '');
elem.style.backgroundImage = 'none';
} else {
url = elem.getAttribute('src');
elem.setAttribute('src', '');
}
elem.setAttribute('data-src', url);
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
elem.classList.add('lazy-hidden');
function fillImageElement(elem, url) {
if (url === undefined) {
throw new TypeError('url cannot be undefined');
}
export function lazyChildren(elem) {
if (userSettings.enableBlurhash()) {
for (const lazyElem of elem.querySelectorAll('.lazy')) {
const blurhashstr = lazyElem.getAttribute('data-blurhash');
if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) {
itemBlurhashing(lazyElem, blurhashstr);
} else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) {
lazyElem.classList.add('non-blurhashable');
}
const preloaderImg = new Image();
preloaderImg.src = url;
elem.classList.add('lazy-hidden');
elem.addEventListener('animationend', onAnimationEnd);
preloaderImg.addEventListener('load', () => {
requestAnimationFrame(() => {
if (elem.tagName !== 'IMG') {
elem.style.backgroundImage = "url('" + url + "')";
} else {
elem.setAttribute('src', url);
}
}
elem.removeAttribute('data-src');
lazyLoader.lazyChildren(elem, fillImage);
}
export function getPrimaryImageAspectRatio(items) {
const values = [];
for (let i = 0, length = items.length; i < length; i++) {
const ratio = items[i].PrimaryImageAspectRatio || 0;
if (!ratio) {
continue;
if (userSettings.enableFastFadein()) {
elem.classList.add('lazy-image-fadein-fast');
} else {
elem.classList.add('lazy-image-fadein');
}
values[values.length] = ratio;
}
if (!values.length) {
return null;
}
// Use the median
values.sort(function (a, b) {
return a - b;
elem.classList.remove('lazy-hidden');
});
});
}
const half = Math.floor(values.length / 2);
let result;
if (values.length % 2) {
result = values[half];
} else {
result = (values[half - 1] + values[half]) / 2.0;
}
// If really close to 2:3 (poster image), just return 2:3
const aspect2x3 = 2 / 3;
if (Math.abs(aspect2x3 - result) <= 0.15) {
return aspect2x3;
}
// If really close to 16:9 (episode image), just return 16:9
const aspect16x9 = 16 / 9;
if (Math.abs(aspect16x9 - result) <= 0.2) {
return aspect16x9;
}
// If really close to 1 (square image), just return 1
if (Math.abs(1 - result) <= 0.15) {
return 1;
}
// If really close to 4:3 (poster image), just return 2:3
const aspect4x3 = 4 / 3;
if (Math.abs(aspect4x3 - result) <= 0.15) {
return aspect4x3;
}
return result;
function emptyImageElement(elem) {
elem.removeEventListener('animationend', onAnimationEnd);
const canvas = elem.previousSibling;
if (canvas?.tagName === 'CANVAS') {
canvas.classList.remove('lazy-hidden');
}
export function fillImages(elems) {
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[0];
fillImage(elem);
// HACK: Unhide the content of the card padder
elem.parentNode?.querySelector('.cardPadder')?.classList.remove('lazy-hidden-children');
let url;
if (elem.tagName !== 'IMG') {
url = elem.style.backgroundImage.slice(4, -1).replace(/"/g, '');
elem.style.backgroundImage = 'none';
} else {
url = elem.getAttribute('src');
elem.setAttribute('src', '');
}
elem.setAttribute('data-src', url);
elem.classList.remove('lazy-image-fadein-fast', 'lazy-image-fadein');
elem.classList.add('lazy-hidden');
}
export function lazyChildren(elem) {
if (userSettings.enableBlurhash()) {
for (const lazyElem of elem.querySelectorAll('.lazy')) {
const blurhashstr = lazyElem.getAttribute('data-blurhash');
if (!lazyElem.classList.contains('blurhashed', 'non-blurhashable') && blurhashstr) {
itemBlurhashing(lazyElem, blurhashstr);
} else if (!blurhashstr && !lazyElem.classList.contains('blurhashed')) {
lazyElem.classList.add('non-blurhashable');
}
}
}
export function setLazyImage(element, url) {
element.classList.add('lazy');
element.setAttribute('data-src', url);
lazyImage(element);
lazyLoader.lazyChildren(elem, fillImage);
}
export function getPrimaryImageAspectRatio(items) {
const values = [];
for (let i = 0, length = items.length; i < length; i++) {
const ratio = items[i].PrimaryImageAspectRatio || 0;
if (!ratio) {
continue;
}
values[values.length] = ratio;
}
/* eslint-enable indent */
if (!values.length) {
return null;
}
// Use the median
values.sort(function (a, b) {
return a - b;
});
const half = Math.floor(values.length / 2);
let result;
if (values.length % 2) {
result = values[half];
} else {
result = (values[half - 1] + values[half]) / 2.0;
}
// If really close to 2:3 (poster image), just return 2:3
const aspect2x3 = 2 / 3;
if (Math.abs(aspect2x3 - result) <= 0.15) {
return aspect2x3;
}
// If really close to 16:9 (episode image), just return 16:9
const aspect16x9 = 16 / 9;
if (Math.abs(aspect16x9 - result) <= 0.2) {
return aspect16x9;
}
// If really close to 1 (square image), just return 1
if (Math.abs(1 - result) <= 0.15) {
return 1;
}
// If really close to 4:3 (poster image), just return 2:3
const aspect4x3 = 4 / 3;
if (Math.abs(aspect4x3 - result) <= 0.15) {
return aspect4x3;
}
return result;
}
export function fillImages(elems) {
for (let i = 0, length = elems.length; i < length; i++) {
const elem = elems[0];
fillImage(elem);
}
}
export function setLazyImage(element, url) {
element.classList.add('lazy');
element.setAttribute('data-src', url);
lazyImage(element);
}
export default {
setLazyImage: setLazyImage,
fillImages: fillImages,

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for display media info.
@ -30,229 +29,228 @@ const copyButtonHtml = layoutManager.tv ? '' :
><span class="material-icons content_copy" aria-hidden="true"></span></button>`;
const attributeDelimiterHtml = layoutManager.tv ? '' : '<span class="hide">: </span>';
function setMediaInfo(user, page, item) {
let html = item.MediaSources.map(version => {
return getMediaSourceHtml(user, item, version);
}).join('<div style="border-top:1px solid #444;margin: 1em 0;"></div>');
if (item.MediaSources.length > 1) {
html = `<br/>${html}`;
}
const mediaInfoContent = page.querySelector('#mediaInfoContent');
mediaInfoContent.innerHTML = html;
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
btn.addEventListener('click', () => {
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
copy(infoBlock.textContent).then(() => {
toast(globalize.translate('Copied'));
}).catch(() => {
console.error('Could not copy text');
toast(globalize.translate('CopyFailed'));
});
});
}
function setMediaInfo(user, page, item) {
let html = item.MediaSources.map(version => {
return getMediaSourceHtml(user, item, version);
}).join('<div style="border-top:1px solid #444;margin: 1em 0;"></div>');
if (item.MediaSources.length > 1) {
html = `<br/>${html}`;
}
const mediaInfoContent = page.querySelector('#mediaInfoContent');
mediaInfoContent.innerHTML = html;
function getMediaSourceHtml(user, item, version) {
let html = '<div class="mediaInfoSource">';
if (version.Name) {
html += `<div><h2 class="mediaInfoStreamType">${escapeHtml(version.Name)}${copyButtonHtml}</h2></div>\n`;
}
if (version.Container) {
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
}
if (version.Formats && version.Formats.length) {
html += `${createAttribute(globalize.translate('MediaInfoFormat'), version.Formats.join(','))}<br/>`;
}
if (version.Path && user && user.Policy.IsAdministrator) {
html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}<br/>`;
}
if (version.Size) {
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
}
version.MediaStreams.sort(itemHelper.sortTracks);
for (const stream of version.MediaStreams) {
if (stream.Type === 'Data') {
continue;
}
for (const btn of mediaInfoContent.querySelectorAll('.btnCopy')) {
btn.addEventListener('click', () => {
const infoBlock = dom.parentWithClass(btn, 'mediaInfoStream') || dom.parentWithClass(btn, 'mediaInfoSource') || mediaInfoContent;
html += '<div class="mediaInfoStream">';
let translateString;
switch (stream.Type) {
case 'Audio':
case 'Data':
case 'Subtitle':
case 'Video':
translateString = stream.Type;
break;
case 'EmbeddedImage':
translateString = 'Image';
break;
}
const displayType = globalize.translate(translateString);
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
const attributes = [];
if (stream.DisplayTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
}
if (stream.Language && stream.Type !== 'Video') {
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
}
if (stream.Codec) {
attributes.push(createAttribute(globalize.translate('MediaInfoCodec'), stream.Codec.toUpperCase()));
}
if (stream.CodecTag) {
attributes.push(createAttribute(globalize.translate('MediaInfoCodecTag'), stream.CodecTag));
}
if (stream.IsAVC != null) {
attributes.push(createAttribute('AVC', (stream.IsAVC ? 'Yes' : 'No')));
}
if (stream.Profile) {
attributes.push(createAttribute(globalize.translate('MediaInfoProfile'), stream.Profile));
}
if (stream.Level > 0) {
attributes.push(createAttribute(globalize.translate('MediaInfoLevel'), stream.Level));
}
if (stream.Width || stream.Height) {
attributes.push(createAttribute(globalize.translate('MediaInfoResolution'), `${stream.Width}x${stream.Height}`));
}
if (stream.AspectRatio && stream.Codec !== 'mjpeg') {
attributes.push(createAttribute(globalize.translate('MediaInfoAspectRatio'), stream.AspectRatio));
}
if (stream.Type === 'Video') {
if (stream.IsAnamorphic != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoAnamorphic'), (stream.IsAnamorphic ? 'Yes' : 'No')));
}
attributes.push(createAttribute(globalize.translate('MediaInfoInterlaced'), (stream.IsInterlaced ? 'Yes' : 'No')));
}
if ((stream.AverageFrameRate || stream.RealFrameRate) && stream.Type === 'Video') {
attributes.push(createAttribute(globalize.translate('MediaInfoFramerate'), (stream.AverageFrameRate || stream.RealFrameRate)));
}
if (stream.ChannelLayout) {
attributes.push(createAttribute(globalize.translate('MediaInfoLayout'), stream.ChannelLayout));
}
if (stream.Channels) {
attributes.push(createAttribute(globalize.translate('MediaInfoChannels'), `${stream.Channels} ch`));
}
if (stream.BitRate) {
attributes.push(createAttribute(globalize.translate('MediaInfoBitrate'), `${parseInt(stream.BitRate / 1000, 10)} kbps`));
}
if (stream.SampleRate) {
attributes.push(createAttribute(globalize.translate('MediaInfoSampleRate'), `${stream.SampleRate} Hz`));
}
if (stream.BitDepth) {
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
}
if (stream.VideoRange) {
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
}
if (stream.VideoRangeType) {
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
}
if (stream.VideoDoViTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoDoViTitle'), stream.VideoDoViTitle));
if (stream.DvVersionMajor != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMajor'), stream.DvVersionMajor));
}
if (stream.DvVersionMinor != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMinor'), stream.DvVersionMinor));
}
if (stream.DvProfile != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvProfile'), stream.DvProfile));
}
if (stream.DvLevel != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvLevel'), stream.DvLevel));
}
if (stream.RpuPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoRpuPresentFlag'), stream.RpuPresentFlag));
}
if (stream.ElPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoElPresentFlag'), stream.ElPresentFlag));
}
if (stream.BlPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoBlPresentFlag'), stream.BlPresentFlag));
}
if (stream.DvBlSignalCompatibilityId != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvBlSignalCompatibilityId'), stream.DvBlSignalCompatibilityId));
}
}
if (stream.ColorSpace) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorSpace'), stream.ColorSpace));
}
if (stream.ColorTransfer) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorTransfer'), stream.ColorTransfer));
}
if (stream.ColorPrimaries) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorPrimaries'), stream.ColorPrimaries));
}
if (stream.PixelFormat) {
attributes.push(createAttribute(globalize.translate('MediaInfoPixelFormat'), stream.PixelFormat));
}
if (stream.RefFrames) {
attributes.push(createAttribute(globalize.translate('MediaInfoRefFrames'), stream.RefFrames));
}
if (stream.NalLengthSize) {
attributes.push(createAttribute('NAL', stream.NalLengthSize));
}
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
}
if (stream.Type === 'Video' && version.Timestamp) {
attributes.push(createAttribute(globalize.translate('MediaInfoTimestamp'), version.Timestamp));
}
html += attributes.join('<br/>');
html += '</div>';
}
html += '</div>';
return html;
}
// File Paths should be always ltr. The isLtr parameter allows this.
function createAttribute(label, value, isLtr) {
return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute" ${isLtr && 'dir="ltr"'}>${escapeHtml(value)}</span>\n`;
}
function loadMediaInfo(itemId, serverId) {
const apiClient = ServerConnections.getApiClient(serverId);
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
const dialogOptions = {
size: 'small',
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
if (layoutManager.tv) {
dlg.querySelector('.formDialogContent');
}
dialogHelper.open(dlg);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
copy(infoBlock.textContent).then(() => {
toast(globalize.translate('Copied'));
}).catch(() => {
console.error('Could not copy text');
toast(globalize.translate('CopyFailed'));
});
apiClient.getCurrentUser().then(user => {
setMediaInfo(user, dlg, item);
});
loading.hide();
});
}
}
export function show(itemId, serverId) {
loading.show();
return loadMediaInfo(itemId, serverId);
function getMediaSourceHtml(user, item, version) {
let html = '<div class="mediaInfoSource">';
if (version.Name) {
html += `<div><h2 class="mediaInfoStreamType">${escapeHtml(version.Name)}${copyButtonHtml}</h2></div>\n`;
}
if (version.Container) {
html += `${createAttribute(globalize.translate('MediaInfoContainer'), version.Container)}<br/>`;
}
if (version.Formats && version.Formats.length) {
html += `${createAttribute(globalize.translate('MediaInfoFormat'), version.Formats.join(','))}<br/>`;
}
if (version.Path && user && user.Policy.IsAdministrator) {
html += `${createAttribute(globalize.translate('MediaInfoPath'), version.Path, true)}<br/>`;
}
if (version.Size) {
const size = `${(version.Size / (1024 * 1024)).toFixed(0)} MB`;
html += `${createAttribute(globalize.translate('MediaInfoSize'), size)}<br/>`;
}
version.MediaStreams.sort(itemHelper.sortTracks);
for (const stream of version.MediaStreams) {
if (stream.Type === 'Data') {
continue;
}
html += '<div class="mediaInfoStream">';
let translateString;
switch (stream.Type) {
case 'Audio':
case 'Data':
case 'Subtitle':
case 'Video':
translateString = stream.Type;
break;
case 'EmbeddedImage':
translateString = 'Image';
break;
}
const displayType = globalize.translate(translateString);
html += `\n<h2 class="mediaInfoStreamType">${displayType}${copyButtonHtml}</h2>\n`;
const attributes = [];
if (stream.DisplayTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoTitle'), stream.DisplayTitle));
}
if (stream.Language && stream.Type !== 'Video') {
attributes.push(createAttribute(globalize.translate('MediaInfoLanguage'), stream.Language));
}
if (stream.Codec) {
attributes.push(createAttribute(globalize.translate('MediaInfoCodec'), stream.Codec.toUpperCase()));
}
if (stream.CodecTag) {
attributes.push(createAttribute(globalize.translate('MediaInfoCodecTag'), stream.CodecTag));
}
if (stream.IsAVC != null) {
attributes.push(createAttribute('AVC', (stream.IsAVC ? 'Yes' : 'No')));
}
if (stream.Profile) {
attributes.push(createAttribute(globalize.translate('MediaInfoProfile'), stream.Profile));
}
if (stream.Level > 0) {
attributes.push(createAttribute(globalize.translate('MediaInfoLevel'), stream.Level));
}
if (stream.Width || stream.Height) {
attributes.push(createAttribute(globalize.translate('MediaInfoResolution'), `${stream.Width}x${stream.Height}`));
}
if (stream.AspectRatio && stream.Codec !== 'mjpeg') {
attributes.push(createAttribute(globalize.translate('MediaInfoAspectRatio'), stream.AspectRatio));
}
if (stream.Type === 'Video') {
if (stream.IsAnamorphic != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoAnamorphic'), (stream.IsAnamorphic ? 'Yes' : 'No')));
}
attributes.push(createAttribute(globalize.translate('MediaInfoInterlaced'), (stream.IsInterlaced ? 'Yes' : 'No')));
}
if ((stream.AverageFrameRate || stream.RealFrameRate) && stream.Type === 'Video') {
attributes.push(createAttribute(globalize.translate('MediaInfoFramerate'), (stream.AverageFrameRate || stream.RealFrameRate)));
}
if (stream.ChannelLayout) {
attributes.push(createAttribute(globalize.translate('MediaInfoLayout'), stream.ChannelLayout));
}
if (stream.Channels) {
attributes.push(createAttribute(globalize.translate('MediaInfoChannels'), `${stream.Channels} ch`));
}
if (stream.BitRate) {
attributes.push(createAttribute(globalize.translate('MediaInfoBitrate'), `${parseInt(stream.BitRate / 1000, 10)} kbps`));
}
if (stream.SampleRate) {
attributes.push(createAttribute(globalize.translate('MediaInfoSampleRate'), `${stream.SampleRate} Hz`));
}
if (stream.BitDepth) {
attributes.push(createAttribute(globalize.translate('MediaInfoBitDepth'), `${stream.BitDepth} bit`));
}
if (stream.VideoRange) {
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRange'), stream.VideoRange));
}
if (stream.VideoRangeType) {
attributes.push(createAttribute(globalize.translate('MediaInfoVideoRangeType'), stream.VideoRangeType));
}
if (stream.VideoDoViTitle) {
attributes.push(createAttribute(globalize.translate('MediaInfoDoViTitle'), stream.VideoDoViTitle));
if (stream.DvVersionMajor != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMajor'), stream.DvVersionMajor));
}
if (stream.DvVersionMinor != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvVersionMinor'), stream.DvVersionMinor));
}
if (stream.DvProfile != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvProfile'), stream.DvProfile));
}
if (stream.DvLevel != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvLevel'), stream.DvLevel));
}
if (stream.RpuPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoRpuPresentFlag'), stream.RpuPresentFlag));
}
if (stream.ElPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoElPresentFlag'), stream.ElPresentFlag));
}
if (stream.BlPresentFlag != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoBlPresentFlag'), stream.BlPresentFlag));
}
if (stream.DvBlSignalCompatibilityId != null) {
attributes.push(createAttribute(globalize.translate('MediaInfoDvBlSignalCompatibilityId'), stream.DvBlSignalCompatibilityId));
}
}
if (stream.ColorSpace) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorSpace'), stream.ColorSpace));
}
if (stream.ColorTransfer) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorTransfer'), stream.ColorTransfer));
}
if (stream.ColorPrimaries) {
attributes.push(createAttribute(globalize.translate('MediaInfoColorPrimaries'), stream.ColorPrimaries));
}
if (stream.PixelFormat) {
attributes.push(createAttribute(globalize.translate('MediaInfoPixelFormat'), stream.PixelFormat));
}
if (stream.RefFrames) {
attributes.push(createAttribute(globalize.translate('MediaInfoRefFrames'), stream.RefFrames));
}
if (stream.NalLengthSize) {
attributes.push(createAttribute('NAL', stream.NalLengthSize));
}
if (stream.Type === 'Subtitle' || stream.Type === 'Audio') {
attributes.push(createAttribute(globalize.translate('MediaInfoDefault'), (stream.IsDefault ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoForced'), (stream.IsForced ? 'Yes' : 'No')));
attributes.push(createAttribute(globalize.translate('MediaInfoExternal'), (stream.IsExternal ? 'Yes' : 'No')));
}
if (stream.Type === 'Video' && version.Timestamp) {
attributes.push(createAttribute(globalize.translate('MediaInfoTimestamp'), version.Timestamp));
}
html += attributes.join('<br/>');
html += '</div>';
}
html += '</div>';
return html;
}
// File Paths should be always ltr. The isLtr parameter allows this.
function createAttribute(label, value, isLtr) {
return `<span class="mediaInfoLabel">${label}</span>${attributeDelimiterHtml}<span class="mediaInfoAttribute" ${isLtr && 'dir="ltr"'}>${escapeHtml(value)}</span>\n`;
}
function loadMediaInfo(itemId, serverId) {
const apiClient = ServerConnections.getApiClient(serverId);
return apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
const dialogOptions = {
size: 'small',
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
if (layoutManager.tv) {
dlg.querySelector('.formDialogContent');
}
dialogHelper.open(dlg);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
apiClient.getCurrentUser().then(user => {
setMediaInfo(user, dlg, item);
});
loading.hide();
});
}
export function show(itemId, serverId) {
loading.show();
return loadMediaInfo(itemId, serverId);
}
/* eslint-enable indent */
export default {
show: show
};

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for itemidentifier media item.
@ -24,388 +23,314 @@ import toast from '../toast/toast';
import template from './itemidentifier.template.html';
import datetime from '../../scripts/datetime';
const enableFocusTransform = !browser.slow && !browser.edge;
const enableFocusTransform = !browser.slow && !browser.edge;
let currentItem;
let currentItemType;
let currentServerId;
let currentResolve;
let currentReject;
let hasChanges = false;
let currentSearchResult;
let currentItem;
let currentItemType;
let currentServerId;
let currentResolve;
let currentReject;
let hasChanges = false;
let currentSearchResult;
function getApiClient() {
return ServerConnections.getApiClient(currentServerId);
function getApiClient() {
return ServerConnections.getApiClient(currentServerId);
}
function searchForIdentificationResults(page) {
let lookupInfo = {
ProviderIds: {}
};
let i;
let length;
const identifyField = page.querySelectorAll('.identifyField');
let value;
for (i = 0, length = identifyField.length; i < length; i++) {
value = identifyField[i].value;
if (value) {
if (identifyField[i].type === 'number') {
value = parseInt(value, 10);
}
lookupInfo[identifyField[i].getAttribute('data-lookup')] = value;
}
}
function searchForIdentificationResults(page) {
let lookupInfo = {
ProviderIds: {}
};
let hasId = false;
let i;
let length;
const identifyField = page.querySelectorAll('.identifyField');
let value;
for (i = 0, length = identifyField.length; i < length; i++) {
value = identifyField[i].value;
const txtLookupId = page.querySelectorAll('.txtLookupId');
for (i = 0, length = txtLookupId.length; i < length; i++) {
value = txtLookupId[i].value;
if (value) {
if (identifyField[i].type === 'number') {
value = parseInt(value, 10);
}
lookupInfo[identifyField[i].getAttribute('data-lookup')] = value;
}
if (value) {
hasId = true;
}
lookupInfo.ProviderIds[txtLookupId[i].getAttribute('data-providerkey')] = value;
}
let hasId = false;
if (!hasId && !lookupInfo.Name) {
toast(globalize.translate('PleaseEnterNameOrId'));
return;
}
const txtLookupId = page.querySelectorAll('.txtLookupId');
for (i = 0, length = txtLookupId.length; i < length; i++) {
value = txtLookupId[i].value;
lookupInfo = {
SearchInfo: lookupInfo
};
if (value) {
hasId = true;
}
lookupInfo.ProviderIds[txtLookupId[i].getAttribute('data-providerkey')] = value;
}
if (currentItem && currentItem.Id) {
lookupInfo.ItemId = currentItem.Id;
} else {
lookupInfo.IncludeDisabledProviders = true;
}
if (!hasId && !lookupInfo.Name) {
toast(globalize.translate('PleaseEnterNameOrId'));
return;
}
loading.show();
lookupInfo = {
SearchInfo: lookupInfo
};
const apiClient = getApiClient();
if (currentItem && currentItem.Id) {
lookupInfo.ItemId = currentItem.Id;
apiClient.ajax({
type: 'POST',
url: apiClient.getUrl(`Items/RemoteSearch/${currentItemType}`),
data: JSON.stringify(lookupInfo),
contentType: 'application/json',
dataType: 'json'
}).then(results => {
loading.hide();
showIdentificationSearchResults(page, results);
});
}
function showIdentificationSearchResults(page, results) {
const identificationSearchResults = page.querySelector('.identificationSearchResults');
page.querySelector('.popupIdentifyForm').classList.add('hide');
identificationSearchResults.classList.remove('hide');
page.querySelector('.identifyOptionsForm').classList.add('hide');
page.querySelector('.dialogContentInner').classList.remove('dialog-content-centered');
let html = '';
let i;
let length;
for (i = 0, length = results.length; i < length; i++) {
const result = results[i];
html += getSearchResultHtml(result, i);
}
const elem = page.querySelector('.identificationSearchResultList');
elem.innerHTML = html;
function onSearchImageClick() {
const index = parseInt(this.getAttribute('data-index'), 10);
const currentResult = results[index];
if (currentItem != null) {
showIdentifyOptions(page, currentResult);
} else {
lookupInfo.IncludeDisabledProviders = true;
}
loading.show();
const apiClient = getApiClient();
apiClient.ajax({
type: 'POST',
url: apiClient.getUrl(`Items/RemoteSearch/${currentItemType}`),
data: JSON.stringify(lookupInfo),
contentType: 'application/json',
dataType: 'json'
}).then(results => {
loading.hide();
showIdentificationSearchResults(page, results);
});
}
function showIdentificationSearchResults(page, results) {
const identificationSearchResults = page.querySelector('.identificationSearchResults');
page.querySelector('.popupIdentifyForm').classList.add('hide');
identificationSearchResults.classList.remove('hide');
page.querySelector('.identifyOptionsForm').classList.add('hide');
page.querySelector('.dialogContentInner').classList.remove('dialog-content-centered');
let html = '';
let i;
let length;
for (i = 0, length = results.length; i < length; i++) {
const result = results[i];
html += getSearchResultHtml(result, i);
}
const elem = page.querySelector('.identificationSearchResultList');
elem.innerHTML = html;
function onSearchImageClick() {
const index = parseInt(this.getAttribute('data-index'), 10);
const currentResult = results[index];
if (currentItem != null) {
showIdentifyOptions(page, currentResult);
} else {
finishFindNewDialog(page, currentResult);
}
}
const searchImages = elem.querySelectorAll('.card');
for (i = 0, length = searchImages.length; i < length; i++) {
searchImages[i].addEventListener('click', onSearchImageClick);
}
if (layoutManager.tv) {
focusManager.autoFocus(identificationSearchResults);
finishFindNewDialog(page, currentResult);
}
}
function finishFindNewDialog(dlg, identifyResult) {
currentSearchResult = identifyResult;
const searchImages = elem.querySelectorAll('.card');
for (i = 0, length = searchImages.length; i < length; i++) {
searchImages[i].addEventListener('click', onSearchImageClick);
}
if (layoutManager.tv) {
focusManager.autoFocus(identificationSearchResults);
}
}
function finishFindNewDialog(dlg, identifyResult) {
currentSearchResult = identifyResult;
hasChanges = true;
loading.hide();
dialogHelper.close(dlg);
}
function showIdentifyOptions(page, identifyResult) {
const identifyOptionsForm = page.querySelector('.identifyOptionsForm');
page.querySelector('.popupIdentifyForm').classList.add('hide');
page.querySelector('.identificationSearchResults').classList.add('hide');
identifyOptionsForm.classList.remove('hide');
page.querySelector('#chkIdentifyReplaceImages').checked = true;
page.querySelector('.dialogContentInner').classList.add('dialog-content-centered');
currentSearchResult = identifyResult;
const lines = [];
lines.push(escapeHtml(identifyResult.Name));
if (identifyResult.ProductionYear) {
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, { useGrouping: false }));
}
let resultHtml = lines.join('<br/>');
if (identifyResult.ImageUrl) {
resultHtml = `<div style="display:flex;align-items:center;"><img src="${identifyResult.ImageUrl}" style="max-height:240px;" /><div style="margin-left:1em;">${resultHtml}</div>`;
}
page.querySelector('.selectedSearchResult').innerHTML = resultHtml;
focusManager.focus(identifyOptionsForm.querySelector('.btnSubmit'));
}
function getSearchResultHtml(result, index) {
// TODO move card creation code to Card component
let html = '';
let cssClass = 'card scalableCard';
let cardBoxCssClass = 'cardBox';
let padderClass;
if (currentItemType === 'Episode') {
cssClass += ' backdropCard backdropCard-scalable';
padderClass = 'cardPadder-backdrop';
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
cssClass += ' squareCard squareCard-scalable';
padderClass = 'cardPadder-square';
} else {
cssClass += ' portraitCard portraitCard-scalable';
padderClass = 'cardPadder-portrait';
}
if (layoutManager.tv) {
cssClass += ' show-focus';
if (enableFocusTransform) {
cssClass += ' show-animation';
}
}
cardBoxCssClass += ' cardBox-bottompadded';
html += `<button type="button" class="${cssClass}" data-index="${index}">`;
html += `<div class="${cardBoxCssClass}">`;
html += '<div class="cardScalable">';
html += `<div class="${padderClass}"></div>`;
html += '<div class="cardContent searchImage">';
if (result.ImageUrl) {
html += `<div class="cardImageContainer coveredImage" style="background-image:url('${result.ImageUrl}');"></div>`;
} else {
html += `<div class="cardImageContainer coveredImage defaultCardBackground defaultCardBackground1"><div class="cardText cardCenteredText">${escapeHtml(result.Name)}</div></div>`;
}
html += '</div>';
html += '</div>';
let numLines = 3;
if (currentItemType === 'MusicAlbum') {
numLines++;
}
const lines = [result.Name];
lines.push(result.SearchProviderName);
if (result.AlbumArtist) {
lines.push(result.AlbumArtist.Name);
}
if (result.ProductionYear) {
lines.push(result.ProductionYear);
}
for (let i = 0; i < numLines; i++) {
if (i === 0) {
html += '<div class="cardText cardText-first cardTextCentered">';
} else {
html += '<div class="cardText cardText-secondary cardTextCentered">';
}
html += escapeHtml(lines[i] || '') || '&nbsp;';
html += '</div>';
}
html += '</div>';
html += '</button>';
return html;
}
function submitIdentficationResult(page) {
loading.show();
const options = {
ReplaceAllImages: page.querySelector('#chkIdentifyReplaceImages').checked
};
const apiClient = getApiClient();
apiClient.ajax({
type: 'POST',
url: apiClient.getUrl(`Items/RemoteSearch/Apply/${currentItem.Id}`, options),
data: JSON.stringify(currentSearchResult),
contentType: 'application/json'
}).then(() => {
hasChanges = true;
loading.hide();
dialogHelper.close(dlg);
}
dialogHelper.close(page);
}, () => {
loading.hide();
function showIdentifyOptions(page, identifyResult) {
const identifyOptionsForm = page.querySelector('.identifyOptionsForm');
dialogHelper.close(page);
});
}
page.querySelector('.popupIdentifyForm').classList.add('hide');
page.querySelector('.identificationSearchResults').classList.add('hide');
identifyOptionsForm.classList.remove('hide');
page.querySelector('#chkIdentifyReplaceImages').checked = true;
page.querySelector('.dialogContentInner').classList.add('dialog-content-centered');
currentSearchResult = identifyResult;
const lines = [];
lines.push(escapeHtml(identifyResult.Name));
if (identifyResult.ProductionYear) {
lines.push(datetime.toLocaleString(identifyResult.ProductionYear, { useGrouping: false }));
}
let resultHtml = lines.join('<br/>');
if (identifyResult.ImageUrl) {
resultHtml = `<div style="display:flex;align-items:center;"><img src="${identifyResult.ImageUrl}" style="max-height:240px;" /><div style="margin-left:1em;">${resultHtml}</div>`;
}
page.querySelector('.selectedSearchResult').innerHTML = resultHtml;
focusManager.focus(identifyOptionsForm.querySelector('.btnSubmit'));
}
function getSearchResultHtml(result, index) {
// TODO move card creation code to Card component
function showIdentificationForm(page, item) {
const apiClient = getApiClient();
apiClient.getJSON(apiClient.getUrl(`Items/${item.Id}/ExternalIdInfos`)).then(idList => {
let html = '';
let cssClass = 'card scalableCard';
let cardBoxCssClass = 'cardBox';
let padderClass;
if (currentItemType === 'Episode') {
cssClass += ' backdropCard backdropCard-scalable';
padderClass = 'cardPadder-backdrop';
} else if (currentItemType === 'MusicAlbum' || currentItemType === 'MusicArtist') {
cssClass += ' squareCard squareCard-scalable';
padderClass = 'cardPadder-square';
} else {
cssClass += ' portraitCard portraitCard-scalable';
padderClass = 'cardPadder-portrait';
}
for (let i = 0, length = idList.length; i < length; i++) {
const idInfo = idList[i];
if (layoutManager.tv) {
cssClass += ' show-focus';
const id = `txtLookup${idInfo.Key}`;
if (enableFocusTransform) {
cssClass += ' show-animation';
html += '<div class="inputContainer">';
let fullName = idInfo.Name;
if (idInfo.Type) {
fullName = `${idInfo.Name} ${globalize.translate(idInfo.Type)}`;
}
}
cardBoxCssClass += ' cardBox-bottompadded';
const idLabel = globalize.translate('LabelDynamicExternalId', escapeHtml(fullName));
html += `<button type="button" class="${cssClass}" data-index="${index}">`;
html += `<div class="${cardBoxCssClass}">`;
html += '<div class="cardScalable">';
html += `<div class="${padderClass}"></div>`;
html += `<input is="emby-input" class="txtLookupId" data-providerkey="${idInfo.Key}" id="${id}" label="${idLabel}"/>`;
html += '<div class="cardContent searchImage">';
if (result.ImageUrl) {
html += `<div class="cardImageContainer coveredImage" style="background-image:url('${result.ImageUrl}');"></div>`;
} else {
html += `<div class="cardImageContainer coveredImage defaultCardBackground defaultCardBackground1"><div class="cardText cardCenteredText">${escapeHtml(result.Name)}</div></div>`;
}
html += '</div>';
html += '</div>';
let numLines = 3;
if (currentItemType === 'MusicAlbum') {
numLines++;
}
const lines = [result.Name];
lines.push(result.SearchProviderName);
if (result.AlbumArtist) {
lines.push(result.AlbumArtist.Name);
}
if (result.ProductionYear) {
lines.push(result.ProductionYear);
}
for (let i = 0; i < numLines; i++) {
if (i === 0) {
html += '<div class="cardText cardText-first cardTextCentered">';
} else {
html += '<div class="cardText cardText-secondary cardTextCentered">';
}
html += escapeHtml(lines[i] || '') || '&nbsp;';
html += '</div>';
}
html += '</div>';
html += '</button>';
return html;
}
page.querySelector('#txtLookupName').value = '';
function submitIdentficationResult(page) {
loading.show();
const options = {
ReplaceAllImages: page.querySelector('#chkIdentifyReplaceImages').checked
};
const apiClient = getApiClient();
apiClient.ajax({
type: 'POST',
url: apiClient.getUrl(`Items/RemoteSearch/Apply/${currentItem.Id}`, options),
data: JSON.stringify(currentSearchResult),
contentType: 'application/json'
}).then(() => {
hasChanges = true;
loading.hide();
dialogHelper.close(page);
}, () => {
loading.hide();
dialogHelper.close(page);
});
}
function showIdentificationForm(page, item) {
const apiClient = getApiClient();
apiClient.getJSON(apiClient.getUrl(`Items/${item.Id}/ExternalIdInfos`)).then(idList => {
let html = '';
for (let i = 0, length = idList.length; i < length; i++) {
const idInfo = idList[i];
const id = `txtLookup${idInfo.Key}`;
html += '<div class="inputContainer">';
let fullName = idInfo.Name;
if (idInfo.Type) {
fullName = `${idInfo.Name} ${globalize.translate(idInfo.Type)}`;
}
const idLabel = globalize.translate('LabelDynamicExternalId', escapeHtml(fullName));
html += `<input is="emby-input" class="txtLookupId" data-providerkey="${idInfo.Key}" id="${id}" label="${idLabel}"/>`;
html += '</div>';
}
page.querySelector('#txtLookupName').value = '';
if (item.Type === 'Person' || item.Type === 'BoxSet') {
page.querySelector('.fldLookupYear').classList.add('hide');
page.querySelector('#txtLookupYear').value = '';
} else {
page.querySelector('.fldLookupYear').classList.remove('hide');
page.querySelector('#txtLookupYear').value = '';
}
page.querySelector('.identifyProviderIds').innerHTML = html;
page.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Identify');
});
}
function showEditor(itemId) {
loading.show();
const apiClient = getApiClient();
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
currentItem = item;
currentItemType = currentItem.Type;
const dialogOptions = {
size: 'small',
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
dlg.classList.add('recordingDialog');
let html = '';
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed);
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
}
if (item.Path) {
dlg.querySelector('.fldPath').classList.remove('hide');
} else {
dlg.querySelector('.fldPath').classList.add('hide');
}
dlg.querySelector('.txtPath').innerText = item.Path || '';
dialogHelper.open(dlg);
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
e.preventDefault();
searchForIdentificationResults(dlg);
return false;
});
dlg.querySelector('.identifyOptionsForm').addEventListener('submit', e => {
e.preventDefault();
submitIdentficationResult(dlg);
return false;
});
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.classList.add('identifyDialog');
showIdentificationForm(dlg, item);
loading.hide();
});
}
function onDialogClosed() {
loading.hide();
if (hasChanges) {
currentResolve();
if (item.Type === 'Person' || item.Type === 'BoxSet') {
page.querySelector('.fldLookupYear').classList.add('hide');
page.querySelector('#txtLookupYear').value = '';
} else {
currentReject();
page.querySelector('.fldLookupYear').classList.remove('hide');
page.querySelector('#txtLookupYear').value = '';
}
}
// TODO investigate where this was used
function showEditorFindNew(itemName, itemYear, itemType, resolveFunc) {
currentItem = null;
currentItemType = itemType;
page.querySelector('.identifyProviderIds').innerHTML = html;
page.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Identify');
});
}
function showEditor(itemId) {
loading.show();
const apiClient = getApiClient();
apiClient.getItem(apiClient.getCurrentUserId(), itemId).then(item => {
currentItem = item;
currentItemType = currentItem.Type;
const dialogOptions = {
size: 'small',
@ -427,15 +352,22 @@ import datetime from '../../scripts/datetime';
dlg.innerHTML = html;
// Has to be assigned a z-index after the call to .open()
dlg.addEventListener('close', onDialogClosed);
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
}
dialogHelper.open(dlg);
if (item.Path) {
dlg.querySelector('.fldPath').classList.remove('hide');
} else {
dlg.querySelector('.fldPath').classList.add('hide');
}
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.querySelector('.txtPath').innerText = item.Path || '';
dialogHelper.open(dlg);
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
e.preventDefault();
@ -443,53 +375,119 @@ import datetime from '../../scripts/datetime';
return false;
});
dlg.addEventListener('close', () => {
loading.hide();
const foundItem = hasChanges ? currentSearchResult : null;
dlg.querySelector('.identifyOptionsForm').addEventListener('submit', e => {
e.preventDefault();
submitIdentficationResult(dlg);
return false;
});
resolveFunc(foundItem);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.classList.add('identifyDialog');
showIdentificationFormFindNew(dlg, itemName, itemYear, itemType);
showIdentificationForm(dlg, item);
loading.hide();
});
}
function onDialogClosed() {
loading.hide();
if (hasChanges) {
currentResolve();
} else {
currentReject();
}
}
// TODO investigate where this was used
function showEditorFindNew(itemName, itemYear, itemType, resolveFunc) {
currentItem = null;
currentItemType = itemType;
const dialogOptions = {
size: 'small',
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
}
function showIdentificationFormFindNew(dlg, itemName, itemYear, itemType) {
dlg.querySelector('#txtLookupName').value = itemName;
const dlg = dialogHelper.createDialog(dialogOptions);
if (itemType === 'Person' || itemType === 'BoxSet') {
dlg.querySelector('.fldLookupYear').classList.add('hide');
dlg.querySelector('#txtLookupYear').value = '';
} else {
dlg.querySelector('.fldLookupYear').classList.remove('hide');
dlg.querySelector('#txtLookupYear').value = itemYear;
}
dlg.classList.add('formDialog');
dlg.classList.add('recordingDialog');
dlg.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Search');
let html = '';
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
}
export function show(itemId, serverId) {
return new Promise((resolve, reject) => {
currentResolve = resolve;
currentReject = reject;
currentServerId = serverId;
hasChanges = false;
dialogHelper.open(dlg);
showEditor(itemId);
});
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
dlg.querySelector('.popupIdentifyForm').addEventListener('submit', e => {
e.preventDefault();
searchForIdentificationResults(dlg);
return false;
});
dlg.addEventListener('close', () => {
loading.hide();
const foundItem = hasChanges ? currentSearchResult : null;
resolveFunc(foundItem);
});
dlg.classList.add('identifyDialog');
showIdentificationFormFindNew(dlg, itemName, itemYear, itemType);
}
function showIdentificationFormFindNew(dlg, itemName, itemYear, itemType) {
dlg.querySelector('#txtLookupName').value = itemName;
if (itemType === 'Person' || itemType === 'BoxSet') {
dlg.querySelector('.fldLookupYear').classList.add('hide');
dlg.querySelector('#txtLookupYear').value = '';
} else {
dlg.querySelector('.fldLookupYear').classList.remove('hide');
dlg.querySelector('#txtLookupYear').value = itemYear;
}
export function showFindNew(itemName, itemYear, itemType, serverId) {
return new Promise((resolve) => {
currentServerId = serverId;
dlg.querySelector('.formDialogHeaderTitle').innerHTML = globalize.translate('Search');
}
hasChanges = false;
showEditorFindNew(itemName, itemYear, itemType, resolve);
});
}
export function show(itemId, serverId) {
return new Promise((resolve, reject) => {
currentResolve = resolve;
currentReject = reject;
currentServerId = serverId;
hasChanges = false;
showEditor(itemId);
});
}
export function showFindNew(itemName, itemYear, itemType, serverId) {
return new Promise((resolve) => {
currentServerId = serverId;
hasChanges = false;
showEditorFindNew(itemName, itemYear, itemType, resolve);
});
}
/* eslint-enable indent */
export default {
show: show,
showFindNew: showFindNew

View file

@ -1,69 +1,68 @@
/* eslint-disable indent */
export class LazyLoader {
constructor(options) {
this.options = options;
}
createObserver() {
const callback = this.options.callback;
export class LazyLoader {
constructor(options) {
this.options = options;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
callback(entry);
});
},
{
rootMargin: '50%',
threshold: 0
createObserver() {
const callback = this.options.callback;
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
callback(entry);
});
this.observer = observer;
}
addElements(elements) {
let observer = this.observer;
if (!observer) {
this.createObserver();
observer = this.observer;
}
Array.from(elements).forEach(element => {
observer.observe(element);
},
{
rootMargin: '50%',
threshold: 0
});
}
destroyObserver() {
const observer = this.observer;
if (observer) {
observer.disconnect();
this.observer = null;
}
}
destroy() {
this.destroyObserver();
this.options = null;
}
this.observer = observer;
}
function unveilElements(elements, root, callback) {
if (!elements.length) {
return;
addElements(elements) {
let observer = this.observer;
if (!observer) {
this.createObserver();
observer = this.observer;
}
const lazyLoader = new LazyLoader({
callback: callback
Array.from(elements).forEach(element => {
observer.observe(element);
});
lazyLoader.addElements(elements);
}
export function lazyChildren(elem, callback) {
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
destroyObserver() {
const observer = this.observer;
if (observer) {
observer.disconnect();
this.observer = null;
}
}
/* eslint-enable indent */
destroy() {
this.destroyObserver();
this.options = null;
}
}
function unveilElements(elements, root, callback) {
if (!elements.length) {
return;
}
const lazyLoader = new LazyLoader({
callback: callback
});
lazyLoader.addElements(elements);
}
export function lazyChildren(elem, callback) {
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
}
export default {
LazyLoader: LazyLoader,
lazyChildren: lazyChildren

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for display list view.
@ -18,484 +17,483 @@ import '../../elements/emby-ratingbutton/emby-ratingbutton';
import '../../elements/emby-playstatebutton/emby-playstatebutton';
import ServerConnections from '../ServerConnections';
function getIndex(item, options) {
if (options.index === 'disc') {
return item.ParentIndexNumber == null ? '' : globalize.translate('ValueDiscNumber', item.ParentIndexNumber);
}
const sortBy = (options.sortBy || '').toLowerCase();
let code;
let name;
if (sortBy.indexOf('sortname') === 0) {
if (item.Type === 'Episode') {
return '';
}
// SortName
name = (item.SortName || item.Name || '?')[0].toUpperCase();
code = name.charCodeAt(0);
if (code < 65 || code > 90) {
return '#';
}
return name.toUpperCase();
}
if (sortBy.indexOf('officialrating') === 0) {
return item.OfficialRating || globalize.translate('Unrated');
}
if (sortBy.indexOf('communityrating') === 0) {
if (item.CommunityRating == null) {
return globalize.translate('Unrated');
}
return Math.floor(item.CommunityRating);
}
if (sortBy.indexOf('criticrating') === 0) {
if (item.CriticRating == null) {
return globalize.translate('Unrated');
}
return Math.floor(item.CriticRating);
}
if (sortBy.indexOf('albumartist') === 0) {
// SortName
if (!item.AlbumArtist) {
return '';
}
name = item.AlbumArtist[0].toUpperCase();
code = name.charCodeAt(0);
if (code < 65 || code > 90) {
return '#';
}
return name.toUpperCase();
}
return '';
function getIndex(item, options) {
if (options.index === 'disc') {
return item.ParentIndexNumber == null ? '' : globalize.translate('ValueDiscNumber', item.ParentIndexNumber);
}
function getImageUrl(item, size) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
let itemId;
const sortBy = (options.sortBy || '').toLowerCase();
let code;
let name;
const options = {
fillWidth: size,
fillHeight: size,
type: 'Primary'
};
if (item.ImageTags && item.ImageTags.Primary) {
options.tag = item.ImageTags.Primary;
itemId = item.Id;
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
itemId = item.AlbumId;
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag;
itemId = item.SeriesId;
} else if (item.ParentPrimaryImageTag) {
options.tag = item.ParentPrimaryImageTag;
itemId = item.ParentPrimaryImageItemId;
if (sortBy.indexOf('sortname') === 0) {
if (item.Type === 'Episode') {
return '';
}
if (itemId) {
return apiClient.getScaledImageUrl(itemId, options);
// SortName
name = (item.SortName || item.Name || '?')[0].toUpperCase();
code = name.charCodeAt(0);
if (code < 65 || code > 90) {
return '#';
}
return null;
return name.toUpperCase();
}
if (sortBy.indexOf('officialrating') === 0) {
return item.OfficialRating || globalize.translate('Unrated');
}
if (sortBy.indexOf('communityrating') === 0) {
if (item.CommunityRating == null) {
return globalize.translate('Unrated');
}
return Math.floor(item.CommunityRating);
}
if (sortBy.indexOf('criticrating') === 0) {
if (item.CriticRating == null) {
return globalize.translate('Unrated');
}
return Math.floor(item.CriticRating);
}
if (sortBy.indexOf('albumartist') === 0) {
// SortName
if (!item.AlbumArtist) {
return '';
}
name = item.AlbumArtist[0].toUpperCase();
code = name.charCodeAt(0);
if (code < 65 || code > 90) {
return '#';
}
return name.toUpperCase();
}
return '';
}
function getImageUrl(item, size) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
let itemId;
const options = {
fillWidth: size,
fillHeight: size,
type: 'Primary'
};
if (item.ImageTags && item.ImageTags.Primary) {
options.tag = item.ImageTags.Primary;
itemId = item.Id;
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
itemId = item.AlbumId;
} else if (item.SeriesId && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag;
itemId = item.SeriesId;
} else if (item.ParentPrimaryImageTag) {
options.tag = item.ParentPrimaryImageTag;
itemId = item.ParentPrimaryImageItemId;
}
function getChannelImageUrl(item, size) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
const options = {
fillWidth: size,
fillHeight: size,
type: 'Primary'
};
if (itemId) {
return apiClient.getScaledImageUrl(itemId, options);
}
return null;
}
if (item.ChannelId && item.ChannelPrimaryImageTag) {
options.tag = item.ChannelPrimaryImageTag;
}
function getChannelImageUrl(item, size) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
const options = {
fillWidth: size,
fillHeight: size,
type: 'Primary'
};
if (item.ChannelId) {
return apiClient.getScaledImageUrl(item.ChannelId, options);
}
if (item.ChannelId && item.ChannelPrimaryImageTag) {
options.tag = item.ChannelPrimaryImageTag;
}
function getTextLinesHtml(textlines, isLargeStyle) {
let html = '';
if (item.ChannelId) {
return apiClient.getScaledImageUrl(item.ChannelId, options);
}
}
const largeTitleTagName = layoutManager.tv ? 'h2' : 'div';
function getTextLinesHtml(textlines, isLargeStyle) {
let html = '';
for (const [i, text] of textlines.entries()) {
if (!text) {
continue;
}
const largeTitleTagName = layoutManager.tv ? 'h2' : 'div';
let elem;
for (const [i, text] of textlines.entries()) {
if (!text) {
continue;
}
if (i === 0) {
if (isLargeStyle) {
elem = document.createElement(largeTitleTagName);
} else {
elem = document.createElement('div');
}
let elem;
if (i === 0) {
if (isLargeStyle) {
elem = document.createElement(largeTitleTagName);
} else {
elem = document.createElement('div');
elem.classList.add('secondary');
}
elem.classList.add('listItemBodyText');
elem.innerHTML = '<bdi>' + escapeHtml(text) + '</bdi>';
html += elem.outerHTML;
} else {
elem = document.createElement('div');
elem.classList.add('secondary');
}
return html;
elem.classList.add('listItemBodyText');
elem.innerHTML = '<bdi>' + escapeHtml(text) + '</bdi>';
html += elem.outerHTML;
}
function getRightButtonsHtml(options) {
return html;
}
function getRightButtonsHtml(options) {
let html = '';
for (let i = 0, length = options.rightButtons.length; i < length; i++) {
const button = options.rightButtons[i];
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
}
return html;
}
export function getListViewHtml(options) {
const items = options.items;
let groupTitle = '';
const action = options.action || 'link';
const isLargeStyle = options.imageSize === 'large';
const enableOverview = options.enableOverview;
const clickEntireItem = layoutManager.tv ? true : false;
const outerTagName = clickEntireItem ? 'button' : 'div';
const enableSideMediaInfo = options.enableSideMediaInfo != null ? options.enableSideMediaInfo : true;
let outerHtml = '';
const enableContentWrapper = options.enableOverview && !layoutManager.tv;
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
let html = '';
for (let i = 0, length = options.rightButtons.length; i < length; i++) {
const button = options.rightButtons[i];
if (options.showIndex) {
const itemGroupTitle = getIndex(item, options);
html += `<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="custom" data-customaction="${button.id}" title="${button.title}"><span class="material-icons ${button.icon}" aria-hidden="true"></span></button>`;
if (itemGroupTitle !== groupTitle) {
if (html) {
html += '</div>';
}
if (i === 0) {
html += '<h2 class="listGroupHeader listGroupHeader-first">';
} else {
html += '<h2 class="listGroupHeader">';
}
html += escapeHtml(itemGroupTitle);
html += '</h2>';
html += '<div>';
groupTitle = itemGroupTitle;
}
}
return html;
}
let cssClass = 'listItem';
export function getListViewHtml(options) {
const items = options.items;
if (options.border || (options.highlight !== false && !layoutManager.tv)) {
cssClass += ' listItem-border';
}
let groupTitle = '';
const action = options.action || 'link';
if (clickEntireItem) {
cssClass += ' itemAction listItem-button';
}
const isLargeStyle = options.imageSize === 'large';
const enableOverview = options.enableOverview;
if (layoutManager.tv) {
cssClass += ' listItem-focusscale';
}
const clickEntireItem = layoutManager.tv ? true : false;
const outerTagName = clickEntireItem ? 'button' : 'div';
const enableSideMediaInfo = options.enableSideMediaInfo != null ? options.enableSideMediaInfo : true;
let downloadWidth = 80;
let outerHtml = '';
if (isLargeStyle) {
cssClass += ' listItem-largeImage';
downloadWidth = 500;
}
const enableContentWrapper = options.enableOverview && !layoutManager.tv;
const playlistItemId = item.PlaylistItemId ? (` data-playlistitemid="${item.PlaylistItemId}"`) : '';
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i];
const positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (` data-positionticks="${item.UserData.PlaybackPositionTicks}"`) : '';
const collectionIdData = options.collectionId ? (` data-collectionid="${options.collectionId}"`) : '';
const playlistIdData = options.playlistId ? (` data-playlistid="${options.playlistId}"`) : '';
const mediaTypeData = item.MediaType ? (` data-mediatype="${item.MediaType}"`) : '';
const collectionTypeData = item.CollectionType ? (` data-collectiontype="${item.CollectionType}"`) : '';
const channelIdData = item.ChannelId ? (` data-channelid="${item.ChannelId}"`) : '';
let html = '';
if (enableContentWrapper) {
cssClass += ' listItem-withContentWrapper';
}
if (options.showIndex) {
const itemGroupTitle = getIndex(item, options);
html += `<${outerTagName} class="${cssClass}"${playlistItemId} data-action="${action}" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}"${mediaTypeData}${collectionTypeData}${channelIdData}${positionTicksData}${collectionIdData}${playlistIdData}>`;
if (itemGroupTitle !== groupTitle) {
if (html) {
html += '</div>';
}
if (enableContentWrapper) {
html += '<div class="listItem-content">';
}
if (i === 0) {
html += '<h2 class="listGroupHeader listGroupHeader-first">';
} else {
html += '<h2 class="listGroupHeader">';
}
html += escapeHtml(itemGroupTitle);
html += '</h2>';
if (!clickEntireItem && options.dragHandle) {
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
}
html += '<div>';
if (options.image !== false) {
const imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
groupTitle = itemGroupTitle;
}
if (options.imageSource === 'channel') {
imageClass += ' listItemImage-channel';
}
let cssClass = 'listItem';
if (options.border || (options.highlight !== false && !layoutManager.tv)) {
cssClass += ' listItem-border';
if (isLargeStyle && layoutManager.tv) {
imageClass += ' listItemImage-large-tv';
}
if (clickEntireItem) {
cssClass += ' itemAction listItem-button';
const playOnImageClick = options.imagePlayButton && !layoutManager.tv;
if (!clickEntireItem) {
imageClass += ' itemAction';
}
if (layoutManager.tv) {
cssClass += ' listItem-focusscale';
const imageAction = playOnImageClick ? 'link' : action;
if (imgUrl) {
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
} else {
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
}
let downloadWidth = 80;
if (isLargeStyle) {
cssClass += ' listItem-largeImage';
downloadWidth = 500;
const mediaSourceCount = item.MediaSourceCount || 1;
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
}
const playlistItemId = item.PlaylistItemId ? (` data-playlistitemid="${item.PlaylistItemId}"`) : '';
let indicatorsHtml = '';
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
const positionTicksData = item.UserData && item.UserData.PlaybackPositionTicks ? (` data-positionticks="${item.UserData.PlaybackPositionTicks}"`) : '';
const collectionIdData = options.collectionId ? (` data-collectionid="${options.collectionId}"`) : '';
const playlistIdData = options.playlistId ? (` data-playlistid="${options.playlistId}"`) : '';
const mediaTypeData = item.MediaType ? (` data-mediatype="${item.MediaType}"`) : '';
const collectionTypeData = item.CollectionType ? (` data-collectiontype="${item.CollectionType}"`) : '';
const channelIdData = item.ChannelId ? (` data-channelid="${item.ChannelId}"`) : '';
if (enableContentWrapper) {
cssClass += ' listItem-withContentWrapper';
if (indicatorsHtml) {
html += `<div class="indicators listItemIndicators">${indicatorsHtml}</div>`;
}
html += `<${outerTagName} class="${cssClass}"${playlistItemId} data-action="${action}" data-isfolder="${item.IsFolder}" data-id="${item.Id}" data-serverid="${item.ServerId}" data-type="${item.Type}"${mediaTypeData}${collectionTypeData}${channelIdData}${positionTicksData}${collectionIdData}${playlistIdData}>`;
if (enableContentWrapper) {
html += '<div class="listItem-content">';
if (playOnImageClick) {
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
}
if (!clickEntireItem && options.dragHandle) {
html += '<span class="listViewDragHandle material-icons listItemIcon listItemIcon-transparent drag_handle" aria-hidden="true"></span>';
}
if (options.image !== false) {
const imgUrl = options.imageSource === 'channel' ? getChannelImageUrl(item, downloadWidth) : getImageUrl(item, downloadWidth);
let imageClass = isLargeStyle ? 'listItemImage listItemImage-large' : 'listItemImage';
if (options.imageSource === 'channel') {
imageClass += ' listItemImage-channel';
}
if (isLargeStyle && layoutManager.tv) {
imageClass += ' listItemImage-large-tv';
}
const playOnImageClick = options.imagePlayButton && !layoutManager.tv;
if (!clickEntireItem) {
imageClass += ' itemAction';
}
const imageAction = playOnImageClick ? 'link' : action;
if (imgUrl) {
html += '<div data-action="' + imageAction + '" class="' + imageClass + ' lazy" data-src="' + imgUrl + '" item-icon>';
} else {
html += '<div class="' + imageClass + ' cardImageContainer ' + cardBuilder.getDefaultBackgroundClass(item.Name) + '">' + cardBuilder.getDefaultText(item, options);
}
const mediaSourceCount = item.MediaSourceCount || 1;
if (mediaSourceCount > 1 && options.disableIndicators !== true) {
html += '<div class="mediaSourceIndicator">' + mediaSourceCount + '</div>';
}
let indicatorsHtml = '';
indicatorsHtml += indicators.getPlayedIndicatorHtml(item);
if (indicatorsHtml) {
html += `<div class="indicators listItemIndicators">${indicatorsHtml}</div>`;
}
if (playOnImageClick) {
html += '<button is="paper-icon-button-light" class="listItemImageButton itemAction" data-action="resume"><span class="material-icons listItemImageButton-icon play_arrow" aria-hidden="true"></span></button>';
}
const progressHtml = indicators.getProgressBarHtml(item, {
containerClass: 'listItemProgressBar'
});
if (progressHtml) {
html += progressHtml;
}
html += '</div>';
}
if (options.showIndexNumberLeft) {
html += '<div class="listItem-indexnumberleft">';
html += (item.IndexNumber || '&nbsp;');
html += '</div>';
}
const textlines = [];
if (options.showProgramDateTime) {
textlines.push(datetime.toLocaleString(datetime.parseISO8601Date(item.StartDate), {
weekday: 'long',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
}));
}
if (options.showProgramTime) {
textlines.push(datetime.getDisplayTime(datetime.parseISO8601Date(item.StartDate)));
}
if (options.showChannel && item.ChannelName) {
textlines.push(item.ChannelName);
}
let parentTitle = null;
if (options.showParentTitle) {
if (item.Type === 'Episode') {
parentTitle = item.SeriesName;
} else if (item.IsSeries || (item.EpisodeTitle && item.Name)) {
parentTitle = item.Name;
}
}
let displayName = itemHelper.getDisplayName(item, {
includeParentInfo: options.includeParentInfoInTitle
const progressHtml = indicators.getProgressBarHtml(item, {
containerClass: 'listItemProgressBar'
});
if (options.showIndexNumber && item.IndexNumber != null) {
displayName = `${item.IndexNumber}. ${displayName}`;
if (progressHtml) {
html += progressHtml;
}
html += '</div>';
}
if (options.showIndexNumberLeft) {
html += '<div class="listItem-indexnumberleft">';
html += (item.IndexNumber || '&nbsp;');
html += '</div>';
}
const textlines = [];
if (options.showProgramDateTime) {
textlines.push(datetime.toLocaleString(datetime.parseISO8601Date(item.StartDate), {
weekday: 'long',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
}));
}
if (options.showProgramTime) {
textlines.push(datetime.getDisplayTime(datetime.parseISO8601Date(item.StartDate)));
}
if (options.showChannel && item.ChannelName) {
textlines.push(item.ChannelName);
}
let parentTitle = null;
if (options.showParentTitle) {
if (item.Type === 'Episode') {
parentTitle = item.SeriesName;
} else if (item.IsSeries || (item.EpisodeTitle && item.Name)) {
parentTitle = item.Name;
}
}
let displayName = itemHelper.getDisplayName(item, {
includeParentInfo: options.includeParentInfoInTitle
});
if (options.showIndexNumber && item.IndexNumber != null) {
displayName = `${item.IndexNumber}. ${displayName}`;
}
if (options.showParentTitle && options.parentTitleWithTitle) {
if (displayName) {
if (parentTitle) {
parentTitle += ' - ';
}
parentTitle = (parentTitle || '') + displayName;
}
if (options.showParentTitle && options.parentTitleWithTitle) {
if (displayName) {
if (parentTitle) {
parentTitle += ' - ';
}
parentTitle = (parentTitle || '') + displayName;
textlines.push(parentTitle || '');
} else if (options.showParentTitle) {
textlines.push(parentTitle || '');
}
if (displayName && !options.parentTitleWithTitle) {
textlines.push(displayName);
}
if (item.IsFolder) {
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
textlines.push(item.AlbumArtist);
}
} else {
if (options.artist) {
const artistItems = item.ArtistItems;
if (artistItems && item.Type !== 'MusicAlbum') {
textlines.push(artistItems.map(a => {
return a.Name;
}).join(', '));
}
}
}
if (item.Type === 'TvChannel' && item.CurrentProgram) {
textlines.push(itemHelper.getDisplayName(item.CurrentProgram));
}
cssClass = 'listItemBody';
if (!clickEntireItem) {
cssClass += ' itemAction';
}
if (options.image === false) {
cssClass += ' listItemBody-noleftpadding';
}
html += `<div class="${cssClass}">`;
html += getTextLinesHtml(textlines, isLargeStyle);
if (options.mediaInfo !== false && !enableSideMediaInfo) {
const mediaInfoClass = 'secondary listItemMediaInfo listItemBodyText';
html += `<div class="${mediaInfoClass}">`;
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
episodeTitle: false,
originalAirDate: false,
subtitles: false
});
html += '</div>';
}
if (enableOverview && item.Overview) {
html += '<div class="secondary listItem-overview listItemBodyText">';
html += '<bdi>' + item.Overview + '</bdi>';
html += '</div>';
}
html += '</div>';
if (options.mediaInfo !== false && enableSideMediaInfo) {
html += '<div class="secondary listItemMediaInfo">';
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
year: false,
container: false,
episodeTitle: false,
criticRating: false,
officialRating: false,
endsAt: false
});
html += '</div>';
}
if (!options.recordButton && (item.Type === 'Timer' || item.Type === 'Program')) {
html += indicators.getTimerIndicator(item).replace('indicatorIcon', 'indicatorIcon listItemAside');
}
html += '<div class="listViewUserDataButtons">';
if (!clickEntireItem) {
if (options.addToListButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
}
if (options.infoButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
}
if (options.rightButtons) {
html += getRightButtonsHtml(options);
}
if (options.enableUserDataButtons !== false) {
const userData = item.UserData || {};
const likes = userData.Likes == null ? '' : userData.Likes;
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
}
textlines.push(parentTitle || '');
} else if (options.showParentTitle) {
textlines.push(parentTitle || '');
}
if (displayName && !options.parentTitleWithTitle) {
textlines.push(displayName);
}
if (item.IsFolder) {
if (options.artist !== false && item.AlbumArtist && item.Type === 'MusicAlbum') {
textlines.push(item.AlbumArtist);
}
} else {
if (options.artist) {
const artistItems = item.ArtistItems;
if (artistItems && item.Type !== 'MusicAlbum') {
textlines.push(artistItems.map(a => {
return a.Name;
}).join(', '));
}
if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
}
}
if (item.Type === 'TvChannel' && item.CurrentProgram) {
textlines.push(itemHelper.getDisplayName(item.CurrentProgram));
if (options.moreButton !== false) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
}
}
html += '</div>';
cssClass = 'listItemBody';
if (!clickEntireItem) {
cssClass += ' itemAction';
}
if (options.image === false) {
cssClass += ' listItemBody-noleftpadding';
}
html += `<div class="${cssClass}">`;
html += getTextLinesHtml(textlines, isLargeStyle);
if (options.mediaInfo !== false && !enableSideMediaInfo) {
const mediaInfoClass = 'secondary listItemMediaInfo listItemBodyText';
html += `<div class="${mediaInfoClass}">`;
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
episodeTitle: false,
originalAirDate: false,
subtitles: false
});
html += '</div>';
}
if (enableContentWrapper) {
html += '</div>';
if (enableOverview && item.Overview) {
html += '<div class="secondary listItem-overview listItemBodyText">';
html += '<div class="listItem-bottomoverview secondary">';
html += '<bdi>' + item.Overview + '</bdi>';
html += '</div>';
}
html += '</div>';
if (options.mediaInfo !== false && enableSideMediaInfo) {
html += '<div class="secondary listItemMediaInfo">';
html += mediaInfo.getPrimaryMediaInfoHtml(item, {
year: false,
container: false,
episodeTitle: false,
criticRating: false,
officialRating: false,
endsAt: false
});
html += '</div>';
}
if (!options.recordButton && (item.Type === 'Timer' || item.Type === 'Program')) {
html += indicators.getTimerIndicator(item).replace('indicatorIcon', 'indicatorIcon listItemAside');
}
html += '<div class="listViewUserDataButtons">';
if (!clickEntireItem) {
if (options.addToListButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="addtoplaylist"><span class="material-icons playlist_add" aria-hidden="true"></span></button>';
}
if (options.infoButton) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="link"><span class="material-icons info_outline" aria-hidden="true"></span></button>';
}
if (options.rightButtons) {
html += getRightButtonsHtml(options);
}
if (options.enableUserDataButtons !== false) {
const userData = item.UserData || {};
const likes = userData.Likes == null ? '' : userData.Likes;
if (itemHelper.canMarkPlayed(item) && options.enablePlayedButton !== false) {
html += '<button is="emby-playstatebutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-played="' + (userData.Played) + '"><span class="material-icons check" aria-hidden="true"></span></button>';
}
if (itemHelper.canRate(item) && options.enableRatingButton !== false) {
html += '<button is="emby-ratingbutton" type="button" class="listItemButton paper-icon-button-light" data-id="' + item.Id + '" data-serverid="' + item.ServerId + '" data-itemtype="' + item.Type + '" data-likes="' + likes + '" data-isfavorite="' + (userData.IsFavorite) + '"><span class="material-icons favorite" aria-hidden="true"></span></button>';
}
}
if (options.moreButton !== false) {
html += '<button is="paper-icon-button-light" class="listItemButton itemAction" data-action="menu"><span class="material-icons more_vert" aria-hidden="true"></span></button>';
}
}
html += '</div>';
if (enableContentWrapper) {
html += '</div>';
if (enableOverview && item.Overview) {
html += '<div class="listItem-bottomoverview secondary">';
html += '<bdi>' + item.Overview + '</bdi>';
html += '</div>';
}
}
html += `</${outerTagName}>`;
outerHtml += html;
}
return outerHtml;
html += `</${outerTagName}>`;
outerHtml += html;
}
/* eslint-enable indent */
return outerHtml;
}
export default {
getListViewHtml: getListViewHtml
};

View file

@ -4,200 +4,196 @@ import Events from '../utils/events.ts';
import '../elements/emby-tabs/emby-tabs';
import '../elements/emby-button/emby-button';
/* eslint-disable indent */
let tabOwnerView;
const queryScope = document.querySelector('.skinHeader');
let headerTabsContainer;
let tabsElem;
let tabOwnerView;
const queryScope = document.querySelector('.skinHeader');
let headerTabsContainer;
let tabsElem;
function ensureElements() {
if (!headerTabsContainer) {
headerTabsContainer = queryScope.querySelector('.headerTabs');
}
function ensureElements() {
if (!headerTabsContainer) {
headerTabsContainer = queryScope.querySelector('.headerTabs');
}
}
function onViewTabsReady() {
this.selectedIndex(this.readySelectedIndex);
this.readySelectedIndex = null;
}
function onViewTabsReady() {
this.selectedIndex(this.readySelectedIndex);
this.readySelectedIndex = null;
}
function allowSwipe(target) {
function allowSwipeOn(elem) {
if (dom.parentWithTag(elem, 'input')) {
return false;
}
const classList = elem.classList;
if (classList) {
return !classList.contains('scrollX') && !classList.contains('animatedScrollX');
}
return true;
function allowSwipe(target) {
function allowSwipeOn(elem) {
if (dom.parentWithTag(elem, 'input')) {
return false;
}
let parent = target;
while (parent != null) {
if (!allowSwipeOn(parent)) {
return false;
}
parent = parent.parentNode;
const classList = elem.classList;
if (classList) {
return !classList.contains('scrollX') && !classList.contains('animatedScrollX');
}
return true;
}
function configureSwipeTabs(view, currentElement) {
if (!browser.touch) {
return;
let parent = target;
while (parent != null) {
if (!allowSwipeOn(parent)) {
return false;
}
// implement without hammer
const onSwipeLeft = function (e, target) {
if (allowSwipe(target) && view.contains(target)) {
currentElement.selectNext();
}
};
const onSwipeRight = function (e, target) {
if (allowSwipe(target) && view.contains(target)) {
currentElement.selectPrevious();
}
};
import('../scripts/touchHelper').then(({ default: TouchHelper }) => {
const touchHelper = new TouchHelper(view.parentNode.parentNode);
Events.on(touchHelper, 'swipeleft', onSwipeLeft);
Events.on(touchHelper, 'swiperight', onSwipeRight);
view.addEventListener('viewdestroy', function () {
touchHelper.destroy();
});
});
parent = parent.parentNode;
}
export function setTabs(view, selectedIndex, getTabsFn, getTabContainersFn, onBeforeTabChange, onTabChange, setSelectedIndex) {
ensureElements();
return true;
}
if (!view) {
if (tabOwnerView) {
document.body.classList.remove('withSectionTabs');
function configureSwipeTabs(view, currentElement) {
if (!browser.touch) {
return;
}
headerTabsContainer.innerHTML = '';
headerTabsContainer.classList.add('hide');
tabOwnerView = null;
}
return {
tabsContainer: headerTabsContainer,
replaced: false
};
// implement without hammer
const onSwipeLeft = function (e, target) {
if (allowSwipe(target) && view.contains(target)) {
currentElement.selectNext();
}
};
const tabsContainerElem = headerTabsContainer;
if (!tabOwnerView) {
tabsContainerElem.classList.remove('hide');
const onSwipeRight = function (e, target) {
if (allowSwipe(target) && view.contains(target)) {
currentElement.selectPrevious();
}
};
if (tabOwnerView !== view) {
let index = 0;
import('../scripts/touchHelper').then(({ default: TouchHelper }) => {
const touchHelper = new TouchHelper(view.parentNode.parentNode);
const indexAttribute = selectedIndex == null ? '' : (' data-index="' + selectedIndex + '"');
const tabsHtml = '<div is="emby-tabs"' + indexAttribute + ' class="tabs-viewmenubar"><div class="emby-tabs-slider" style="white-space:nowrap;">' + getTabsFn().map(function (t) {
let tabClass = 'emby-tab-button';
Events.on(touchHelper, 'swipeleft', onSwipeLeft);
Events.on(touchHelper, 'swiperight', onSwipeRight);
if (t.enabled === false) {
tabClass += ' hide';
}
view.addEventListener('viewdestroy', function () {
touchHelper.destroy();
});
});
}
let tabHtml;
export function setTabs(view, selectedIndex, getTabsFn, getTabContainersFn, onBeforeTabChange, onTabChange, setSelectedIndex) {
ensureElements();
if (t.cssClass) {
tabClass += ' ' + t.cssClass;
}
if (!view) {
if (tabOwnerView) {
document.body.classList.remove('withSectionTabs');
if (t.href) {
tabHtml = '<a href="' + t.href + '" is="emby-linkbutton" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></a>';
} else {
tabHtml = '<button type="button" is="emby-button" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></button>';
}
headerTabsContainer.innerHTML = '';
headerTabsContainer.classList.add('hide');
index++;
return tabHtml;
}).join('') + '</div></div>';
tabsContainerElem.innerHTML = tabsHtml;
window.CustomElements.upgradeSubtree(tabsContainerElem);
document.body.classList.add('withSectionTabs');
tabOwnerView = view;
tabsElem = tabsContainerElem.querySelector('[is="emby-tabs"]');
configureSwipeTabs(view, tabsElem);
if (getTabContainersFn) {
tabsElem.addEventListener('beforetabchange', function (e) {
const tabContainers = getTabContainersFn();
if (e.detail.previousIndex != null) {
const previousPanel = tabContainers[e.detail.previousIndex];
if (previousPanel) {
previousPanel.classList.remove('is-active');
}
}
const newPanel = tabContainers[e.detail.selectedTabIndex];
if (newPanel) {
newPanel.classList.add('is-active');
}
});
}
if (onBeforeTabChange) {
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
}
if (onTabChange) {
tabsElem.addEventListener('tabchange', onTabChange);
}
if (setSelectedIndex !== false) {
if (tabsElem.selectedIndex) {
tabsElem.selectedIndex(selectedIndex);
} else {
tabsElem.readySelectedIndex = selectedIndex;
tabsElem.addEventListener('ready', onViewTabsReady);
}
}
return {
tabsContainer: tabsContainerElem,
tabs: tabsElem,
replaced: true
};
tabOwnerView = null;
}
tabsElem.selectedIndex(selectedIndex);
return {
tabsContainer: tabsContainerElem,
tabs: tabsElem,
tabsContainer: headerTabsContainer,
replaced: false
};
}
export function selectedTabIndex(index) {
if (index != null) {
tabsElem.selectedIndex(index);
} else {
tabsElem.triggerTabChange();
const tabsContainerElem = headerTabsContainer;
if (!tabOwnerView) {
tabsContainerElem.classList.remove('hide');
}
if (tabOwnerView !== view) {
let index = 0;
const indexAttribute = selectedIndex == null ? '' : (' data-index="' + selectedIndex + '"');
const tabsHtml = '<div is="emby-tabs"' + indexAttribute + ' class="tabs-viewmenubar"><div class="emby-tabs-slider" style="white-space:nowrap;">' + getTabsFn().map(function (t) {
let tabClass = 'emby-tab-button';
if (t.enabled === false) {
tabClass += ' hide';
}
let tabHtml;
if (t.cssClass) {
tabClass += ' ' + t.cssClass;
}
if (t.href) {
tabHtml = '<a href="' + t.href + '" is="emby-linkbutton" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></a>';
} else {
tabHtml = '<button type="button" is="emby-button" class="' + tabClass + '" data-index="' + index + '"><div class="emby-button-foreground">' + t.name + '</div></button>';
}
index++;
return tabHtml;
}).join('') + '</div></div>';
tabsContainerElem.innerHTML = tabsHtml;
window.CustomElements.upgradeSubtree(tabsContainerElem);
document.body.classList.add('withSectionTabs');
tabOwnerView = view;
tabsElem = tabsContainerElem.querySelector('[is="emby-tabs"]');
configureSwipeTabs(view, tabsElem);
if (getTabContainersFn) {
tabsElem.addEventListener('beforetabchange', function (e) {
const tabContainers = getTabContainersFn();
if (e.detail.previousIndex != null) {
const previousPanel = tabContainers[e.detail.previousIndex];
if (previousPanel) {
previousPanel.classList.remove('is-active');
}
}
const newPanel = tabContainers[e.detail.selectedTabIndex];
if (newPanel) {
newPanel.classList.add('is-active');
}
});
}
if (onBeforeTabChange) {
tabsElem.addEventListener('beforetabchange', onBeforeTabChange);
}
if (onTabChange) {
tabsElem.addEventListener('tabchange', onTabChange);
}
if (setSelectedIndex !== false) {
if (tabsElem.selectedIndex) {
tabsElem.selectedIndex(selectedIndex);
} else {
tabsElem.readySelectedIndex = selectedIndex;
tabsElem.addEventListener('ready', onViewTabsReady);
}
}
return {
tabsContainer: tabsContainerElem,
tabs: tabsElem,
replaced: true
};
}
export function getTabsElement() {
return document.querySelector('.tabs-viewmenubar');
}
tabsElem.selectedIndex(selectedIndex);
/* eslint-enable indent */
return {
tabsContainer: tabsContainerElem,
tabs: tabsElem,
replaced: false
};
}
export function selectedTabIndex(index) {
if (index != null) {
tabsElem.selectedIndex(index);
} else {
tabsElem.triggerTabChange();
}
}
export function getTabsElement() {
return document.querySelector('.tabs-viewmenubar');
}

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for media library creator.
@ -25,167 +24,169 @@ import toast from '../toast/toast';
import alert from '../alert';
import template from './mediaLibraryCreator.template.html';
function onAddLibrary(e) {
if (isCreating) {
return false;
}
function onAddLibrary(e) {
if (isCreating) {
return false;
}
if (pathInfos.length == 0) {
alert({
text: globalize.translate('PleaseAddAtLeastOneFolder'),
type: 'error'
});
return false;
}
isCreating = true;
loading.show();
const dlg = dom.parentWithClass(this, 'dlg-librarycreator');
const name = $('#txtValue', dlg).val();
let type = $('#selectCollectionType', dlg).val();
if (type == 'mixed') {
type = null;
}
const libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
libraryOptions.PathInfos = pathInfos;
ApiClient.addVirtualFolder(name, type, currentOptions.refresh, libraryOptions).then(() => {
hasChanges = true;
isCreating = false;
loading.hide();
dialogHelper.close(dlg);
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
isCreating = false;
loading.hide();
if (pathInfos.length == 0) {
alert({
text: globalize.translate('PleaseAddAtLeastOneFolder'),
type: 'error'
});
e.preventDefault();
return false;
}
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
return collectionTypeOptions.map(i => {
return `<option value="${i.value}">${i.name}</option>`;
}).join('');
isCreating = true;
loading.show();
const dlg = dom.parentWithClass(this, 'dlg-librarycreator');
const name = $('#txtValue', dlg).val();
let type = $('#selectCollectionType', dlg).val();
if (type == 'mixed') {
type = null;
}
function initEditor(page, collectionTypeOptions) {
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
const value = this.value;
const dlg = $(this).parents('.dialog')[0];
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
const libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
libraryOptions.PathInfos = pathInfos;
ApiClient.addVirtualFolder(name, type, currentOptions.refresh, libraryOptions).then(() => {
hasChanges = true;
isCreating = false;
loading.hide();
dialogHelper.close(dlg);
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
if (value) {
dlg.querySelector('.libraryOptions').classList.remove('hide');
} else {
dlg.querySelector('.libraryOptions').classList.add('hide');
}
isCreating = false;
loading.hide();
});
e.preventDefault();
}
if (value != 'mixed') {
const index = this.selectedIndex;
function getCollectionTypeOptionsHtml(collectionTypeOptions) {
return collectionTypeOptions.map(i => {
return `<option value="${i.value}">${i.name}</option>`;
}).join('');
}
if (index != -1) {
const name = this.options[index].innerHTML.replace('*', '').replace('&amp;', '&');
$('#txtValue', dlg).val(name);
}
}
function initEditor(page, collectionTypeOptions) {
$('#selectCollectionType', page).html(getCollectionTypeOptionsHtml(collectionTypeOptions)).val('').on('change', function () {
const value = this.value;
const dlg = $(this).parents('.dialog')[0];
libraryoptionseditor.setContentType(dlg.querySelector('.libraryOptions'), value);
const folderOption = collectionTypeOptions.find(i => i.value === value);
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
});
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.addLibraryForm').addEventListener('submit', onAddLibrary);
page.querySelector('.folderList').addEventListener('click', onRemoveClick);
}
function onAddButtonClick() {
const page = dom.parentWithClass(this, 'dlg-librarycreator');
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
callback: function (path, networkSharePath) {
if (path) {
addMediaLocation(page, path, networkSharePath);
}
picker.close();
}
});
});
}
function getFolderHtml(pathInfo, index) {
let html = '';
html += '<div class="listItem listItem-border lnkPath">';
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
html += `<div class="listItemBodyText" dir="ltr">${escapeHtml(pathInfo.Path)}</div>`;
if (pathInfo.NetworkPath) {
html += `<div class="listItemBodyText secondary" dir="ltr">${escapeHtml(pathInfo.NetworkPath)}</div>`;
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}
function renderPaths(page) {
const foldersHtml = pathInfos.map(getFolderHtml).join('');
const folderList = page.querySelector('.folderList');
folderList.innerHTML = foldersHtml;
if (foldersHtml) {
folderList.classList.remove('hide');
if (value) {
dlg.querySelector('.libraryOptions').classList.remove('hide');
} else {
folderList.classList.add('hide');
dlg.querySelector('.libraryOptions').classList.add('hide');
}
}
function addMediaLocation(page, path, networkSharePath) {
const pathLower = path.toLowerCase();
const pathFilter = pathInfos.filter(p => {
return p.Path.toLowerCase() == pathLower;
});
if (value != 'mixed') {
const index = this.selectedIndex;
if (!pathFilter.length) {
const pathInfo = {
Path: path
};
if (networkSharePath) {
pathInfo.NetworkPath = networkSharePath;
if (index != -1) {
const name = this.options[index].innerHTML
.replaceAll('*', '')
.replaceAll('&amp;', '&');
$('#txtValue', dlg).val(name);
}
pathInfos.push(pathInfo);
renderPaths(page);
}
}
function onRemoveClick(e) {
const button = dom.parentWithClass(e.target, 'btnRemovePath');
const index = parseInt(button.getAttribute('data-index'), 10);
const location = pathInfos[index].Path;
const locationLower = location.toLowerCase();
pathInfos = pathInfos.filter(p => {
return p.Path.toLowerCase() != locationLower;
const folderOption = collectionTypeOptions.find(i => i.value === value);
$('.collectionTypeFieldDescription', dlg).html(folderOption?.message || '');
});
page.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
page.querySelector('.addLibraryForm').addEventListener('submit', onAddLibrary);
page.querySelector('.folderList').addEventListener('click', onRemoveClick);
}
function onAddButtonClick() {
const page = dom.parentWithClass(this, 'dlg-librarycreator');
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
callback: function (path, networkSharePath) {
if (path) {
addMediaLocation(page, path, networkSharePath);
}
picker.close();
}
});
renderPaths(dom.parentWithClass(button, 'dlg-librarycreator'));
});
}
function getFolderHtml(pathInfo, index) {
let html = '';
html += '<div class="listItem listItem-border lnkPath">';
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
html += `<div class="listItemBodyText" dir="ltr">${escapeHtml(pathInfo.Path)}</div>`;
if (pathInfo.NetworkPath) {
html += `<div class="listItemBodyText secondary" dir="ltr">${escapeHtml(pathInfo.NetworkPath)}</div>`;
}
function onDialogClosed() {
currentResolve(hasChanges);
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light"" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}
function initLibraryOptions(dlg) {
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions')).then(() => {
$('#selectCollectionType', dlg).trigger('change');
});
function renderPaths(page) {
const foldersHtml = pathInfos.map(getFolderHtml).join('');
const folderList = page.querySelector('.folderList');
folderList.innerHTML = foldersHtml;
if (foldersHtml) {
folderList.classList.remove('hide');
} else {
folderList.classList.add('hide');
}
}
function addMediaLocation(page, path, networkSharePath) {
const pathLower = path.toLowerCase();
const pathFilter = pathInfos.filter(p => {
return p.Path.toLowerCase() == pathLower;
});
if (!pathFilter.length) {
const pathInfo = {
Path: path
};
if (networkSharePath) {
pathInfo.NetworkPath = networkSharePath;
}
pathInfos.push(pathInfo);
renderPaths(page);
}
}
function onRemoveClick(e) {
const button = dom.parentWithClass(e.target, 'btnRemovePath');
const index = parseInt(button.getAttribute('data-index'), 10);
const location = pathInfos[index].Path;
const locationLower = location.toLowerCase();
pathInfos = pathInfos.filter(p => {
return p.Path.toLowerCase() != locationLower;
});
renderPaths(dom.parentWithClass(button, 'dlg-librarycreator'));
}
function onDialogClosed() {
currentResolve(hasChanges);
}
function initLibraryOptions(dlg) {
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions')).then(() => {
$('#selectCollectionType', dlg).trigger('change');
});
}
export class showEditor {
constructor(options) {
@ -217,11 +218,10 @@ export class showEditor {
}
}
let pathInfos = [];
let currentResolve;
let currentOptions;
let hasChanges = false;
let isCreating = false;
let pathInfos = [];
let currentResolve;
let currentOptions;
let hasChanges = false;
let isCreating = false;
/* eslint-enable indent */
export default showEditor;

View file

@ -1,4 +1,3 @@
/* eslint-disable indent */
/**
* Module for media library editor.
@ -23,180 +22,180 @@ import toast from '../toast/toast';
import confirm from '../confirm/confirm';
import template from './mediaLibraryEditor.template.html';
function onEditLibrary() {
if (isCreating) {
return false;
}
isCreating = true;
loading.show();
const dlg = dom.parentWithClass(this, 'dlg-libraryeditor');
let libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
libraryOptions = Object.assign(currentOptions.library.LibraryOptions || {}, libraryOptions);
ApiClient.updateVirtualFolderOptions(currentOptions.library.ItemId, libraryOptions).then(() => {
hasChanges = true;
isCreating = false;
loading.hide();
dialogHelper.close(dlg);
}, () => {
isCreating = false;
loading.hide();
});
function onEditLibrary() {
if (isCreating) {
return false;
}
function addMediaLocation(page, path, networkSharePath) {
const virtualFolder = currentOptions.library;
isCreating = true;
loading.show();
const dlg = dom.parentWithClass(this, 'dlg-libraryeditor');
let libraryOptions = libraryoptionseditor.getLibraryOptions(dlg.querySelector('.libraryOptions'));
libraryOptions = Object.assign(currentOptions.library.LibraryOptions || {}, libraryOptions);
ApiClient.updateVirtualFolderOptions(currentOptions.library.ItemId, libraryOptions).then(() => {
hasChanges = true;
isCreating = false;
loading.hide();
dialogHelper.close(dlg);
}, () => {
isCreating = false;
loading.hide();
});
return false;
}
function addMediaLocation(page, path, networkSharePath) {
const virtualFolder = currentOptions.library;
const refreshAfterChange = currentOptions.refresh;
ApiClient.addMediaPath(virtualFolder.Name, path, networkSharePath, refreshAfterChange).then(() => {
hasChanges = true;
refreshLibraryFromServer(page);
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
});
}
function updateMediaLocation(page, path, networkSharePath) {
const virtualFolder = currentOptions.library;
ApiClient.updateMediaPath(virtualFolder.Name, {
Path: path,
NetworkPath: networkSharePath
}).then(() => {
hasChanges = true;
refreshLibraryFromServer(page);
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
});
}
function onRemoveClick(btnRemovePath, location) {
const button = btnRemovePath;
const virtualFolder = currentOptions.library;
confirm({
title: globalize.translate('HeaderRemoveMediaLocation'),
text: globalize.translate('MessageConfirmRemoveMediaLocation'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(() => {
const refreshAfterChange = currentOptions.refresh;
ApiClient.addMediaPath(virtualFolder.Name, path, networkSharePath, refreshAfterChange).then(() => {
ApiClient.removeMediaPath(virtualFolder.Name, location, refreshAfterChange).then(() => {
hasChanges = true;
refreshLibraryFromServer(page);
refreshLibraryFromServer(dom.parentWithClass(button, 'dlg-libraryeditor'));
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
toast(globalize.translate('ErrorDefault'));
});
});
}
function onListItemClick(e) {
const listItem = dom.parentWithClass(e.target, 'listItem');
if (listItem) {
const index = parseInt(listItem.getAttribute('data-index'), 10);
const pathInfos = (currentOptions.library.LibraryOptions || {}).PathInfos || [];
const pathInfo = index == null ? {} : pathInfos[index] || {};
const originalPath = pathInfo.Path || (index == null ? null : currentOptions.library.Locations[index]);
const btnRemovePath = dom.parentWithClass(e.target, 'btnRemovePath');
if (btnRemovePath) {
onRemoveClick(btnRemovePath, originalPath);
return;
}
showDirectoryBrowser(dom.parentWithClass(listItem, 'dlg-libraryeditor'), originalPath, pathInfo.NetworkPath);
}
}
function getFolderHtml(pathInfo, index) {
let html = '';
html += `<div class="listItem listItem-border lnkPath" data-index="${index}">`;
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
html += '<h3 class="listItemBodyText">';
html += escapeHtml(pathInfo.Path);
html += '</h3>';
if (pathInfo.NetworkPath) {
html += `<div class="listItemBodyText secondary">${escapeHtml(pathInfo.NetworkPath)}</div>`;
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}
function refreshLibraryFromServer(page) {
ApiClient.getVirtualFolders().then(result => {
const library = result.filter(f => {
return f.Name === currentOptions.library.Name;
})[0];
if (library) {
currentOptions.library = library;
renderLibrary(page, currentOptions);
}
});
}
function renderLibrary(page, options) {
let pathInfos = (options.library.LibraryOptions || {}).PathInfos || [];
if (!pathInfos.length) {
pathInfos = options.library.Locations.map(p => {
return {
Path: p
};
});
}
function updateMediaLocation(page, path, networkSharePath) {
const virtualFolder = currentOptions.library;
ApiClient.updateMediaPath(virtualFolder.Name, {
Path: path,
NetworkPath: networkSharePath
}).then(() => {
hasChanges = true;
refreshLibraryFromServer(page);
}, () => {
toast(globalize.translate('ErrorAddingMediaPathToVirtualFolder'));
});
if (options.library.CollectionType === 'boxsets') {
page.querySelector('.folders').classList.add('hide');
} else {
page.querySelector('.folders').classList.remove('hide');
}
function onRemoveClick(btnRemovePath, location) {
const button = btnRemovePath;
const virtualFolder = currentOptions.library;
page.querySelector('.folderList').innerHTML = pathInfos.map(getFolderHtml).join('');
}
confirm({
title: globalize.translate('HeaderRemoveMediaLocation'),
text: globalize.translate('MessageConfirmRemoveMediaLocation'),
confirmText: globalize.translate('Delete'),
primary: 'delete'
}).then(() => {
const refreshAfterChange = currentOptions.refresh;
ApiClient.removeMediaPath(virtualFolder.Name, location, refreshAfterChange).then(() => {
hasChanges = true;
refreshLibraryFromServer(dom.parentWithClass(button, 'dlg-libraryeditor'));
}, () => {
toast(globalize.translate('ErrorDefault'));
});
});
}
function onAddButtonClick() {
showDirectoryBrowser(dom.parentWithClass(this, 'dlg-libraryeditor'));
}
function onListItemClick(e) {
const listItem = dom.parentWithClass(e.target, 'listItem');
if (listItem) {
const index = parseInt(listItem.getAttribute('data-index'), 10);
const pathInfos = (currentOptions.library.LibraryOptions || {}).PathInfos || [];
const pathInfo = index == null ? {} : pathInfos[index] || {};
const originalPath = pathInfo.Path || (index == null ? null : currentOptions.library.Locations[index]);
const btnRemovePath = dom.parentWithClass(e.target, 'btnRemovePath');
if (btnRemovePath) {
onRemoveClick(btnRemovePath, originalPath);
return;
}
showDirectoryBrowser(dom.parentWithClass(listItem, 'dlg-libraryeditor'), originalPath, pathInfo.NetworkPath);
}
}
function getFolderHtml(pathInfo, index) {
let html = '';
html += `<div class="listItem listItem-border lnkPath" data-index="${index}">`;
html += `<div class="${pathInfo.NetworkPath ? 'listItemBody two-line' : 'listItemBody'}">`;
html += '<h3 class="listItemBodyText">';
html += escapeHtml(pathInfo.Path);
html += '</h3>';
if (pathInfo.NetworkPath) {
html += `<div class="listItemBodyText secondary">${escapeHtml(pathInfo.NetworkPath)}</div>`;
}
html += '</div>';
html += `<button type="button" is="paper-icon-button-light" class="listItemButton btnRemovePath" data-index="${index}"><span class="material-icons remove_circle" aria-hidden="true"></span></button>`;
html += '</div>';
return html;
}
function refreshLibraryFromServer(page) {
ApiClient.getVirtualFolders().then(result => {
const library = result.filter(f => {
return f.Name === currentOptions.library.Name;
})[0];
if (library) {
currentOptions.library = library;
renderLibrary(page, currentOptions);
}
});
}
function renderLibrary(page, options) {
let pathInfos = (options.library.LibraryOptions || {}).PathInfos || [];
if (!pathInfos.length) {
pathInfos = options.library.Locations.map(p => {
return {
Path: p
};
});
}
if (options.library.CollectionType === 'boxsets') {
page.querySelector('.folders').classList.add('hide');
} else {
page.querySelector('.folders').classList.remove('hide');
}
page.querySelector('.folderList').innerHTML = pathInfos.map(getFolderHtml).join('');
}
function onAddButtonClick() {
showDirectoryBrowser(dom.parentWithClass(this, 'dlg-libraryeditor'));
}
function showDirectoryBrowser(context, originalPath, networkPath) {
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
pathReadOnly: originalPath != null,
path: originalPath,
networkSharePath: networkPath,
callback: function (path, networkSharePath) {
if (path) {
if (originalPath) {
updateMediaLocation(context, originalPath, networkSharePath);
} else {
addMediaLocation(context, path, networkSharePath);
}
function showDirectoryBrowser(context, originalPath, networkPath) {
import('../directorybrowser/directorybrowser').then(({ default: DirectoryBrowser }) => {
const picker = new DirectoryBrowser();
picker.show({
enableNetworkSharePath: true,
pathReadOnly: originalPath != null,
path: originalPath,
networkSharePath: networkPath,
callback: function (path, networkSharePath) {
if (path) {
if (originalPath) {
updateMediaLocation(context, originalPath, networkSharePath);
} else {
addMediaLocation(context, path, networkSharePath);
}
picker.close();
}
});
picker.close();
}
});
}
});
}
function initEditor(dlg, options) {
renderLibrary(dlg, options);
dlg.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
dlg.querySelector('.folderList').addEventListener('click', onListItemClick);
dlg.querySelector('.btnSubmit').addEventListener('click', onEditLibrary);
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions'), options.library.CollectionType, options.library.LibraryOptions);
}
function initEditor(dlg, options) {
renderLibrary(dlg, options);
dlg.querySelector('.btnAddFolder').addEventListener('click', onAddButtonClick);
dlg.querySelector('.folderList').addEventListener('click', onListItemClick);
dlg.querySelector('.btnSubmit').addEventListener('click', onEditLibrary);
libraryoptionseditor.embed(dlg.querySelector('.libraryOptions'), options.library.CollectionType, options.library.LibraryOptions);
}
function onDialogClosed() {
currentDeferred.resolveWith(null, [hasChanges]);
}
function onDialogClosed() {
currentDeferred.resolveWith(null, [hasChanges]);
}
export class showEditor {
constructor(options) {
@ -227,10 +226,9 @@ export class showEditor {
}
}
let currentDeferred;
let currentOptions;
let hasChanges = false;
let isCreating = false;
let currentDeferred;
let currentOptions;
let hasChanges = false;
let isCreating = false;
/* eslint-enable indent */
export default showEditor;

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -8,94 +8,91 @@ import '../../elements/emby-select/emby-select';
import '../formdialog.scss';
import template from './personEditor.template.html';
/* eslint-disable indent */
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
}
function show(person) {
return new Promise(function (resolve, reject) {
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
function show(person) {
return new Promise(function (resolve, reject) {
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
let submitted = false;
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
dlg.querySelector('.txtPersonName', dlg).value = person.Name || '';
dlg.querySelector('.selectPersonType', dlg).value = person.Type || '';
dlg.querySelector('.txtPersonRole', dlg).value = person.Role || '';
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
dialogHelper.open(dlg);
dlg.addEventListener('close', function () {
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (submitted) {
resolve(person);
} else {
dialogOptions.size = 'small';
reject();
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
let submitted = false;
html += globalize.translateHtml(template, 'core');
dlg.innerHTML = html;
dlg.querySelector('.txtPersonName', dlg).value = person.Name || '';
dlg.querySelector('.selectPersonType', dlg).value = person.Type || '';
dlg.querySelector('.txtPersonRole', dlg).value = person.Role || '';
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
dialogHelper.open(dlg);
dlg.addEventListener('close', function () {
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (submitted) {
resolve(person);
} else {
reject();
}
});
dlg.querySelector('.selectPersonType').addEventListener('change', function () {
if (this.value === 'Actor') {
dlg.querySelector('.fldRole').classList.remove('hide');
} else {
dlg.querySelector('.fldRole').classList.add('hide');
}
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
dlg.querySelector('form').addEventListener('submit', function (e) {
submitted = true;
person.Name = dlg.querySelector('.txtPersonName', dlg).value;
person.Type = dlg.querySelector('.selectPersonType', dlg).value;
person.Role = dlg.querySelector('.txtPersonRole', dlg).value || null;
dialogHelper.close(dlg);
e.preventDefault();
return false;
});
dlg.querySelector('.selectPersonType').dispatchEvent(new CustomEvent('change', {
bubbles: true
}));
});
}
dlg.querySelector('.selectPersonType').addEventListener('change', function () {
if (this.value === 'Actor') {
dlg.querySelector('.fldRole').classList.remove('hide');
} else {
dlg.querySelector('.fldRole').classList.add('hide');
}
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
dlg.querySelector('form').addEventListener('submit', function (e) {
submitted = true;
person.Name = dlg.querySelector('.txtPersonName', dlg).value;
person.Type = dlg.querySelector('.selectPersonType', dlg).value;
person.Role = dlg.querySelector('.txtPersonRole', dlg).value || null;
dialogHelper.close(dlg);
e.preventDefault();
return false;
});
dlg.querySelector('.selectPersonType').dispatchEvent(new CustomEvent('change', {
bubbles: true
}));
});
}
export default {
show: show
};
/* eslint-enable indent */

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ import serverNotifications from '../../scripts/serverNotifications';
import { playbackManager } from '../playback/playbackmanager';
import Events from '../../utils/events.ts';
import globalize from '../../scripts/globalize';
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
import NotificationIcon from './notificationicon.png';
@ -130,7 +131,7 @@ function onLibraryChanged(data, apiClient) {
newItems.length = 12;
}
apiClient.getItems(apiClient.getCurrentUserId(), {
getItems(apiClient, apiClient.getCurrentUserId(), {
Recursive: true,
Limit: 3,

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,17 @@
import appSettings from '../scripts/settings/appSettings';
import { pluginManager } from './pluginManager';
/* eslint-disable indent */
class PackageManager {
#packagesList = [];
#settingsKey = 'installedpackages1';
class PackageManager {
#packagesList = [];
#settingsKey = 'installedpackages1';
init() {
console.groupCollapsed('loading packages');
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
init() {
console.groupCollapsed('loading packages');
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
return Promise.all(manifestUrls.map((url) => {
return this.loadPackage(url);
}))
return Promise.all(manifestUrls.map((url) => {
return this.loadPackage(url);
}))
.then(() => {
console.debug('finished loading packages');
return Promise.resolve();
@ -22,119 +21,117 @@ import { pluginManager } from './pluginManager';
}).finally(() => {
console.groupEnd('loading packages');
});
}
get packages() {
return this.#packagesList.slice(0);
}
install(url) {
return this.loadPackage(url, true).then((pkg) => {
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
if (!manifestUrls.includes(url)) {
manifestUrls.push(url);
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
}
return pkg;
});
}
uninstall(name) {
const pkg = this.#packagesList.filter((p) => {
return p.name === name;
})[0];
if (pkg) {
this.#packagesList = this.#packagesList.filter((p) => {
return p.name !== name;
});
this.removeUrl(pkg.url);
}
return Promise.resolve();
}
mapPath(pkg, pluginUrl) {
const urlLower = pluginUrl.toLowerCase();
if (urlLower.startsWith('http:') || urlLower.startsWith('https:') || urlLower.startsWith('file:')) {
return pluginUrl;
}
let packageUrl = pkg.url;
packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/'));
packageUrl += '/';
packageUrl += pluginUrl;
return packageUrl;
}
addPackage(pkg) {
this.#packagesList = this.#packagesList.filter((p) => {
return p.name !== pkg.name;
});
this.#packagesList.push(pkg);
}
removeUrl(url) {
let manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
manifestUrls = manifestUrls.filter((i) => {
return i !== url;
});
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
}
loadPackage(url, throwError = false) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const originalUrl = url;
url += url.indexOf('?') === -1 ? '?' : '&';
url += 't=' + new Date().getTime();
xhr.open('GET', url, true);
const onError = () => {
if (throwError === true) {
reject();
} else {
this.removeUrl(originalUrl);
resolve();
}
};
xhr.onload = () => {
if (this.status < 400) {
const pkg = JSON.parse(this.response);
pkg.url = originalUrl;
this.addPackage(pkg);
const plugins = pkg.plugins || [];
if (pkg.plugin) {
plugins.push(pkg.plugin);
}
const promises = plugins.map((pluginUrl) => {
return pluginManager.loadPlugin(this.mapPath(pkg, pluginUrl));
});
Promise.all(promises).then(resolve, resolve);
} else {
onError();
}
};
xhr.onerror = onError;
xhr.send();
});
}
}
/* eslint-enable indent */
get packages() {
return this.#packagesList.slice(0);
}
install(url) {
return this.loadPackage(url, true).then((pkg) => {
const manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
if (!manifestUrls.includes(url)) {
manifestUrls.push(url);
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
}
return pkg;
});
}
uninstall(name) {
const pkg = this.#packagesList.filter((p) => {
return p.name === name;
})[0];
if (pkg) {
this.#packagesList = this.#packagesList.filter((p) => {
return p.name !== name;
});
this.removeUrl(pkg.url);
}
return Promise.resolve();
}
mapPath(pkg, pluginUrl) {
const urlLower = pluginUrl.toLowerCase();
if (urlLower.startsWith('http:') || urlLower.startsWith('https:') || urlLower.startsWith('file:')) {
return pluginUrl;
}
let packageUrl = pkg.url;
packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/'));
packageUrl += '/';
packageUrl += pluginUrl;
return packageUrl;
}
addPackage(pkg) {
this.#packagesList = this.#packagesList.filter((p) => {
return p.name !== pkg.name;
});
this.#packagesList.push(pkg);
}
removeUrl(url) {
let manifestUrls = JSON.parse(appSettings.get(this.#settingsKey) || '[]');
manifestUrls = manifestUrls.filter((i) => {
return i !== url;
});
appSettings.set(this.#settingsKey, JSON.stringify(manifestUrls));
}
loadPackage(url, throwError = false) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const originalUrl = url;
url += url.indexOf('?') === -1 ? '?' : '&';
url += 't=' + new Date().getTime();
xhr.open('GET', url, true);
const onError = () => {
if (throwError === true) {
reject();
} else {
this.removeUrl(originalUrl);
resolve();
}
};
xhr.onload = () => {
if (this.status < 400) {
const pkg = JSON.parse(this.response);
pkg.url = originalUrl;
this.addPackage(pkg);
const plugins = pkg.plugins || [];
if (pkg.plugin) {
plugins.push(pkg.plugin);
}
const promises = plugins.map((pluginUrl) => {
return pluginManager.loadPlugin(this.mapPath(pkg, pluginUrl));
});
Promise.all(promises).then(resolve, resolve);
} else {
onError();
}
};
xhr.onerror = onError;
xhr.send();
});
}
}
export default new PackageManager();

View file

@ -4,259 +4,256 @@ import Events from '../../utils/events.ts';
import ServerConnections from '../ServerConnections';
import shell from '../../scripts/shell';
/* eslint-disable indent */
// Reports media playback to the device for lock screen control
// Reports media playback to the device for lock screen control
let currentPlayer;
let currentPlayer;
function seriesImageUrl(item, options = {}) {
options.type = options.type || 'Primary';
function seriesImageUrl(item, options = {}) {
options.type = options.type || 'Primary';
if (item.Type !== 'Episode') {
return null;
} else if (options.type === 'Primary' && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag;
if (item.Type !== 'Episode') {
return null;
} else if (options.type === 'Primary' && item.SeriesPrimaryImageTag) {
options.tag = item.SeriesPrimaryImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
} else if (options.type === 'Thumb') {
if (item.SeriesThumbImageTag) {
options.tag = item.SeriesThumbImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
} else if (options.type === 'Thumb') {
if (item.SeriesThumbImageTag) {
options.tag = item.SeriesThumbImageTag;
} else if (item.ParentThumbImageTag) {
options.tag = item.ParentThumbImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.SeriesId, options);
} else if (item.ParentThumbImageTag) {
options.tag = item.ParentThumbImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options);
}
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.ParentThumbItemId, options);
}
}
return null;
}
function imageUrl(item, options = {}) {
options.type = options.type || 'Primary';
if (item.ImageTags && item.ImageTags[options.type]) {
options.tag = item.ImageTags[options.type];
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.Id, options);
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options);
}
return null;
}
function getImageUrl(item, imageOptions = {}) {
const url = seriesImageUrl(item, imageOptions) || imageUrl(item, imageOptions);
if (url) {
const height = imageOptions.height || imageOptions.maxHeight;
return {
src: url,
sizes: height + 'x' + height
};
} else {
return null;
}
}
function imageUrl(item, options = {}) {
options.type = options.type || 'Primary';
function getImageUrls(item, imageSizes = [96, 128, 192, 256, 384, 512]) {
const list = [];
if (item.ImageTags && item.ImageTags[options.type]) {
options.tag = item.ImageTags[options.type];
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.Id, options);
} else if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
return ServerConnections.getApiClient(item.ServerId).getScaledImageUrl(item.AlbumId, options);
imageSizes.forEach((size) => {
const url = getImageUrl(item, { height: size });
if (url !== null) {
list.push(url);
}
});
return null;
return list;
}
function updatePlayerState(player, state, eventName) {
// Don't go crazy reporting position changes
if (eventName === 'timeupdate') {
// Only report if this item hasn't been reported yet, or if there's an actual playback change.
// Don't report on simple time updates
return;
}
function getImageUrl(item, imageOptions = {}) {
const url = seriesImageUrl(item, imageOptions) || imageUrl(item, imageOptions);
const item = state.NowPlayingItem;
if (url) {
const height = imageOptions.height || imageOptions.maxHeight;
return {
src: url,
sizes: height + 'x' + height
};
} else {
return null;
}
}
function getImageUrls(item, imageSizes = [96, 128, 192, 256, 384, 512]) {
const list = [];
imageSizes.forEach((size) => {
const url = getImageUrl(item, { height: size });
if (url !== null) {
list.push(url);
}
});
return list;
}
function updatePlayerState(player, state, eventName) {
// Don't go crazy reporting position changes
if (eventName === 'timeupdate') {
// Only report if this item hasn't been reported yet, or if there's an actual playback change.
// Don't report on simple time updates
return;
}
const item = state.NowPlayingItem;
if (!item) {
hideMediaControls();
return;
}
if (eventName === 'init') { // transform "init" event into "timeupdate" to restraint update rate
eventName = 'timeupdate';
}
const isVideo = item.MediaType === 'Video';
const isLocalPlayer = player.isLocalPlayer || false;
// Local players do their own notifications
if (isLocalPlayer && isVideo) {
return;
}
const playState = state.PlayState || {};
const parts = nowPlayingHelper.getNowPlayingNames(item);
const artist = parts[parts.length - 1].text;
const title = parts.length === 1 ? '' : parts[0].text;
const album = item.Album || '';
const itemId = item.Id;
// Convert to ms
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
const currentTime = parseInt(playState.PositionTicks ? (playState.PositionTicks / 10000) : 0, 10);
const isPaused = playState.IsPaused || false;
const canSeek = playState.CanSeek || false;
if ('mediaSession' in navigator) {
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.metadata = new MediaMetadata({
title: title,
artist: artist,
album: album,
artwork: getImageUrls(item)
});
} else {
const itemImageUrl = seriesImageUrl(item, { maxHeight: 3000 }) || imageUrl(item, { maxHeight: 3000 });
shell.updateMediaSession({
action: eventName,
isLocalPlayer: isLocalPlayer,
itemId: itemId,
title: title,
artist: artist,
album: album,
duration: duration,
position: currentTime,
imageUrl: itemImageUrl,
canSeek: canSeek,
isPaused: isPaused
});
}
}
function onGeneralEvent(e) {
const state = playbackManager.getPlayerState(this);
updatePlayerState(this, state, e.type);
}
function onStateChanged(e, state) {
updatePlayerState(this, state, 'statechange');
}
function onPlaybackStart(e, state) {
updatePlayerState(this, state, e.type);
}
function onPlaybackStopped() {
if (!item) {
hideMediaControls();
return;
}
function releaseCurrentPlayer() {
if (currentPlayer) {
Events.off(currentPlayer, 'playbackstart', onPlaybackStart);
Events.off(currentPlayer, 'playbackstop', onPlaybackStopped);
Events.off(currentPlayer, 'unpause', onGeneralEvent);
Events.off(currentPlayer, 'pause', onGeneralEvent);
Events.off(currentPlayer, 'statechange', onStateChanged);
Events.off(currentPlayer, 'timeupdate', onGeneralEvent);
currentPlayer = null;
hideMediaControls();
}
if (eventName === 'init') { // transform "init" event into "timeupdate" to restraint update rate
eventName = 'timeupdate';
}
function hideMediaControls() {
if ('mediaSession' in navigator) {
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.metadata = null;
} else {
shell.hideMediaSession();
}
const isVideo = item.MediaType === 'Video';
const isLocalPlayer = player.isLocalPlayer || false;
// Local players do their own notifications
if (isLocalPlayer && isVideo) {
return;
}
function bindToPlayer(player) {
releaseCurrentPlayer();
const playState = state.PlayState || {};
const parts = nowPlayingHelper.getNowPlayingNames(item);
const artist = parts[parts.length - 1].text;
const title = parts.length === 1 ? '' : parts[0].text;
if (!player) {
return;
}
const album = item.Album || '';
const itemId = item.Id;
currentPlayer = player;
// Convert to ms
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
const currentTime = parseInt(playState.PositionTicks ? (playState.PositionTicks / 10000) : 0, 10);
const state = playbackManager.getPlayerState(player);
updatePlayerState(player, state, 'init');
Events.on(currentPlayer, 'playbackstart', onPlaybackStart);
Events.on(currentPlayer, 'playbackstop', onPlaybackStopped);
Events.on(currentPlayer, 'unpause', onGeneralEvent);
Events.on(currentPlayer, 'pause', onGeneralEvent);
Events.on(currentPlayer, 'statechange', onStateChanged);
Events.on(currentPlayer, 'timeupdate', onGeneralEvent);
}
function execute(name) {
playbackManager[name](currentPlayer);
}
const isPaused = playState.IsPaused || false;
const canSeek = playState.CanSeek || false;
if ('mediaSession' in navigator) {
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('previoustrack', function () {
execute('previousTrack');
navigator.mediaSession.metadata = new MediaMetadata({
title: title,
artist: artist,
album: album,
artwork: getImageUrls(item)
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('nexttrack', function () {
execute('nextTrack');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('play', function () {
execute('unpause');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('pause', function () {
execute('pause');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekbackward', function () {
execute('rewind');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekforward', function () {
execute('fastForward');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekto', function (object) {
const item = playbackManager.getPlayerState(currentPlayer).NowPlayingItem;
// Convert to ms
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
const wantedTime = object.seekTime * 1000;
playbackManager.seekPercent(wantedTime / duration * 100, currentPlayer);
} else {
const itemImageUrl = seriesImageUrl(item, { maxHeight: 3000 }) || imageUrl(item, { maxHeight: 3000 });
shell.updateMediaSession({
action: eventName,
isLocalPlayer: isLocalPlayer,
itemId: itemId,
title: title,
artist: artist,
album: album,
duration: duration,
position: currentTime,
imageUrl: itemImageUrl,
canSeek: canSeek,
isPaused: isPaused
});
}
}
Events.on(playbackManager, 'playerchange', function () {
bindToPlayer(playbackManager.getCurrentPlayer());
function onGeneralEvent(e) {
const state = playbackManager.getPlayerState(this);
updatePlayerState(this, state, e.type);
}
function onStateChanged(e, state) {
updatePlayerState(this, state, 'statechange');
}
function onPlaybackStart(e, state) {
updatePlayerState(this, state, e.type);
}
function onPlaybackStopped() {
hideMediaControls();
}
function releaseCurrentPlayer() {
if (currentPlayer) {
Events.off(currentPlayer, 'playbackstart', onPlaybackStart);
Events.off(currentPlayer, 'playbackstop', onPlaybackStopped);
Events.off(currentPlayer, 'unpause', onGeneralEvent);
Events.off(currentPlayer, 'pause', onGeneralEvent);
Events.off(currentPlayer, 'statechange', onStateChanged);
Events.off(currentPlayer, 'timeupdate', onGeneralEvent);
currentPlayer = null;
hideMediaControls();
}
}
function hideMediaControls() {
if ('mediaSession' in navigator) {
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.metadata = null;
} else {
shell.hideMediaSession();
}
}
function bindToPlayer(player) {
releaseCurrentPlayer();
if (!player) {
return;
}
currentPlayer = player;
const state = playbackManager.getPlayerState(player);
updatePlayerState(player, state, 'init');
Events.on(currentPlayer, 'playbackstart', onPlaybackStart);
Events.on(currentPlayer, 'playbackstop', onPlaybackStopped);
Events.on(currentPlayer, 'unpause', onGeneralEvent);
Events.on(currentPlayer, 'pause', onGeneralEvent);
Events.on(currentPlayer, 'statechange', onStateChanged);
Events.on(currentPlayer, 'timeupdate', onGeneralEvent);
}
function execute(name) {
playbackManager[name](currentPlayer);
}
if ('mediaSession' in navigator) {
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('previoustrack', function () {
execute('previousTrack');
});
bindToPlayer(playbackManager.getCurrentPlayer());
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('nexttrack', function () {
execute('nextTrack');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('play', function () {
execute('unpause');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('pause', function () {
execute('pause');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekbackward', function () {
execute('rewind');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekforward', function () {
execute('fastForward');
});
/* eslint-disable-next-line compat/compat */
navigator.mediaSession.setActionHandler('seekto', function (object) {
const item = playbackManager.getPlayerState(currentPlayer).NowPlayingItem;
// Convert to ms
const duration = parseInt(item.RunTimeTicks ? (item.RunTimeTicks / 10000) : 0, 10);
const wantedTime = object.seekTime * 1000;
playbackManager.seekPercent(wantedTime / duration * 100, currentPlayer);
});
}
Events.on(playbackManager, 'playerchange', function () {
bindToPlayer(playbackManager.getCurrentPlayer());
});
bindToPlayer(playbackManager.getCurrentPlayer());
/* eslint-enable indent */

View file

@ -13,6 +13,7 @@ import ServerConnections from '../ServerConnections';
import alert from '../alert';
import { PluginType } from '../../types/plugin.ts';
import { includesAny } from '../../utils/container.ts';
import { getItems } from '../../utils/jellyfin-apiclient/getItems.ts';
const UNLIMITED_ITEMS = -1;
@ -127,7 +128,7 @@ function getItemsForPlayback(serverId, query) {
query.EnableTotalRecordCount = false;
query.CollapseBoxSetItems = false;
return apiClient.getItems(apiClient.getCurrentUserId(), query);
return getItems(apiClient, apiClient.getCurrentUserId(), query);
}
}
@ -1038,7 +1039,6 @@ class PlaybackManager {
}
}
//var mediaType = item.MediaType;
return getPlayer(item, getDefaultPlayOptions()) != null;
};

View file

@ -12,102 +12,120 @@ import ServerConnections from '../ServerConnections';
import toast from '../toast/toast';
import template from './playbackSettings.template.html';
/* eslint-disable indent */
function fillSkipLengths(select) {
const options = [5, 10, 15, 20, 25, 30];
function fillSkipLengths(select) {
const options = [5, 10, 15, 20, 25, 30];
select.innerHTML = options.map(option => {
return {
name: globalize.translate('ValueSeconds', option),
value: option * 1000
};
}).map(o => {
return `<option value="${o.value}">${o.name}</option>`;
}).join('');
}
select.innerHTML = options.map(option => {
return {
name: globalize.translate('ValueSeconds', option),
value: option * 1000
};
}).map(o => {
return `<option value="${o.value}">${o.name}</option>`;
}).join('');
function populateLanguages(select, languages) {
let html = '';
html += `<option value=''>${globalize.translate('AnyLanguage')}</option>`;
for (let i = 0, length = languages.length; i < length; i++) {
const culture = languages[i];
html += `<option value='${culture.ThreeLetterISOLanguageName}'>${culture.DisplayName}</option>`;
}
function populateLanguages(select, languages) {
let html = '';
select.innerHTML = html;
}
html += `<option value=''>${globalize.translate('AnyLanguage')}</option>`;
function fillQuality(select, isInNetwork, mediatype, maxVideoWidth) {
const options = mediatype === 'Audio' ? qualityoptions.getAudioQualityOptions({
for (let i = 0, length = languages.length; i < length; i++) {
const culture = languages[i];
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
enableAuto: true
html += `<option value='${culture.ThreeLetterISOLanguageName}'>${culture.DisplayName}</option>`;
}
}) : qualityoptions.getVideoQualityOptions({
select.innerHTML = html;
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
enableAuto: true,
maxVideoWidth
});
select.innerHTML = options.map(i => {
// render empty string instead of 0 for the auto option
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
}).join('');
}
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
fillQuality(select, isInNetwork, mediatype);
if (appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype)) {
select.value = '';
} else {
select.value = appSettings.maxStreamingBitrate(isInNetwork, mediatype);
}
}
function fillChromecastQuality(select, maxVideoWidth) {
const options = qualityoptions.getVideoQualityOptions({
currentMaxBitrate: appSettings.maxChromecastBitrate(),
isAutomaticBitrateEnabled: !appSettings.maxChromecastBitrate(),
enableAuto: true,
maxVideoWidth
});
select.innerHTML = options.map(i => {
// render empty string instead of 0 for the auto option
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
}).join('');
select.value = appSettings.maxChromecastBitrate() || '';
}
function setMaxBitrateFromField(select, isInNetwork, mediatype) {
if (select.value) {
appSettings.maxStreamingBitrate(isInNetwork, mediatype, select.value);
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, false);
} else {
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, true);
}
}
function showHideQualityFields(context, user, apiClient) {
if (user.Policy.EnableVideoPlaybackTranscoding) {
context.querySelector('.videoQualitySection').classList.remove('hide');
} else {
context.querySelector('.videoQualitySection').classList.add('hide');
}
function fillQuality(select, isInNetwork, mediatype, maxVideoWidth) {
const options = mediatype === 'Audio' ? qualityoptions.getAudioQualityOptions({
if (appHost.supports('multiserver')) {
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
enableAuto: true
}) : qualityoptions.getVideoQualityOptions({
currentMaxBitrate: appSettings.maxStreamingBitrate(isInNetwork, mediatype),
isAutomaticBitrateEnabled: appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype),
enableAuto: true,
maxVideoWidth
});
select.innerHTML = options.map(i => {
// render empty string instead of 0 for the auto option
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
}).join('');
}
function setMaxBitrateIntoField(select, isInNetwork, mediatype) {
fillQuality(select, isInNetwork, mediatype);
if (appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype)) {
select.value = '';
if (user.Policy.EnableAudioPlaybackTranscoding) {
context.querySelector('.musicQualitySection').classList.remove('hide');
} else {
select.value = appSettings.maxStreamingBitrate(isInNetwork, mediatype);
}
}
function fillChromecastQuality(select, maxVideoWidth) {
const options = qualityoptions.getVideoQualityOptions({
currentMaxBitrate: appSettings.maxChromecastBitrate(),
isAutomaticBitrateEnabled: !appSettings.maxChromecastBitrate(),
enableAuto: true,
maxVideoWidth
});
select.innerHTML = options.map(i => {
// render empty string instead of 0 for the auto option
return `<option value="${i.bitrate || ''}">${i.name}</option>`;
}).join('');
select.value = appSettings.maxChromecastBitrate() || '';
}
function setMaxBitrateFromField(select, isInNetwork, mediatype) {
if (select.value) {
appSettings.maxStreamingBitrate(isInNetwork, mediatype, select.value);
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, false);
} else {
appSettings.enableAutomaticBitrateDetection(isInNetwork, mediatype, true);
}
}
function showHideQualityFields(context, user, apiClient) {
if (user.Policy.EnableVideoPlaybackTranscoding) {
context.querySelector('.videoQualitySection').classList.remove('hide');
} else {
context.querySelector('.videoQualitySection').classList.add('hide');
context.querySelector('.musicQualitySection').classList.add('hide');
}
if (appHost.supports('multiserver')) {
return;
}
apiClient.getEndpointInfo().then(endpointInfo => {
if (endpointInfo.IsInNetwork) {
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
context.querySelector('.fldVideoInternetQuality').classList.add('hide');
context.querySelector('.musicQualitySection').classList.add('hide');
} else {
context.querySelector('.fldVideoInNetworkQuality').classList.add('hide');
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
if (user.Policy.EnableAudioPlaybackTranscoding) {
@ -115,249 +133,228 @@ import template from './playbackSettings.template.html';
} else {
context.querySelector('.musicQualitySection').classList.add('hide');
}
return;
}
});
}
apiClient.getEndpointInfo().then(endpointInfo => {
if (endpointInfo.IsInNetwork) {
context.querySelector('.fldVideoInNetworkQuality').classList.remove('hide');
context.querySelector('.fldVideoInternetQuality').classList.add('hide');
context.querySelector('.musicQualitySection').classList.add('hide');
} else {
context.querySelector('.fldVideoInNetworkQuality').classList.add('hide');
context.querySelector('.fldVideoInternetQuality').classList.remove('hide');
if (user.Policy.EnableAudioPlaybackTranscoding) {
context.querySelector('.musicQualitySection').classList.remove('hide');
} else {
context.querySelector('.musicQualitySection').classList.add('hide');
}
}
});
function showOrHideEpisodesField(context) {
if (browser.tizen || browser.web0s) {
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
return;
}
function showOrHideEpisodesField(context) {
if (browser.tizen || browser.web0s) {
context.querySelector('.fldEpisodeAutoPlay').classList.add('hide');
return;
}
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
}
context.querySelector('.fldEpisodeAutoPlay').classList.remove('hide');
function loadForm(context, user, userSettings, apiClient) {
const loggedInUserId = apiClient.getCurrentUserId();
const userId = user.Id;
showHideQualityFields(context, user, apiClient);
context.querySelector('#selectAllowedAudioChannels').value = userSettings.allowedAudioChannels();
apiClient.getCultures().then(allCultures => {
populateLanguages(context.querySelector('#selectAudioLanguage'), allCultures);
context.querySelector('#selectAudioLanguage', context).value = user.Configuration.AudioLanguagePreference || '';
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
});
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
context.querySelector('.fldExternalPlayer').classList.remove('hide');
} else {
context.querySelector('.fldExternalPlayer').classList.add('hide');
}
function loadForm(context, user, userSettings, apiClient) {
const loggedInUserId = apiClient.getCurrentUserId();
const userId = user.Id;
if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
context.querySelector('.qualitySections').classList.remove('hide');
showHideQualityFields(context, user, apiClient);
context.querySelector('#selectAllowedAudioChannels').value = userSettings.allowedAudioChannels();
apiClient.getCultures().then(allCultures => {
populateLanguages(context.querySelector('#selectAudioLanguage'), allCultures);
context.querySelector('#selectAudioLanguage', context).value = user.Configuration.AudioLanguagePreference || '';
context.querySelector('.chkEpisodeAutoPlay').checked = user.Configuration.EnableNextEpisodeAutoPlay || false;
});
if (appHost.supports('externalplayerintent') && userId === loggedInUserId) {
context.querySelector('.fldExternalPlayer').classList.remove('hide');
if (appHost.supports('chromecast') && user.Policy.EnableVideoPlaybackTranscoding) {
context.querySelector('.fldChromecastQuality').classList.remove('hide');
} else {
context.querySelector('.fldExternalPlayer').classList.add('hide');
}
if (userId === loggedInUserId && (user.Policy.EnableVideoPlaybackTranscoding || user.Policy.EnableAudioPlaybackTranscoding)) {
context.querySelector('.qualitySections').classList.remove('hide');
if (appHost.supports('chromecast') && user.Policy.EnableVideoPlaybackTranscoding) {
context.querySelector('.fldChromecastQuality').classList.remove('hide');
} else {
context.querySelector('.fldChromecastQuality').classList.add('hide');
}
} else {
context.querySelector('.qualitySections').classList.add('hide');
context.querySelector('.fldChromecastQuality').classList.add('hide');
}
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
setMaxBitrateIntoField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
fillChromecastQuality(context.querySelector('.selectChromecastVideoQuality'));
const selectChromecastVersion = context.querySelector('.selectChromecastVersion');
selectChromecastVersion.value = userSettings.chromecastVersion();
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
fillSkipLengths(selectSkipForwardLength);
selectSkipForwardLength.value = userSettings.skipForwardLength();
const selectSkipBackLength = context.querySelector('.selectSkipBackLength');
fillSkipLengths(selectSkipBackLength);
selectSkipBackLength.value = userSettings.skipBackLength();
showOrHideEpisodesField(context);
loading.hide();
} else {
context.querySelector('.qualitySections').classList.add('hide');
context.querySelector('.fldChromecastQuality').classList.add('hide');
}
function saveUser(context, user, userSettingsInstance, apiClient) {
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
context.querySelector('.chkPlayDefaultAudioTrack').checked = user.Configuration.PlayDefaultAudioTrack || false;
context.querySelector('.chkPreferFmp4HlsContainer').checked = userSettings.preferFmp4HlsContainer();
context.querySelector('.chkEnableCinemaMode').checked = userSettings.enableCinemaMode();
context.querySelector('.chkEnableNextVideoOverlay').checked = userSettings.enableNextVideoInfoOverlay();
context.querySelector('.chkRememberAudioSelections').checked = user.Configuration.RememberAudioSelections || false;
context.querySelector('.chkRememberSubtitleSelections').checked = user.Configuration.RememberSubtitleSelections || false;
context.querySelector('.chkExternalVideoPlayer').checked = appSettings.enableSystemExternalPlayers();
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
setMaxBitrateIntoField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
setMaxBitrateIntoField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
setMaxBitrateIntoField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
setMaxBitrateFromField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
fillChromecastQuality(context.querySelector('.selectChromecastVideoQuality'));
userSettingsInstance.allowedAudioChannels(context.querySelector('#selectAllowedAudioChannels').value);
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
const selectChromecastVersion = context.querySelector('.selectChromecastVersion');
selectChromecastVersion.value = userSettings.chromecastVersion();
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);
user.Configuration.RememberAudioSelections = context.querySelector('.chkRememberAudioSelections').checked;
user.Configuration.RememberSubtitleSelections = context.querySelector('.chkRememberSubtitleSelections').checked;
userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value);
userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value);
userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value);
const selectLabelMaxVideoWidth = context.querySelector('.selectLabelMaxVideoWidth');
selectLabelMaxVideoWidth.value = appSettings.maxVideoWidth();
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
const selectSkipForwardLength = context.querySelector('.selectSkipForwardLength');
fillSkipLengths(selectSkipForwardLength);
selectSkipForwardLength.value = userSettings.skipForwardLength();
const selectSkipBackLength = context.querySelector('.selectSkipBackLength');
fillSkipLengths(selectSkipBackLength);
selectSkipBackLength.value = userSettings.skipBackLength();
showOrHideEpisodesField(context);
loading.hide();
}
function saveUser(context, user, userSettingsInstance, apiClient) {
appSettings.enableSystemExternalPlayers(context.querySelector('.chkExternalVideoPlayer').checked);
appSettings.maxChromecastBitrate(context.querySelector('.selectChromecastVideoQuality').value);
appSettings.maxVideoWidth(context.querySelector('.selectLabelMaxVideoWidth').value);
setMaxBitrateFromField(context.querySelector('.selectVideoInNetworkQuality'), true, 'Video');
setMaxBitrateFromField(context.querySelector('.selectVideoInternetQuality'), false, 'Video');
setMaxBitrateFromField(context.querySelector('.selectMusicInternetQuality'), false, 'Audio');
userSettingsInstance.allowedAudioChannels(context.querySelector('#selectAllowedAudioChannels').value);
user.Configuration.AudioLanguagePreference = context.querySelector('#selectAudioLanguage').value;
user.Configuration.PlayDefaultAudioTrack = context.querySelector('.chkPlayDefaultAudioTrack').checked;
user.Configuration.EnableNextEpisodeAutoPlay = context.querySelector('.chkEpisodeAutoPlay').checked;
userSettingsInstance.preferFmp4HlsContainer(context.querySelector('.chkPreferFmp4HlsContainer').checked);
userSettingsInstance.enableCinemaMode(context.querySelector('.chkEnableCinemaMode').checked);
userSettingsInstance.enableNextVideoInfoOverlay(context.querySelector('.chkEnableNextVideoOverlay').checked);
user.Configuration.RememberAudioSelections = context.querySelector('.chkRememberAudioSelections').checked;
user.Configuration.RememberSubtitleSelections = context.querySelector('.chkRememberSubtitleSelections').checked;
userSettingsInstance.chromecastVersion(context.querySelector('.selectChromecastVersion').value);
userSettingsInstance.skipForwardLength(context.querySelector('.selectSkipForwardLength').value);
userSettingsInstance.skipBackLength(context.querySelector('.selectSkipBackLength').value);
return apiClient.updateUserConfiguration(user.Id, user.Configuration);
}
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
loading.show();
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function setSelectValue(select, value, defaultValue) {
select.value = value;
if (select.selectedIndex < 0) {
select.value = defaultValue;
}
}
function onMaxVideoWidthChange(e) {
const context = this.options.element;
const selectVideoInNetworkQuality = context.querySelector('.selectVideoInNetworkQuality');
const selectVideoInternetQuality = context.querySelector('.selectVideoInternetQuality');
const selectChromecastVideoQuality = context.querySelector('.selectChromecastVideoQuality');
const selectVideoInNetworkQualityValue = selectVideoInNetworkQuality.value;
const selectVideoInternetQualityValue = selectVideoInternetQuality.value;
const selectChromecastVideoQualityValue = selectChromecastVideoQuality.value;
const maxVideoWidth = parseInt(e.target.value || '0', 10) || 0;
fillQuality(selectVideoInNetworkQuality, true, 'Video', maxVideoWidth);
fillQuality(selectVideoInternetQuality, false, 'Video', maxVideoWidth);
fillChromecastQuality(selectChromecastVideoQuality, maxVideoWidth);
setSelectValue(selectVideoInNetworkQuality, selectVideoInNetworkQualityValue, '');
setSelectValue(selectVideoInternetQuality, selectVideoInternetQualityValue, '');
setSelectValue(selectChromecastVideoQuality, selectChromecastVideoQualityValue, '');
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
function embed(options, self) {
options.element.innerHTML = globalize.translateHtml(template, 'core');
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
function save(instance, context, userId, userSettings, apiClient, enableSaveConfirmation) {
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
self.loadData();
if (options.autoFocus) {
focusManager.autoFocus(options.element);
}
}
class PlaybackSettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData() {
const self = this;
const context = self.options.element;
loading.show();
apiClient.getUser(userId).then(user => {
saveUser(context, user, userSettings, apiClient).then(() => {
loading.hide();
if (enableSaveConfirmation) {
toast(globalize.translate('SettingsSaved'));
}
Events.trigger(instance, 'saved');
}, () => {
loading.hide();
});
});
}
function setSelectValue(select, value, defaultValue) {
select.value = value;
if (select.selectedIndex < 0) {
select.value = defaultValue;
}
}
function onMaxVideoWidthChange(e) {
const context = this.options.element;
const selectVideoInNetworkQuality = context.querySelector('.selectVideoInNetworkQuality');
const selectVideoInternetQuality = context.querySelector('.selectVideoInternetQuality');
const selectChromecastVideoQuality = context.querySelector('.selectChromecastVideoQuality');
const selectVideoInNetworkQualityValue = selectVideoInNetworkQuality.value;
const selectVideoInternetQualityValue = selectVideoInternetQuality.value;
const selectChromecastVideoQualityValue = selectChromecastVideoQuality.value;
const maxVideoWidth = parseInt(e.target.value || '0', 10) || 0;
fillQuality(selectVideoInNetworkQuality, true, 'Video', maxVideoWidth);
fillQuality(selectVideoInternetQuality, false, 'Video', maxVideoWidth);
fillChromecastQuality(selectChromecastVideoQuality, maxVideoWidth);
setSelectValue(selectVideoInNetworkQuality, selectVideoInNetworkQualityValue, '');
setSelectValue(selectVideoInternetQuality, selectVideoInternetQualityValue, '');
setSelectValue(selectChromecastVideoQuality, selectChromecastVideoQualityValue, '');
}
function onSubmit(e) {
const self = this;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
userSettings.setUserInfo(userId, apiClient).then(() => {
const enableSaveConfirmation = self.options.enableSaveConfirmation;
save(self, self.options.element, userId, userSettings, apiClient, enableSaveConfirmation);
});
apiClient.getUser(userId).then(user => {
userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
// Disable default form submission
if (e) {
e.preventDefault();
}
return false;
}
function embed(options, self) {
options.element.innerHTML = globalize.translateHtml(template, 'core');
options.element.querySelector('form').addEventListener('submit', onSubmit.bind(self));
if (options.enableSaveButton) {
options.element.querySelector('.btnSave').classList.remove('hide');
}
options.element.querySelector('.selectLabelMaxVideoWidth').addEventListener('change', onMaxVideoWidthChange.bind(self));
self.loadData();
if (options.autoFocus) {
focusManager.autoFocus(options.element);
}
}
class PlaybackSettings {
constructor(options) {
this.options = options;
embed(options, this);
}
loadData() {
const self = this;
const context = self.options.element;
loading.show();
const userId = self.options.userId;
const apiClient = ServerConnections.getApiClient(self.options.serverId);
const userSettings = self.options.userSettings;
apiClient.getUser(userId).then(user => {
userSettings.setUserInfo(userId, apiClient).then(() => {
self.dataLoaded = true;
loadForm(context, user, userSettings, apiClient);
});
loadForm(context, user, userSettings, apiClient);
});
}
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
});
}
/* eslint-enable indent */
submit() {
onSubmit.call(this);
}
destroy() {
this.options = null;
}
}
export default PlaybackSettings;

View file

@ -9,473 +9,471 @@ import { PluginType } from '../../types/plugin.ts';
import './playerstats.scss';
import ServerConnections from '../ServerConnections';
/* eslint-disable indent */
function init(instance) {
const parent = document.createElement('div');
function init(instance) {
const parent = document.createElement('div');
parent.classList.add('playerStats');
parent.classList.add('playerStats');
if (layoutManager.tv) {
parent.classList.add('playerStats-tv');
}
parent.classList.add('hide');
let button;
if (layoutManager.tv) {
button = '';
} else {
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
}
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';
parent.innerHTML = '<div class="' + contentClass + '">' + button + '<div class="playerStats-stats"></div></div>';
button = parent.querySelector('.playerStats-closeButton');
if (button) {
button.addEventListener('click', onCloseButtonClick.bind(instance));
}
document.body.appendChild(parent);
instance.element = parent;
if (layoutManager.tv) {
parent.classList.add('playerStats-tv');
}
function onCloseButtonClick() {
this.enabled(false);
parent.classList.add('hide');
let button;
if (layoutManager.tv) {
button = '';
} else {
button = '<button type="button" is="paper-icon-button-light" class="playerStats-closeButton"><span class="material-icons close" aria-hidden="true"></span></button>';
}
function renderStats(elem, categories) {
elem.querySelector('.playerStats-stats').innerHTML = categories.map(function (category) {
let categoryHtml = '';
const contentClass = layoutManager.tv ? 'playerStats-content playerStats-content-tv' : 'playerStats-content';
const stats = category.stats;
parent.innerHTML = '<div class="' + contentClass + '">' + button + '<div class="playerStats-stats"></div></div>';
if (stats.length && category.name) {
categoryHtml += '<div class="playerStats-stat playerStats-stat-header">';
button = parent.querySelector('.playerStats-closeButton');
categoryHtml += '<div class="playerStats-stat-label">';
categoryHtml += category.name;
categoryHtml += '</div>';
categoryHtml += '<div class="playerStats-stat-value">';
categoryHtml += category.subText || '';
categoryHtml += '</div>';
categoryHtml += '</div>';
}
for (let i = 0, length = stats.length; i < length; i++) {
categoryHtml += '<div class="playerStats-stat">';
const stat = stats[i];
categoryHtml += '<div class="playerStats-stat-label">';
categoryHtml += stat.label;
categoryHtml += '</div>';
categoryHtml += '<div class="playerStats-stat-value">';
categoryHtml += stat.value;
categoryHtml += '</div>';
categoryHtml += '</div>';
}
return categoryHtml;
}).join('');
if (button) {
button.addEventListener('click', onCloseButtonClick.bind(instance));
}
function getSession(instance, player) {
const now = new Date().getTime();
document.body.appendChild(parent);
if ((now - (instance.lastSessionTime || 0)) < 10000) {
return Promise.resolve(instance.lastSession);
instance.element = parent;
}
function onCloseButtonClick() {
this.enabled(false);
}
function renderStats(elem, categories) {
elem.querySelector('.playerStats-stats').innerHTML = categories.map(function (category) {
let categoryHtml = '';
const stats = category.stats;
if (stats.length && category.name) {
categoryHtml += '<div class="playerStats-stat playerStats-stat-header">';
categoryHtml += '<div class="playerStats-stat-label">';
categoryHtml += category.name;
categoryHtml += '</div>';
categoryHtml += '<div class="playerStats-stat-value">';
categoryHtml += category.subText || '';
categoryHtml += '</div>';
categoryHtml += '</div>';
}
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
for (let i = 0, length = stats.length; i < length; i++) {
categoryHtml += '<div class="playerStats-stat">';
return apiClient.getSessions({
deviceId: apiClient.deviceId()
}).then(function (sessions) {
instance.lastSession = sessions[0] || {};
instance.lastSessionTime = new Date().getTime();
const stat = stats[i];
return Promise.resolve(instance.lastSession);
}, function () {
return Promise.resolve({});
categoryHtml += '<div class="playerStats-stat-label">';
categoryHtml += stat.label;
categoryHtml += '</div>';
categoryHtml += '<div class="playerStats-stat-value">';
categoryHtml += stat.value;
categoryHtml += '</div>';
categoryHtml += '</div>';
}
return categoryHtml;
}).join('');
}
function getSession(instance, player) {
const now = new Date().getTime();
if ((now - (instance.lastSessionTime || 0)) < 10000) {
return Promise.resolve(instance.lastSession);
}
const apiClient = ServerConnections.getApiClient(playbackManager.currentItem(player).ServerId);
return apiClient.getSessions({
deviceId: apiClient.deviceId()
}).then(function (sessions) {
instance.lastSession = sessions[0] || {};
instance.lastSessionTime = new Date().getTime();
return Promise.resolve(instance.lastSession);
}, function () {
return Promise.resolve({});
});
}
function translateReason(reason) {
return globalize.translate('' + reason);
}
function getTranscodingStats(session, player, displayPlayMethod) {
const sessionStats = [];
let videoCodec;
let audioCodec;
let totalBitrate;
let audioChannels;
if (session.TranscodingInfo) {
videoCodec = session.TranscodingInfo.VideoCodec;
audioCodec = session.TranscodingInfo.AudioCodec;
totalBitrate = session.TranscodingInfo.Bitrate;
audioChannels = session.TranscodingInfo.AudioChannels;
}
if (videoCodec) {
sessionStats.push({
label: globalize.translate('LabelVideoCodec'),
value: session.TranscodingInfo.IsVideoDirect ? (videoCodec.toUpperCase() + ' (direct)') : videoCodec.toUpperCase()
});
}
function translateReason(reason) {
return globalize.translate('' + reason);
if (audioCodec) {
sessionStats.push({
label: globalize.translate('LabelAudioCodec'),
value: session.TranscodingInfo.IsAudioDirect ? (audioCodec.toUpperCase() + ' (direct)') : audioCodec.toUpperCase()
});
}
function getTranscodingStats(session, player, displayPlayMethod) {
const sessionStats = [];
let videoCodec;
let audioCodec;
let totalBitrate;
let audioChannels;
if (session.TranscodingInfo) {
videoCodec = session.TranscodingInfo.VideoCodec;
audioCodec = session.TranscodingInfo.AudioCodec;
totalBitrate = session.TranscodingInfo.Bitrate;
audioChannels = session.TranscodingInfo.AudioChannels;
}
if (videoCodec) {
sessionStats.push({
label: globalize.translate('LabelVideoCodec'),
value: session.TranscodingInfo.IsVideoDirect ? (videoCodec.toUpperCase() + ' (direct)') : videoCodec.toUpperCase()
});
}
if (audioCodec) {
sessionStats.push({
label: globalize.translate('LabelAudioCodec'),
value: session.TranscodingInfo.IsAudioDirect ? (audioCodec.toUpperCase() + ' (direct)') : audioCodec.toUpperCase()
});
}
if (displayPlayMethod === 'Transcode') {
if (audioChannels) {
sessionStats.push({
label: globalize.translate('LabelAudioChannels'),
value: audioChannels
});
}
if (totalBitrate) {
sessionStats.push({
label: globalize.translate('LabelBitrate'),
value: getDisplayBitrate(totalBitrate)
});
}
if (session.TranscodingInfo.CompletionPercentage) {
sessionStats.push({
label: globalize.translate('LabelTranscodingProgress'),
value: session.TranscodingInfo.CompletionPercentage.toFixed(1) + '%'
});
}
if (session.TranscodingInfo.Framerate) {
sessionStats.push({
label: globalize.translate('LabelTranscodingFramerate'),
value: session.TranscodingInfo.Framerate + ' fps'
});
}
if (session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo.TranscodeReasons.length) {
sessionStats.push({
label: globalize.translate('LabelReasonForTranscoding'),
value: session.TranscodingInfo.TranscodeReasons.map(translateReason).join('<br/>')
});
}
if (session.TranscodingInfo.HardwareAccelerationType) {
sessionStats.push({
label: globalize.translate('LabelHardwareEncoding'),
value: session.TranscodingInfo.HardwareAccelerationType
});
}
}
return sessionStats;
}
function getDisplayBitrate(bitrate) {
if (bitrate > 1000000) {
return (bitrate / 1000000).toFixed(1) + ' Mbps';
} else {
return Math.floor(bitrate / 1000) + ' kbps';
}
}
function getReadableSize(size) {
if (size >= 1073741824) {
return parseFloat((size / 1073741824).toFixed(1)) + ' GiB';
} else if (size >= 1048576) {
return parseFloat((size / 1048576).toFixed(1)) + ' MiB';
} else {
return Math.floor(size / 1024) + ' KiB';
}
}
function getMediaSourceStats(session, player) {
const sessionStats = [];
const mediaSource = playbackManager.currentMediaSource(player) || {};
const totalBitrate = mediaSource.Bitrate;
const mediaFileSize = mediaSource.Size;
if (mediaSource.Container) {
sessionStats.push({
label: globalize.translate('LabelProfileContainer'),
value: mediaSource.Container
});
}
if (mediaFileSize) {
sessionStats.push({
label: globalize.translate('LabelSize'),
value: getReadableSize(mediaFileSize)
});
}
if (totalBitrate) {
sessionStats.push({
label: globalize.translate('LabelBitrate'),
value: getDisplayBitrate(totalBitrate)
});
}
const mediaStreams = mediaSource.MediaStreams || [];
const videoStream = mediaStreams.filter(function (s) {
return s.Type === 'Video';
})[0] || {};
const videoCodec = videoStream.Codec;
const audioStreamIndex = playbackManager.getAudioStreamIndex(player);
const audioStream = playbackManager.audioTracks(player).filter(function (s) {
return s.Type === 'Audio' && s.Index === audioStreamIndex;
})[0] || {};
const audioCodec = audioStream.Codec;
const audioChannels = audioStream.Channels;
const videoInfos = [];
if (videoCodec) {
videoInfos.push(videoCodec.toUpperCase());
}
if (videoStream.Profile) {
videoInfos.push(videoStream.Profile);
}
if (videoInfos.length) {
sessionStats.push({
label: globalize.translate('LabelVideoCodec'),
value: videoInfos.join(' ')
});
}
if (videoStream.BitRate) {
sessionStats.push({
label: globalize.translate('LabelVideoBitrate'),
value: getDisplayBitrate(videoStream.BitRate)
});
}
if (videoStream.VideoRangeType) {
sessionStats.push({
label: globalize.translate('LabelVideoRangeType'),
value: videoStream.VideoRangeType
});
}
const audioInfos = [];
if (audioCodec) {
audioInfos.push(audioCodec.toUpperCase());
}
if (audioStream.Profile) {
audioInfos.push(audioStream.Profile);
}
if (audioInfos.length) {
sessionStats.push({
label: globalize.translate('LabelAudioCodec'),
value: audioInfos.join(' ')
});
}
if (audioStream.BitRate) {
sessionStats.push({
label: globalize.translate('LabelAudioBitrate'),
value: getDisplayBitrate(audioStream.BitRate)
});
}
if (displayPlayMethod === 'Transcode') {
if (audioChannels) {
sessionStats.push({
label: globalize.translate('LabelAudioChannels'),
value: audioChannels
});
}
if (audioStream.SampleRate) {
if (totalBitrate) {
sessionStats.push({
label: globalize.translate('LabelAudioSampleRate'),
value: audioStream.SampleRate + ' Hz'
label: globalize.translate('LabelBitrate'),
value: getDisplayBitrate(totalBitrate)
});
}
if (audioStream.BitDepth) {
if (session.TranscodingInfo.CompletionPercentage) {
sessionStats.push({
label: globalize.translate('LabelAudioBitDepth'),
value: audioStream.BitDepth
label: globalize.translate('LabelTranscodingProgress'),
value: session.TranscodingInfo.CompletionPercentage.toFixed(1) + '%'
});
}
if (session.TranscodingInfo.Framerate) {
sessionStats.push({
label: globalize.translate('LabelTranscodingFramerate'),
value: session.TranscodingInfo.Framerate + ' fps'
});
}
if (session.TranscodingInfo.TranscodeReasons && session.TranscodingInfo.TranscodeReasons.length) {
sessionStats.push({
label: globalize.translate('LabelReasonForTranscoding'),
value: session.TranscodingInfo.TranscodeReasons.map(translateReason).join('<br/>')
});
}
if (session.TranscodingInfo.HardwareAccelerationType) {
sessionStats.push({
label: globalize.translate('LabelHardwareEncoding'),
value: session.TranscodingInfo.HardwareAccelerationType
});
}
return sessionStats;
}
function getSyncPlayStats() {
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
return sessionStats;
}
if (!SyncPlay?.Manager.isSyncPlayEnabled()) {
return [];
}
function getDisplayBitrate(bitrate) {
if (bitrate > 1000000) {
return (bitrate / 1000000).toFixed(1) + ' Mbps';
} else {
return Math.floor(bitrate / 1000) + ' kbps';
}
}
const syncStats = [];
const stats = SyncPlay.Manager.getStats();
function getReadableSize(size) {
if (size >= 1073741824) {
return parseFloat((size / 1073741824).toFixed(1)) + ' GiB';
} else if (size >= 1048576) {
return parseFloat((size / 1048576).toFixed(1)) + ' MiB';
} else {
return Math.floor(size / 1024) + ' KiB';
}
}
syncStats.push({
label: globalize.translate('LabelSyncPlayTimeSyncDevice'),
value: stats.TimeSyncDevice
function getMediaSourceStats(session, player) {
const sessionStats = [];
const mediaSource = playbackManager.currentMediaSource(player) || {};
const totalBitrate = mediaSource.Bitrate;
const mediaFileSize = mediaSource.Size;
if (mediaSource.Container) {
sessionStats.push({
label: globalize.translate('LabelProfileContainer'),
value: mediaSource.Container
});
syncStats.push({
// TODO: clean old string 'LabelSyncPlayTimeOffset' from translations.
label: globalize.translate('LabelSyncPlayTimeSyncOffset'),
value: stats.TimeSyncOffset + ' ' + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate('LabelSyncPlayPlaybackDiff'),
value: stats.PlaybackDiff + ' ' + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate('LabelSyncPlaySyncMethod'),
value: stats.SyncMethod
});
return syncStats;
}
function getStats(instance, player) {
const statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
const sessionPromise = getSession(instance, player);
if (mediaFileSize) {
sessionStats.push({
label: globalize.translate('LabelSize'),
value: getReadableSize(mediaFileSize)
});
}
return Promise.all([statsPromise, sessionPromise]).then(function (responses) {
const playerStatsResult = responses[0];
const playerStats = playerStatsResult.categories || [];
const session = responses[1];
if (totalBitrate) {
sessionStats.push({
label: globalize.translate('LabelBitrate'),
value: getDisplayBitrate(totalBitrate)
});
}
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
let localizedDisplayMethod = displayPlayMethod;
const mediaStreams = mediaSource.MediaStreams || [];
const videoStream = mediaStreams.filter(function (s) {
return s.Type === 'Video';
})[0] || {};
if (displayPlayMethod === 'DirectPlay') {
localizedDisplayMethod = globalize.translate('DirectPlaying');
} else if (displayPlayMethod === 'Remux') {
localizedDisplayMethod = globalize.translate('Remuxing');
} else if (displayPlayMethod === 'DirectStream') {
localizedDisplayMethod = globalize.translate('DirectStreaming');
} else if (displayPlayMethod === 'Transcode') {
localizedDisplayMethod = globalize.translate('Transcoding');
const videoCodec = videoStream.Codec;
const audioStreamIndex = playbackManager.getAudioStreamIndex(player);
const audioStream = playbackManager.audioTracks(player).filter(function (s) {
return s.Type === 'Audio' && s.Index === audioStreamIndex;
})[0] || {};
const audioCodec = audioStream.Codec;
const audioChannels = audioStream.Channels;
const videoInfos = [];
if (videoCodec) {
videoInfos.push(videoCodec.toUpperCase());
}
if (videoStream.Profile) {
videoInfos.push(videoStream.Profile);
}
if (videoInfos.length) {
sessionStats.push({
label: globalize.translate('LabelVideoCodec'),
value: videoInfos.join(' ')
});
}
if (videoStream.BitRate) {
sessionStats.push({
label: globalize.translate('LabelVideoBitrate'),
value: getDisplayBitrate(videoStream.BitRate)
});
}
if (videoStream.VideoRangeType) {
sessionStats.push({
label: globalize.translate('LabelVideoRangeType'),
value: videoStream.VideoRangeType
});
}
const audioInfos = [];
if (audioCodec) {
audioInfos.push(audioCodec.toUpperCase());
}
if (audioStream.Profile) {
audioInfos.push(audioStream.Profile);
}
if (audioInfos.length) {
sessionStats.push({
label: globalize.translate('LabelAudioCodec'),
value: audioInfos.join(' ')
});
}
if (audioStream.BitRate) {
sessionStats.push({
label: globalize.translate('LabelAudioBitrate'),
value: getDisplayBitrate(audioStream.BitRate)
});
}
if (audioChannels) {
sessionStats.push({
label: globalize.translate('LabelAudioChannels'),
value: audioChannels
});
}
if (audioStream.SampleRate) {
sessionStats.push({
label: globalize.translate('LabelAudioSampleRate'),
value: audioStream.SampleRate + ' Hz'
});
}
if (audioStream.BitDepth) {
sessionStats.push({
label: globalize.translate('LabelAudioBitDepth'),
value: audioStream.BitDepth
});
}
return sessionStats;
}
function getSyncPlayStats() {
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
if (!SyncPlay?.Manager.isSyncPlayEnabled()) {
return [];
}
const syncStats = [];
const stats = SyncPlay.Manager.getStats();
syncStats.push({
label: globalize.translate('LabelSyncPlayTimeSyncDevice'),
value: stats.TimeSyncDevice
});
syncStats.push({
// TODO: clean old string 'LabelSyncPlayTimeOffset' from translations.
label: globalize.translate('LabelSyncPlayTimeSyncOffset'),
value: stats.TimeSyncOffset + ' ' + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate('LabelSyncPlayPlaybackDiff'),
value: stats.PlaybackDiff + ' ' + globalize.translate('MillisecondsUnit')
});
syncStats.push({
label: globalize.translate('LabelSyncPlaySyncMethod'),
value: stats.SyncMethod
});
return syncStats;
}
function getStats(instance, player) {
const statsPromise = player.getStats ? player.getStats() : Promise.resolve({});
const sessionPromise = getSession(instance, player);
return Promise.all([statsPromise, sessionPromise]).then(function (responses) {
const playerStatsResult = responses[0];
const playerStats = playerStatsResult.categories || [];
const session = responses[1];
const displayPlayMethod = playMethodHelper.getDisplayPlayMethod(session);
let localizedDisplayMethod = displayPlayMethod;
if (displayPlayMethod === 'DirectPlay') {
localizedDisplayMethod = globalize.translate('DirectPlaying');
} else if (displayPlayMethod === 'Remux') {
localizedDisplayMethod = globalize.translate('Remuxing');
} else if (displayPlayMethod === 'DirectStream') {
localizedDisplayMethod = globalize.translate('DirectStreaming');
} else if (displayPlayMethod === 'Transcode') {
localizedDisplayMethod = globalize.translate('Transcoding');
}
const baseCategory = {
stats: [],
name: globalize.translate('LabelPlaybackInfo')
};
baseCategory.stats.unshift({
label: globalize.translate('LabelPlayMethod'),
value: localizedDisplayMethod
});
baseCategory.stats.unshift({
label: globalize.translate('LabelPlayer'),
value: player.name
});
const categories = [];
categories.push(baseCategory);
for (let i = 0, length = playerStats.length; i < length; i++) {
const category = playerStats[i];
if (category.type === 'audio') {
category.name = globalize.translate('LabelAudioInfo');
} else if (category.type === 'video') {
category.name = globalize.translate('LabelVideoInfo');
}
categories.push(category);
}
const baseCategory = {
stats: [],
name: globalize.translate('LabelPlaybackInfo')
};
baseCategory.stats.unshift({
label: globalize.translate('LabelPlayMethod'),
value: localizedDisplayMethod
});
baseCategory.stats.unshift({
label: globalize.translate('LabelPlayer'),
value: player.name
});
const categories = [];
categories.push(baseCategory);
for (let i = 0, length = playerStats.length; i < length; i++) {
const category = playerStats[i];
if (category.type === 'audio') {
category.name = globalize.translate('LabelAudioInfo');
} else if (category.type === 'video') {
category.name = globalize.translate('LabelVideoInfo');
}
categories.push(category);
}
let localizedTranscodingInfo = globalize.translate('LabelTranscodingInfo');
if (displayPlayMethod === 'Remux') {
localizedTranscodingInfo = globalize.translate('LabelRemuxingInfo');
} else if (displayPlayMethod === 'DirectStream') {
localizedTranscodingInfo = globalize.translate('LabelDirectStreamingInfo');
}
if (session.TranscodingInfo) {
categories.push({
stats: getTranscodingStats(session, player, displayPlayMethod),
name: localizedTranscodingInfo
});
}
let localizedTranscodingInfo = globalize.translate('LabelTranscodingInfo');
if (displayPlayMethod === 'Remux') {
localizedTranscodingInfo = globalize.translate('LabelRemuxingInfo');
} else if (displayPlayMethod === 'DirectStream') {
localizedTranscodingInfo = globalize.translate('LabelDirectStreamingInfo');
}
if (session.TranscodingInfo) {
categories.push({
stats: getMediaSourceStats(session, player),
name: globalize.translate('LabelOriginalMediaInfo')
stats: getTranscodingStats(session, player, displayPlayMethod),
name: localizedTranscodingInfo
});
}
const syncPlayStats = getSyncPlayStats();
if (syncPlayStats.length > 0) {
categories.push({
stats: syncPlayStats,
name: globalize.translate('LabelSyncPlayInfo')
});
}
return Promise.resolve(categories);
categories.push({
stats: getMediaSourceStats(session, player),
name: globalize.translate('LabelOriginalMediaInfo')
});
const syncPlayStats = getSyncPlayStats();
if (syncPlayStats.length > 0) {
categories.push({
stats: syncPlayStats,
name: globalize.translate('LabelSyncPlayInfo')
});
}
return Promise.resolve(categories);
});
}
function renderPlayerStats(instance, player) {
const now = new Date().getTime();
if ((now - (instance.lastRender || 0)) < 700) {
return;
}
function renderPlayerStats(instance, player) {
const now = new Date().getTime();
instance.lastRender = now;
if ((now - (instance.lastRender || 0)) < 700) {
getStats(instance, player).then(function (stats) {
const elem = instance.element;
if (!elem) {
return;
}
instance.lastRender = now;
renderStats(elem, stats);
});
}
getStats(instance, player).then(function (stats) {
const elem = instance.element;
if (!elem) {
return;
}
function bindEvents(instance, player) {
const localOnTimeUpdate = function () {
renderPlayerStats(instance, player);
};
renderStats(elem, stats);
});
}
function bindEvents(instance, player) {
const localOnTimeUpdate = function () {
renderPlayerStats(instance, player);
};
instance.onTimeUpdate = localOnTimeUpdate;
Events.on(player, 'timeupdate', localOnTimeUpdate);
}
function unbindEvents(instance, player) {
const localOnTimeUpdate = instance.onTimeUpdate;
if (localOnTimeUpdate) {
Events.off(player, 'timeupdate', localOnTimeUpdate);
}
instance.onTimeUpdate = localOnTimeUpdate;
Events.on(player, 'timeupdate', localOnTimeUpdate);
}
function unbindEvents(instance, player) {
const localOnTimeUpdate = instance.onTimeUpdate;
if (localOnTimeUpdate) {
Events.off(player, 'timeupdate', localOnTimeUpdate);
}
}
class PlayerStats {
constructor(options) {
@ -527,6 +525,4 @@ class PlayerStats {
}
}
/* eslint-enable indent */
export default PlayerStats;

View file

@ -18,269 +18,266 @@ import 'material-design-icons-iconfont';
import '../formdialog.scss';
import ServerConnections from '../ServerConnections';
/* eslint-disable indent */
let currentServerId;
let currentServerId;
function onSubmit(e) {
const panel = dom.parentWithClass(this, 'dialog');
function onSubmit(e) {
const panel = dom.parentWithClass(this, 'dialog');
const playlistId = panel.querySelector('#selectPlaylistToAddTo').value;
const apiClient = ServerConnections.getApiClient(currentServerId);
const playlistId = panel.querySelector('#selectPlaylistToAddTo').value;
const apiClient = ServerConnections.getApiClient(currentServerId);
if (playlistId) {
userSettings.set('playlisteditor-lastplaylistid', playlistId);
addToPlaylist(apiClient, panel, playlistId);
} else {
createPlaylist(apiClient, panel);
}
e.preventDefault();
return false;
if (playlistId) {
userSettings.set('playlisteditor-lastplaylistid', playlistId);
addToPlaylist(apiClient, panel, playlistId);
} else {
createPlaylist(apiClient, panel);
}
function createPlaylist(apiClient, dlg) {
loading.show();
e.preventDefault();
return false;
}
const url = apiClient.getUrl('Playlists', {
Name: dlg.querySelector('#txtNewPlaylistName').value,
Ids: dlg.querySelector('.fldSelectedItemIds').value || '',
userId: apiClient.getCurrentUserId()
function createPlaylist(apiClient, dlg) {
loading.show();
});
const url = apiClient.getUrl('Playlists', {
Name: dlg.querySelector('#txtNewPlaylistName').value,
Ids: dlg.querySelector('.fldSelectedItemIds').value || '',
userId: apiClient.getCurrentUserId()
apiClient.ajax({
type: 'POST',
url: url,
dataType: 'json',
contentType: 'application/json'
}).then(result => {
loading.hide();
const id = result.Id;
dlg.submitted = true;
dialogHelper.close(dlg);
redirectToPlaylist(apiClient, id);
});
}
function redirectToPlaylist(apiClient, id) {
appRouter.showItem(id, apiClient.serverId());
}
function addToPlaylist(apiClient, dlg, id) {
const itemIds = dlg.querySelector('.fldSelectedItemIds').value || '';
if (id === 'queue') {
playbackManager.queue({
serverId: apiClient.serverId(),
ids: itemIds.split(',')
});
dlg.submitted = true;
dialogHelper.close(dlg);
return;
}
loading.show();
const url = apiClient.getUrl(`Playlists/${id}/Items`, {
Ids: itemIds,
userId: apiClient.getCurrentUserId()
});
apiClient.ajax({
type: 'POST',
url: url
}).then(() => {
loading.hide();
dlg.submitted = true;
dialogHelper.close(dlg);
});
}
function triggerChange(select) {
select.dispatchEvent(new CustomEvent('change', {}));
}
function populatePlaylists(editorOptions, panel) {
const select = panel.querySelector('#selectPlaylistToAddTo');
});
apiClient.ajax({
type: 'POST',
url: url,
dataType: 'json',
contentType: 'application/json'
}).then(result => {
loading.hide();
panel.querySelector('.newPlaylistInfo').classList.add('hide');
const id = result.Id;
dlg.submitted = true;
dialogHelper.close(dlg);
redirectToPlaylist(apiClient, id);
});
}
const options = {
Recursive: true,
IncludeItemTypes: 'Playlist',
SortBy: 'SortName',
EnableTotalRecordCount: false
};
function redirectToPlaylist(apiClient, id) {
appRouter.showItem(id, apiClient.serverId());
}
const apiClient = ServerConnections.getApiClient(currentServerId);
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
function addToPlaylist(apiClient, dlg, id) {
const itemIds = dlg.querySelector('.fldSelectedItemIds').value || '';
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = '';
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
}
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += result.Items.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
select.innerHTML = html;
let defaultValue = editorOptions.defaultValue;
if (!defaultValue) {
defaultValue = userSettings.get('playlisteditor-lastplaylistid') || '';
}
select.value = defaultValue === 'new' ? '' : defaultValue;
// If the value is empty set it again, in case we tried to set a lastplaylistid that is no longer valid
if (!select.value) {
select.value = '';
}
triggerChange(select);
loading.hide();
if (id === 'queue') {
playbackManager.queue({
serverId: apiClient.serverId(),
ids: itemIds.split(',')
});
dlg.submitted = true;
dialogHelper.close(dlg);
return;
}
function getEditorHtml(items) {
loading.show();
const url = apiClient.getUrl(`Playlists/${id}/Items`, {
Ids: itemIds,
userId: apiClient.getCurrentUserId()
});
apiClient.ajax({
type: 'POST',
url: url
}).then(() => {
loading.hide();
dlg.submitted = true;
dialogHelper.close(dlg);
});
}
function triggerChange(select) {
select.dispatchEvent(new CustomEvent('change', {}));
}
function populatePlaylists(editorOptions, panel) {
const select = panel.querySelector('#selectPlaylistToAddTo');
loading.hide();
panel.querySelector('.newPlaylistInfo').classList.add('hide');
const options = {
Recursive: true,
IncludeItemTypes: 'Playlist',
SortBy: 'SortName',
EnableTotalRecordCount: false
};
const apiClient = ServerConnections.getApiClient(currentServerId);
const SyncPlay = pluginManager.firstOfType(PluginType.SyncPlay)?.instance;
apiClient.getItems(apiClient.getCurrentUserId(), options).then(result => {
let html = '';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form style="margin:auto;">';
if ((editorOptions.enableAddToPlayQueue !== false && playbackManager.isPlaying()) || SyncPlay?.Manager.isSyncPlayEnabled()) {
html += `<option value="queue">${globalize.translate('AddToPlayQueue')}</option>`;
}
html += '<div class="fldSelectPlaylist selectContainer">';
let autoFocus = items.length ? ' autofocus' : '';
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
html += '</div>';
html += `<option value="">${globalize.translate('OptionNew')}</option>`;
html += '<div class="newPlaylistInfo">';
html += '<div class="inputContainer">';
autoFocus = items.length ? '' : ' autofocus';
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
html += '</div>';
// newPlaylistInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
return html;
}
function initEditor(content, options, items) {
content.querySelector('#selectPlaylistToAddTo').addEventListener('change', function () {
if (this.value) {
content.querySelector('.newPlaylistInfo').classList.add('hide');
content.querySelector('#txtNewPlaylistName').removeAttribute('required');
} else {
content.querySelector('.newPlaylistInfo').classList.remove('hide');
content.querySelector('#txtNewPlaylistName').setAttribute('required', 'required');
}
html += result.Items.map(i => {
return `<option value="${i.Id}">${escapeHtml(i.Name)}</option>`;
});
content.querySelector('form').addEventListener('submit', onSubmit);
select.innerHTML = html;
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
let defaultValue = editorOptions.defaultValue;
if (!defaultValue) {
defaultValue = userSettings.get('playlisteditor-lastplaylistid') || '';
}
select.value = defaultValue === 'new' ? '' : defaultValue;
if (items.length) {
content.querySelector('.fldSelectPlaylist').classList.remove('hide');
populatePlaylists(options, content);
// If the value is empty set it again, in case we tried to set a lastplaylistid that is no longer valid
if (!select.value) {
select.value = '';
}
triggerChange(select);
loading.hide();
});
}
function getEditorHtml(items) {
let html = '';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form style="margin:auto;">';
html += '<div class="fldSelectPlaylist selectContainer">';
let autoFocus = items.length ? ' autofocus' : '';
html += `<select is="emby-select" id="selectPlaylistToAddTo" label="${globalize.translate('LabelPlaylist')}"${autoFocus}></select>`;
html += '</div>';
html += '<div class="newPlaylistInfo">';
html += '<div class="inputContainer">';
autoFocus = items.length ? '' : ' autofocus';
html += `<input is="emby-input" type="text" id="txtNewPlaylistName" required="required" label="${globalize.translate('LabelName')}"${autoFocus} />`;
html += '</div>';
// newPlaylistInfo
html += '</div>';
html += '<div class="formDialogFooter">';
html += `<button is="emby-button" type="submit" class="raised btnSubmit block formDialogFooterItem button-submit">${globalize.translate('Add')}</button>`;
html += '</div>';
html += '<input type="hidden" class="fldSelectedItemIds" />';
html += '</form>';
html += '</div>';
html += '</div>';
return html;
}
function initEditor(content, options, items) {
content.querySelector('#selectPlaylistToAddTo').addEventListener('change', function () {
if (this.value) {
content.querySelector('.newPlaylistInfo').classList.add('hide');
content.querySelector('#txtNewPlaylistName').removeAttribute('required');
} else {
content.querySelector('.fldSelectPlaylist').classList.add('hide');
const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo');
selectPlaylistToAddTo.innerHTML = '';
selectPlaylistToAddTo.value = '';
triggerChange(selectPlaylistToAddTo);
content.querySelector('.newPlaylistInfo').classList.remove('hide');
content.querySelector('#txtNewPlaylistName').setAttribute('required', 'required');
}
}
});
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
content.querySelector('form').addEventListener('submit', onSubmit);
content.querySelector('.fldSelectedItemIds', content).value = items.join(',');
if (items.length) {
content.querySelector('.fldSelectPlaylist').classList.remove('hide');
populatePlaylists(options, content);
} else {
content.querySelector('.fldSelectPlaylist').classList.add('hide');
const selectPlaylistToAddTo = content.querySelector('#selectPlaylistToAddTo');
selectPlaylistToAddTo.innerHTML = '';
selectPlaylistToAddTo.value = '';
triggerChange(selectPlaylistToAddTo);
}
}
function centerFocus(elem, horiz, on) {
import('../../scripts/scrollHelper').then((scrollHelper) => {
const fn = on ? 'on' : 'off';
scrollHelper.centerFocus[fn](elem, horiz);
});
}
export class showEditor {
constructor(options) {
const items = options.items || {};
currentServerId = options.serverId;
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
const title = globalize.translate('HeaderAddToPlaylist');
html += '<div class="formDialogHeader">';
html += `<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">';
html += title;
html += '</h3>';
html += '</div>';
html += getEditorHtml(items);
dlg.innerHTML = html;
initEditor(dlg, options, items);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
return dialogHelper.open(dlg).then(() => {
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (dlg.submitted) {
return Promise.resolve();
}
return Promise.reject();
});
}
}
export class showEditor {
constructor(options) {
const items = options.items || {};
currentServerId = options.serverId;
const dialogOptions = {
removeOnClose: true,
scrollY: false
};
if (layoutManager.tv) {
dialogOptions.size = 'fullscreen';
} else {
dialogOptions.size = 'small';
}
const dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('formDialog');
let html = '';
const title = globalize.translate('HeaderAddToPlaylist');
html += '<div class="formDialogHeader">';
html += `<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">';
html += title;
html += '</h3>';
html += '</div>';
html += getEditorHtml(items);
dlg.innerHTML = html;
initEditor(dlg, options, items);
dlg.querySelector('.btnCancel').addEventListener('click', () => {
dialogHelper.close(dlg);
});
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, true);
}
return dialogHelper.open(dlg).then(() => {
if (layoutManager.tv) {
centerFocus(dlg.querySelector('.formDialogContent'), false, false);
}
if (dlg.submitted) {
return Promise.resolve();
}
return Promise.reject();
});
}
}
/* eslint-enable indent */
export default showEditor;

View file

@ -325,10 +325,11 @@ export default function () {
if (layoutManager.mobile) {
const playingVideo = playbackManager.isPlayingVideo() && item !== null;
const playingAudio = !playbackManager.isPlayingVideo() && item !== null;
buttonVisible(context.querySelector('.btnRepeat'), playingAudio);
buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio);
buttonVisible(context.querySelector('.btnRewind'), playingVideo);
buttonVisible(context.querySelector('.btnFastForward'), playingVideo);
const playingAudioBook = playingAudio && item.Type == 'AudioBook';
buttonVisible(context.querySelector('.btnRepeat'), playingAudio && !playingAudioBook);
buttonVisible(context.querySelector('.btnShuffleQueue'), playingAudio && !playingAudioBook);
buttonVisible(context.querySelector('.btnRewind'), playingVideo || playingAudioBook);
buttonVisible(context.querySelector('.btnFastForward'), playingVideo || playingAudioBook);
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnShuffleQueue'), playingVideo);
buttonVisible(context.querySelector('.nowPlayingSecondaryButtons .btnRepeat'), playingVideo);
} else {
@ -763,18 +764,22 @@ export default function () {
context.querySelector('.btnPreviousTrack').addEventListener('click', function (e) {
if (currentPlayer) {
if (lastPlayerState.NowPlayingItem.MediaType === 'Audio' && (currentPlayer._currentTime >= 5 || !playbackManager.previousTrack(currentPlayer))) {
// Cancel this event if doubleclick is fired
if (e.detail > 1 && playbackManager.previousTrack(currentPlayer)) {
if (lastPlayerState.NowPlayingItem.MediaType === 'Audio') {
// Cancel this event if doubleclick is fired. The actual previousTrack will be processed by the 'dblclick' event
if (e.detail > 1 ) {
return;
}
// Return to start of track, unless we are already (almost) at the beginning. In the latter case, continue and move
// to the previous track, unless we are at the first track so no previous track exists.
if (currentPlayer._currentTime >= 5 || playbackManager.getCurrentPlaylistIndex(currentPlayer) <= 1) {
playbackManager.seekPercent(0, currentPlayer);
// This is done automatically by playbackManager, however, setting this here gives instant visual feedback.
// TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround.
positionSlider.value = 0;
return;
}
playbackManager.seekPercent(0, currentPlayer);
// This is done automatically by playbackManager. However, setting this here gives instant visual feedback.
// TODO: Check why seekPercentage doesn't reflect the changes inmmediately, so we can remove this workaround.
positionSlider.value = 0;
} else {
playbackManager.previousTrack(currentPlayer);
}
playbackManager.previousTrack(currentPlayer);
}
});

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module for controlling scroll behavior.
* @module components/scrollManager
@ -9,50 +7,50 @@ import dom from '../scripts/dom';
import browser from '../scripts/browser';
import layoutManager from './layoutManager';
/**
/**
* Scroll time in ms.
*/
const ScrollTime = 270;
const ScrollTime = 270;
/**
/**
* Epsilon for comparing values.
*/
const Epsilon = 1e-6;
const Epsilon = 1e-6;
// FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers
/**
// FIXME: Need to scroll to top of page to fully show the top menu. This can be solved by some marker of top most elements or their containers
/**
* Returns minimum vertical scroll.
* Scroll less than that value will be zeroed.
*
* @return {number} Minimum vertical scroll.
*/
function minimumScrollY() {
const topMenu = document.querySelector('.headerTop');
if (topMenu) {
return topMenu.clientHeight;
function minimumScrollY() {
const topMenu = document.querySelector('.headerTop');
if (topMenu) {
return topMenu.clientHeight;
}
return 0;
}
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
let supportsScrollToOptions = false;
try {
const elem = document.createElement('div');
const opts = Object.defineProperty({}, 'behavior', {
// eslint-disable-next-line getter-return
get: function () {
supportsScrollToOptions = true;
}
return 0;
}
});
const supportsSmoothScroll = 'scrollBehavior' in document.documentElement.style;
elem.scrollTo(opts);
} catch (e) {
console.error('error checking ScrollToOptions support');
}
let supportsScrollToOptions = false;
try {
const elem = document.createElement('div');
const opts = Object.defineProperty({}, 'behavior', {
// eslint-disable-next-line getter-return
get: function () {
supportsScrollToOptions = true;
}
});
elem.scrollTo(opts);
} catch (e) {
console.error('error checking ScrollToOptions support');
}
/**
/**
* Returns value clamped by range [min, max].
*
* @param {number} value - Clamped value.
@ -60,16 +58,16 @@ import layoutManager from './layoutManager';
* @param {number} max - Ending of range.
* @return {number} Clamped value.
*/
function clamp(value, min, max) {
if (value <= min) {
return min;
} else if (value >= max) {
return max;
}
return value;
function clamp(value, min, max) {
if (value <= min) {
return min;
} else if (value >= max) {
return max;
}
return value;
}
/**
/**
* Returns the required delta to fit range 1 into range 2.
* In case of range 1 is bigger than range 2 returns delta to fit most out of range part.
*
@ -79,28 +77,28 @@ import layoutManager from './layoutManager';
* @param {number} end2 - Ending of range 2.
* @return {number} Delta: <0 move range1 to the left, >0 - to the right.
*/
function fitRange(begin1, end1, begin2, end2) {
const delta1 = begin1 - begin2;
const delta2 = end2 - end1;
if (delta1 < 0 && delta1 < delta2) {
return -delta1;
} else if (delta2 < 0) {
return delta2;
}
return 0;
function fitRange(begin1, end1, begin2, end2) {
const delta1 = begin1 - begin2;
const delta2 = end2 - end1;
if (delta1 < 0 && delta1 < delta2) {
return -delta1;
} else if (delta2 < 0) {
return delta2;
}
return 0;
}
/**
/**
* Ease value.
*
* @param {number} t - Value in range [0, 1].
* @return {number} Eased value in range [0, 1].
*/
function ease(t) {
return t * (2 - t); // easeOutQuad === ease-out
}
function ease(t) {
return t * (2 - t); // easeOutQuad === ease-out
}
/**
/**
* @typedef {Object} Rect
* @property {number} left - X coordinate of top-left corner.
* @property {number} top - Y coordinate of top-left corner.
@ -108,7 +106,7 @@ import layoutManager from './layoutManager';
* @property {number} height - Height.
*/
/**
/**
* Document scroll wrapper helps to unify scrolling and fix issues of some browsers.
*
* webOS 2 Browser: scrolls documentElement (and window), but body has a scroll size
@ -121,157 +119,157 @@ import layoutManager from './layoutManager';
*
* Tizen 5 Browser/Native: scrolls documentElement (and window); has a document.scrollingElement
*/
class DocumentScroller {
/**
class DocumentScroller {
/**
* Horizontal scroll position.
* @type {number}
*/
get scrollLeft() {
return window.pageXOffset;
}
get scrollLeft() {
return window.pageXOffset;
}
set scrollLeft(val) {
window.scroll(val, window.pageYOffset);
}
set scrollLeft(val) {
window.scroll(val, window.pageYOffset);
}
/**
/**
* Vertical scroll position.
* @type {number}
*/
get scrollTop() {
return window.pageYOffset;
}
get scrollTop() {
return window.pageYOffset;
}
set scrollTop(val) {
window.scroll(window.pageXOffset, val);
}
set scrollTop(val) {
window.scroll(window.pageXOffset, val);
}
/**
/**
* Horizontal scroll size (scroll width).
* @type {number}
*/
get scrollWidth() {
return Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
}
get scrollWidth() {
return Math.max(document.documentElement.scrollWidth, document.body.scrollWidth);
}
/**
/**
* Vertical scroll size (scroll height).
* @type {number}
*/
get scrollHeight() {
return Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
}
get scrollHeight() {
return Math.max(document.documentElement.scrollHeight, document.body.scrollHeight);
}
/**
/**
* Horizontal client size (client width).
* @type {number}
*/
get clientWidth() {
return Math.min(document.documentElement.clientWidth, document.body.clientWidth);
}
get clientWidth() {
return Math.min(document.documentElement.clientWidth, document.body.clientWidth);
}
/**
/**
* Vertical client size (client height).
* @type {number}
*/
get clientHeight() {
return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
}
get clientHeight() {
return Math.min(document.documentElement.clientHeight, document.body.clientHeight);
}
/**
/**
* Returns attribute value.
* @param {string} attributeName - Attibute name.
* @return {string} Attibute value.
*/
getAttribute(attributeName) {
return document.body.getAttribute(attributeName);
}
/**
* Returns bounding client rect.
* @return {Rect} Bounding client rect.
*/
getBoundingClientRect() {
// Make valid viewport coordinates: documentElement.getBoundingClientRect returns rect of entire document relative to viewport
return {
left: 0,
top: 0,
width: this.clientWidth,
height: this.clientHeight
};
}
/**
* Scrolls window.
* @param {...mixed} args See window.scrollTo.
*/
scrollTo() {
window.scrollTo.apply(window, arguments);
}
getAttribute(attributeName) {
return document.body.getAttribute(attributeName);
}
/**
* Default (document) scroller.
*/
const documentScroller = new DocumentScroller();
const scrollerHints = {
x: {
nameScroll: 'scrollWidth',
nameClient: 'clientWidth',
nameStyle: 'overflowX',
nameScrollMode: 'data-scroll-mode-x'
},
y: {
nameScroll: 'scrollHeight',
nameClient: 'clientHeight',
nameStyle: 'overflowY',
nameScrollMode: 'data-scroll-mode-y'
}
};
* Returns bounding client rect.
* @return {Rect} Bounding client rect.
*/
getBoundingClientRect() {
// Make valid viewport coordinates: documentElement.getBoundingClientRect returns rect of entire document relative to viewport
return {
left: 0,
top: 0,
width: this.clientWidth,
height: this.clientHeight
};
}
/**
* Scrolls window.
* @param {...mixed} args See window.scrollTo.
*/
scrollTo() {
window.scrollTo.apply(window, arguments);
}
}
/**
* Default (document) scroller.
*/
const documentScroller = new DocumentScroller();
const scrollerHints = {
x: {
nameScroll: 'scrollWidth',
nameClient: 'clientWidth',
nameStyle: 'overflowX',
nameScrollMode: 'data-scroll-mode-x'
},
y: {
nameScroll: 'scrollHeight',
nameClient: 'clientHeight',
nameStyle: 'overflowY',
nameScrollMode: 'data-scroll-mode-y'
}
};
/**
* Returns parent element that can be scrolled. If no such, returns document scroller.
*
* @param {HTMLElement} element - Element for which parent is being searched.
* @param {boolean} vertical - Search for vertical scrollable parent.
* @param {HTMLElement|DocumentScroller} Parent element that can be scrolled or document scroller.
*/
function getScrollableParent(element, vertical) {
if (element) {
const scrollerHint = vertical ? scrollerHints.y : scrollerHints.x;
function getScrollableParent(element, vertical) {
if (element) {
const scrollerHint = vertical ? scrollerHints.y : scrollerHints.x;
let parent = element.parentElement;
let parent = element.parentElement;
while (parent && parent !== document.body) {
const scrollMode = parent.getAttribute(scrollerHint.nameScrollMode);
while (parent && parent !== document.body) {
const scrollMode = parent.getAttribute(scrollerHint.nameScrollMode);
// Stop on self-scrolled containers
if (scrollMode === 'custom') {
return parent;
}
const styles = window.getComputedStyle(parent);
// Stop on fixed parent
if (styles.position === 'fixed') {
return parent;
}
const overflow = styles[scrollerHint.nameStyle];
if (overflow === 'scroll' || overflow === 'auto' && parent[scrollerHint.nameScroll] > parent[scrollerHint.nameClient]) {
return parent;
}
parent = parent.parentElement;
// Stop on self-scrolled containers
if (scrollMode === 'custom') {
return parent;
}
}
return documentScroller;
const styles = window.getComputedStyle(parent);
// Stop on fixed parent
if (styles.position === 'fixed') {
return parent;
}
const overflow = styles[scrollerHint.nameStyle];
if (overflow === 'scroll' || overflow === 'auto' && parent[scrollerHint.nameScroll] > parent[scrollerHint.nameClient]) {
return parent;
}
parent = parent.parentElement;
}
}
/**
return documentScroller;
}
/**
* @typedef {Object} ScrollerData
* @property {number} scrollPos - Current scroll position.
* @property {number} scrollSize - Scroll size.
@ -280,34 +278,34 @@ import layoutManager from './layoutManager';
* @property {boolean} custom - Custom scrolling mode.
*/
/**
/**
* Returns scroller data for specified orientation.
*
* @param {HTMLElement} scroller - Scroller.
* @param {boolean} vertical - Vertical scroller data.
* @return {ScrollerData} Scroller data.
*/
function getScrollerData(scroller, vertical) {
const data = {};
function getScrollerData(scroller, vertical) {
const data = {};
if (!vertical) {
data.scrollPos = scroller.scrollLeft;
data.scrollSize = scroller.scrollWidth;
data.clientSize = scroller.clientWidth;
data.mode = scroller.getAttribute(scrollerHints.x.nameScrollMode);
} else {
data.scrollPos = scroller.scrollTop;
data.scrollSize = scroller.scrollHeight;
data.clientSize = scroller.clientHeight;
data.mode = scroller.getAttribute(scrollerHints.y.nameScrollMode);
}
data.custom = data.mode === 'custom';
return data;
if (!vertical) {
data.scrollPos = scroller.scrollLeft;
data.scrollSize = scroller.scrollWidth;
data.clientSize = scroller.clientWidth;
data.mode = scroller.getAttribute(scrollerHints.x.nameScrollMode);
} else {
data.scrollPos = scroller.scrollTop;
data.scrollSize = scroller.scrollHeight;
data.clientSize = scroller.clientHeight;
data.mode = scroller.getAttribute(scrollerHints.y.nameScrollMode);
}
/**
data.custom = data.mode === 'custom';
return data;
}
/**
* Returns position of child of scroller for specified orientation.
*
* @param {HTMLElement} scroller - Scroller.
@ -315,18 +313,18 @@ import layoutManager from './layoutManager';
* @param {boolean} vertical - Vertical scroll.
* @return {number} Child position.
*/
function getScrollerChildPos(scroller, element, vertical) {
const elementRect = element.getBoundingClientRect();
const scrollerRect = scroller.getBoundingClientRect();
function getScrollerChildPos(scroller, element, vertical) {
const elementRect = element.getBoundingClientRect();
const scrollerRect = scroller.getBoundingClientRect();
if (!vertical) {
return scroller.scrollLeft + elementRect.left - scrollerRect.left;
} else {
return scroller.scrollTop + elementRect.top - scrollerRect.top;
}
if (!vertical) {
return scroller.scrollLeft + elementRect.left - scrollerRect.left;
} else {
return scroller.scrollTop + elementRect.top - scrollerRect.top;
}
}
/**
/**
* Returns scroll position for element.
*
* @param {ScrollerData} scrollerData - Scroller data.
@ -335,47 +333,47 @@ import layoutManager from './layoutManager';
* @param {boolean} centered - Scroll to center.
* @return {number} Scroll position.
*/
function calcScroll(scrollerData, elementPos, elementSize, centered) {
const maxScroll = scrollerData.scrollSize - scrollerData.clientSize;
function calcScroll(scrollerData, elementPos, elementSize, centered) {
const maxScroll = scrollerData.scrollSize - scrollerData.clientSize;
let scroll;
let scroll;
if (centered) {
scroll = elementPos + (elementSize - scrollerData.clientSize) / 2;
} else {
const delta = fitRange(elementPos, elementPos + elementSize - 1, scrollerData.scrollPos, scrollerData.scrollPos + scrollerData.clientSize - 1);
scroll = scrollerData.scrollPos - delta;
}
return clamp(Math.round(scroll), 0, maxScroll);
if (centered) {
scroll = elementPos + (elementSize - scrollerData.clientSize) / 2;
} else {
const delta = fitRange(elementPos, elementPos + elementSize - 1, scrollerData.scrollPos, scrollerData.scrollPos + scrollerData.clientSize - 1);
scroll = scrollerData.scrollPos - delta;
}
/**
return clamp(Math.round(scroll), 0, maxScroll);
}
/**
* Calls scrollTo function in proper way.
*
* @param {HTMLElement} scroller - Scroller.
* @param {ScrollToOptions} options - Scroll options.
*/
function scrollToHelper(scroller, options) {
if ('scrollTo' in scroller) {
if (!supportsScrollToOptions) {
const scrollX = (options.left !== undefined ? options.left : scroller.scrollLeft);
const scrollY = (options.top !== undefined ? options.top : scroller.scrollTop);
scroller.scrollTo(scrollX, scrollY);
} else {
scroller.scrollTo(options);
}
} else if ('scrollLeft' in scroller) {
if (options.left !== undefined) {
scroller.scrollLeft = options.left;
}
if (options.top !== undefined) {
scroller.scrollTop = options.top;
}
function scrollToHelper(scroller, options) {
if ('scrollTo' in scroller) {
if (!supportsScrollToOptions) {
const scrollX = (options.left !== undefined ? options.left : scroller.scrollLeft);
const scrollY = (options.top !== undefined ? options.top : scroller.scrollTop);
scroller.scrollTo(scrollX, scrollY);
} else {
scroller.scrollTo(options);
}
} else if ('scrollLeft' in scroller) {
if (options.left !== undefined) {
scroller.scrollLeft = options.left;
}
if (options.top !== undefined) {
scroller.scrollTop = options.top;
}
}
}
/**
/**
* Performs built-in scroll.
*
* @param {HTMLElement} xScroller - Horizontal scroller.
@ -384,35 +382,35 @@ import layoutManager from './layoutManager';
* @param {number} scrollY - Vertical coordinate.
* @param {boolean} smooth - Smooth scrolling.
*/
function builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
const scrollBehavior = smooth ? 'smooth' : 'instant';
function builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
const scrollBehavior = smooth ? 'smooth' : 'instant';
if (xScroller !== yScroller) {
if (xScroller) {
scrollToHelper(xScroller, { left: scrollX, behavior: scrollBehavior });
}
if (yScroller) {
scrollToHelper(yScroller, { top: scrollY, behavior: scrollBehavior });
}
} else if (xScroller) {
scrollToHelper(xScroller, { left: scrollX, top: scrollY, behavior: scrollBehavior });
if (xScroller !== yScroller) {
if (xScroller) {
scrollToHelper(xScroller, { left: scrollX, behavior: scrollBehavior });
}
if (yScroller) {
scrollToHelper(yScroller, { top: scrollY, behavior: scrollBehavior });
}
} else if (xScroller) {
scrollToHelper(xScroller, { left: scrollX, top: scrollY, behavior: scrollBehavior });
}
}
/**
/**
* Requested frame for animated scroll.
*/
let scrollTimer;
let scrollTimer;
/**
/**
* Resets scroll timer to stop scrolling.
*/
function resetScrollTimer() {
cancelAnimationFrame(scrollTimer);
scrollTimer = undefined;
}
function resetScrollTimer() {
cancelAnimationFrame(scrollTimer);
scrollTimer = undefined;
}
/**
/**
* Performs animated scroll.
*
* @param {HTMLElement} xScroller - Horizontal scroller.
@ -420,43 +418,43 @@ import layoutManager from './layoutManager';
* @param {HTMLElement} yScroller - Vertical scroller.
* @param {number} scrollY - Vertical coordinate.
*/
function animateScroll(xScroller, scrollX, yScroller, scrollY) {
const ox = xScroller ? xScroller.scrollLeft : scrollX;
const oy = yScroller ? yScroller.scrollTop : scrollY;
const dx = scrollX - ox;
const dy = scrollY - oy;
function animateScroll(xScroller, scrollX, yScroller, scrollY) {
const ox = xScroller ? xScroller.scrollLeft : scrollX;
const oy = yScroller ? yScroller.scrollTop : scrollY;
const dx = scrollX - ox;
const dy = scrollY - oy;
if (Math.abs(dx) < Epsilon && Math.abs(dy) < Epsilon) {
if (Math.abs(dx) < Epsilon && Math.abs(dy) < Epsilon) {
return;
}
let start;
function scrollAnim(currentTimestamp) {
start = start || currentTimestamp;
let k = Math.min(1, (currentTimestamp - start) / ScrollTime);
if (k === 1) {
resetScrollTimer();
builtinScroll(xScroller, scrollX, yScroller, scrollY, false);
return;
}
let start;
k = ease(k);
function scrollAnim(currentTimestamp) {
start = start || currentTimestamp;
const x = ox + dx * k;
const y = oy + dy * k;
let k = Math.min(1, (currentTimestamp - start) / ScrollTime);
if (k === 1) {
resetScrollTimer();
builtinScroll(xScroller, scrollX, yScroller, scrollY, false);
return;
}
k = ease(k);
const x = ox + dx * k;
const y = oy + dy * k;
builtinScroll(xScroller, x, yScroller, y, false);
scrollTimer = requestAnimationFrame(scrollAnim);
}
builtinScroll(xScroller, x, yScroller, y, false);
scrollTimer = requestAnimationFrame(scrollAnim);
}
/**
scrollTimer = requestAnimationFrame(scrollAnim);
}
/**
* Performs scroll.
*
* @param {HTMLElement} xScroller - Horizontal scroller.
@ -465,142 +463,140 @@ import layoutManager from './layoutManager';
* @param {number} scrollY - Vertical coordinate.
* @param {boolean} smooth - Smooth scrolling.
*/
function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
resetScrollTimer();
function doScroll(xScroller, scrollX, yScroller, scrollY, smooth) {
resetScrollTimer();
if (smooth && useAnimatedScroll()) {
animateScroll(xScroller, scrollX, yScroller, scrollY);
} else {
builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth);
}
if (smooth && useAnimatedScroll()) {
animateScroll(xScroller, scrollX, yScroller, scrollY);
} else {
builtinScroll(xScroller, scrollX, yScroller, scrollY, smooth);
}
}
/**
/**
* Returns true if smooth scroll must be used.
*/
function useSmoothScroll() {
return !!browser.tizen;
}
function useSmoothScroll() {
return !!browser.tizen;
}
/**
/**
* Returns true if animated implementation of smooth scroll must be used.
*/
function useAnimatedScroll() {
// Add block to force using (or not) of animated implementation
function useAnimatedScroll() {
// Add block to force using (or not) of animated implementation
return !supportsSmoothScroll;
}
return !supportsSmoothScroll;
}
/**
/**
* Returns true if scroll manager is enabled.
*/
export function isEnabled() {
return layoutManager.tv;
}
export function isEnabled() {
return layoutManager.tv;
}
/**
/**
* Scrolls the document to a given position.
*
* @param {number} scrollX - Horizontal coordinate.
* @param {number} scrollY - Vertical coordinate.
* @param {boolean} [smooth=false] - Smooth scrolling.
*/
export function scrollTo(scrollX, scrollY, smooth) {
smooth = !!smooth;
export function scrollTo(scrollX, scrollY, smooth) {
smooth = !!smooth;
// Scroller is document itself by default
const scroller = getScrollableParent(null, false);
// Scroller is document itself by default
const scroller = getScrollableParent(null, false);
const xScrollerData = getScrollerData(scroller, false);
const yScrollerData = getScrollerData(scroller, true);
const xScrollerData = getScrollerData(scroller, false);
const yScrollerData = getScrollerData(scroller, true);
scrollX = clamp(Math.round(scrollX), 0, xScrollerData.scrollSize - xScrollerData.clientSize);
scrollY = clamp(Math.round(scrollY), 0, yScrollerData.scrollSize - yScrollerData.clientSize);
scrollX = clamp(Math.round(scrollX), 0, xScrollerData.scrollSize - xScrollerData.clientSize);
scrollY = clamp(Math.round(scrollY), 0, yScrollerData.scrollSize - yScrollerData.clientSize);
doScroll(scroller, scrollX, scroller, scrollY, smooth);
}
doScroll(scroller, scrollX, scroller, scrollY, smooth);
}
/**
/**
* Scrolls the document to a given element.
*
* @param {HTMLElement} element - Target element of scroll task.
* @param {boolean} [smooth=false] - Smooth scrolling.
*/
export function scrollToElement(element, smooth) {
smooth = !!smooth;
export function scrollToElement(element, smooth) {
smooth = !!smooth;
let scrollCenterX = true;
let scrollCenterY = true;
let scrollCenterX = true;
let scrollCenterY = true;
const offsetParent = element.offsetParent;
const offsetParent = element.offsetParent;
// In Firefox offsetParent.offsetParent is BODY
const isFixed = offsetParent && (!offsetParent.offsetParent || window.getComputedStyle(offsetParent).position === 'fixed');
// In Firefox offsetParent.offsetParent is BODY
const isFixed = offsetParent && (!offsetParent.offsetParent || window.getComputedStyle(offsetParent).position === 'fixed');
// Scroll fixed elements to nearest edge (or do not scroll at all)
if (isFixed) {
scrollCenterX = scrollCenterY = false;
}
let xScroller = getScrollableParent(element, false);
let yScroller = getScrollableParent(element, true);
const xScrollerData = getScrollerData(xScroller, false);
const yScrollerData = getScrollerData(yScroller, true);
// Exit, since we have no control over scrolling in this container
if (xScroller === yScroller && (xScrollerData.custom || yScrollerData.custom)) {
return;
}
// Exit, since we have no control over scrolling in these containers
if (xScrollerData.custom && yScrollerData.custom) {
return;
}
const elementRect = element.getBoundingClientRect();
let scrollX = 0;
let scrollY = 0;
if (!xScrollerData.custom) {
const xPos = getScrollerChildPos(xScroller, element, false);
scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
} else {
xScroller = null;
}
if (!yScrollerData.custom) {
const yPos = getScrollerChildPos(yScroller, element, true);
scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
// HACK: Scroll to top for top menu because it is hidden
// FIXME: Need a marker to scroll top/bottom
if (isFixed && elementRect.bottom < 0) {
scrollY = 0;
}
// HACK: Ensure we are at the top
// FIXME: Need a marker to scroll top/bottom
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
scrollY = 0;
}
} else {
yScroller = null;
}
doScroll(xScroller, scrollX, yScroller, scrollY, smooth);
// Scroll fixed elements to nearest edge (or do not scroll at all)
if (isFixed) {
scrollCenterX = scrollCenterY = false;
}
if (isEnabled()) {
dom.addEventListener(window, 'focusin', function(e) {
setTimeout(function() {
scrollToElement(e.target, useSmoothScroll());
}, 0);
}, { capture: true });
let xScroller = getScrollableParent(element, false);
let yScroller = getScrollableParent(element, true);
const xScrollerData = getScrollerData(xScroller, false);
const yScrollerData = getScrollerData(yScroller, true);
// Exit, since we have no control over scrolling in this container
if (xScroller === yScroller && (xScrollerData.custom || yScrollerData.custom)) {
return;
}
/* eslint-enable indent */
// Exit, since we have no control over scrolling in these containers
if (xScrollerData.custom && yScrollerData.custom) {
return;
}
const elementRect = element.getBoundingClientRect();
let scrollX = 0;
let scrollY = 0;
if (!xScrollerData.custom) {
const xPos = getScrollerChildPos(xScroller, element, false);
scrollX = calcScroll(xScrollerData, xPos, elementRect.width, scrollCenterX);
} else {
xScroller = null;
}
if (!yScrollerData.custom) {
const yPos = getScrollerChildPos(yScroller, element, true);
scrollY = calcScroll(yScrollerData, yPos, elementRect.height, scrollCenterY);
// HACK: Scroll to top for top menu because it is hidden
// FIXME: Need a marker to scroll top/bottom
if (isFixed && elementRect.bottom < 0) {
scrollY = 0;
}
// HACK: Ensure we are at the top
// FIXME: Need a marker to scroll top/bottom
if (scrollY < minimumScrollY() && yScroller === documentScroller) {
scrollY = 0;
}
} else {
yScroller = null;
}
doScroll(xScroller, scrollX, yScroller, scrollY, smooth);
}
if (isEnabled()) {
dom.addEventListener(window, 'focusin', function(e) {
setTimeout(function() {
scrollToElement(e.target, useSmoothScroll());
}, 0);
}, { capture: true });
}
export default {
isEnabled: isEnabled,

View file

@ -1,5 +1,3 @@
/* eslint-disable indent */
/**
* Module shortcuts.
* @module components/shortcuts
@ -14,385 +12,383 @@ import recordingHelper from './recordingcreator/recordinghelper';
import ServerConnections from './ServerConnections';
import toast from './toast/toast';
function playAllFromHere(card, serverId, queue) {
const parent = card.parentNode;
const className = card.classList.length ? (`.${card.classList[0]}`) : '';
const cards = parent.querySelectorAll(`${className}[data-id]`);
function playAllFromHere(card, serverId, queue) {
const parent = card.parentNode;
const className = card.classList.length ? (`.${card.classList[0]}`) : '';
const cards = parent.querySelectorAll(`${className}[data-id]`);
const ids = [];
const ids = [];
let foundCard = false;
let startIndex;
let foundCard = false;
let startIndex;
for (let i = 0, length = cards.length; i < length; i++) {
if (cards[i] === card) {
foundCard = true;
startIndex = i;
}
if (foundCard || !queue) {
ids.push(cards[i].getAttribute('data-id'));
for (let i = 0, length = cards.length; i < length; i++) {
if (cards[i] === card) {
foundCard = true;
startIndex = i;
}
if (foundCard || !queue) {
ids.push(cards[i].getAttribute('data-id'));
}
}
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
if (itemsContainer && itemsContainer.fetchData) {
const queryOptions = queue ? { StartIndex: startIndex } : {};
return itemsContainer.fetchData(queryOptions).then(result => {
if (queue) {
return playbackManager.queue({
items: result.Items
});
} else {
return playbackManager.play({
items: result.Items,
startIndex: startIndex
});
}
});
}
if (!ids.length) {
return;
}
if (queue) {
return playbackManager.queue({
ids: ids,
serverId: serverId
});
} else {
return playbackManager.play({
ids: ids,
serverId: serverId,
startIndex: startIndex
});
}
}
function showProgramDialog(item) {
import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
recordingCreator.show(item.Id, item.ServerId);
});
}
function getItem(button) {
button = dom.parentWithAttribute(button, 'data-id');
const serverId = button.getAttribute('data-serverid');
const id = button.getAttribute('data-id');
const type = button.getAttribute('data-type');
const apiClient = ServerConnections.getApiClient(serverId);
if (type === 'Timer') {
return apiClient.getLiveTvTimer(id);
}
if (type === 'SeriesTimer') {
return apiClient.getLiveTvSeriesTimer(id);
}
return apiClient.getItem(apiClient.getCurrentUserId(), id);
}
function notifyRefreshNeeded(childElement, itemsContainer) {
itemsContainer = itemsContainer || dom.parentWithAttribute(childElement, 'is', 'emby-itemscontainer');
if (itemsContainer) {
itemsContainer.notifyRefreshNeeded(true);
}
}
function showContextMenu(card, options) {
getItem(card).then(item => {
const playlistId = card.getAttribute('data-playlistid');
const collectionId = card.getAttribute('data-collectionid');
if (playlistId) {
const elem = dom.parentWithAttribute(card, 'data-playlistitemid');
item.PlaylistItemId = elem ? elem.getAttribute('data-playlistitemid') : null;
}
const itemsContainer = dom.parentWithClass(card, 'itemsContainer');
if (itemsContainer && itemsContainer.fetchData) {
const queryOptions = queue ? { StartIndex: startIndex } : {};
import('./itemContextMenu').then((itemContextMenu) => {
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(user => {
itemContextMenu.show(Object.assign({
item: item,
play: true,
queue: true,
playAllFromHere: !item.IsFolder,
queueAllFromHere: !item.IsFolder,
playlistId: playlistId,
collectionId: collectionId,
user: user
return itemsContainer.fetchData(queryOptions).then(result => {
if (queue) {
return playbackManager.queue({
items: result.Items
});
} else {
return playbackManager.play({
items: result.Items,
startIndex: startIndex
});
}
}, options || {})).then(result => {
if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') {
executeAction(card, options.positionTo, result.command);
} else if (result.updated || result.deleted) {
notifyRefreshNeeded(card, options.itemsContainer);
}
});
});
}
});
});
}
if (!ids.length) {
return;
function getItemInfoFromCard(card) {
return {
Type: card.getAttribute('data-type'),
Id: card.getAttribute('data-id'),
TimerId: card.getAttribute('data-timerid'),
CollectionType: card.getAttribute('data-collectiontype'),
ChannelId: card.getAttribute('data-channelid'),
SeriesId: card.getAttribute('data-seriesid'),
ServerId: card.getAttribute('data-serverid'),
MediaType: card.getAttribute('data-mediatype'),
Path: card.getAttribute('data-path'),
IsFolder: card.getAttribute('data-isfolder') === 'true',
StartDate: card.getAttribute('data-startdate'),
EndDate: card.getAttribute('data-enddate'),
UserData: {
PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0', 10)
}
};
}
if (queue) {
return playbackManager.queue({
ids: ids,
function showPlayMenu(card, target) {
const item = getItemInfoFromCard(card);
import('./playmenu').then((playMenu) => {
playMenu.show({
item: item,
positionTo: target
});
});
}
function executeAction(card, target, action) {
target = target || card;
let id = card.getAttribute('data-id');
if (!id) {
card = dom.parentWithAttribute(card, 'data-id');
id = card.getAttribute('data-id');
}
const item = getItemInfoFromCard(card);
const serverId = item.ServerId;
const type = item.Type;
const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
if (item.MediaType === 'Photo' && action === 'link') {
action = 'play';
}
if (action === 'link') {
appRouter.showItem(item, {
context: card.getAttribute('data-context'),
parentId: card.getAttribute('data-parentid')
});
} else if (action === 'programdialog') {
showProgramDialog(item);
} else if (action === 'instantmix') {
playbackManager.instantMix({
Id: playableItemId,
ServerId: serverId
});
} else if (action === 'play' || action === 'resume') {
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
if (playbackManager.canPlay(item)) {
playbackManager.play({
ids: [playableItemId],
startPositionTicks: startPositionTicks,
serverId: serverId
});
} else {
return playbackManager.play({
ids: ids,
serverId: serverId,
startIndex: startIndex
console.warn('Unable to play item', item);
}
} else if (action === 'queue') {
if (playbackManager.isPlaying()) {
playbackManager.queue({
ids: [playableItemId],
serverId: serverId
});
toast(globalize.translate('MediaQueued'));
} else {
playbackManager.queue({
ids: [playableItemId],
serverId: serverId
});
}
}
} else if (action === 'playallfromhere') {
playAllFromHere(card, serverId);
} else if (action === 'queueallfromhere') {
playAllFromHere(card, serverId, true);
} else if (action === 'setplaylistindex') {
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
} else if (action === 'record') {
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
} else if (action === 'menu') {
const options = target.getAttribute('data-playoptions') === 'false' ?
{
shuffle: false,
instantMix: false,
play: false,
playAllFromHere: false,
queue: false,
queueAllFromHere: false
} :
{};
function showProgramDialog(item) {
import('./recordingcreator/recordingcreator').then(({ default:recordingCreator }) => {
recordingCreator.show(item.Id, item.ServerId);
options.positionTo = target;
showContextMenu(card, options);
} else if (action === 'playmenu') {
showPlayMenu(card, target);
} else if (action === 'edit') {
getItem(target).then(itemToEdit => {
editItem(itemToEdit, serverId);
});
} else if (action === 'playtrailer') {
getItem(target).then(playTrailer);
} else if (action === 'addtoplaylist') {
getItem(target).then(addToPlaylist);
} else if (action === 'custom') {
const customAction = target.getAttribute('data-customaction');
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
detail: {
playlistItemId: card.getAttribute('data-playlistitemid')
},
cancelable: false,
bubbles: true
}));
}
}
function getItem(button) {
button = dom.parentWithAttribute(button, 'data-id');
const serverId = button.getAttribute('data-serverid');
const id = button.getAttribute('data-id');
const type = button.getAttribute('data-type');
function addToPlaylist(item) {
import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor().show({
items: [item.Id],
serverId: item.ServerId
const apiClient = ServerConnections.getApiClient(serverId);
if (type === 'Timer') {
return apiClient.getLiveTvTimer(id);
}
if (type === 'SeriesTimer') {
return apiClient.getLiveTvSeriesTimer(id);
}
return apiClient.getItem(apiClient.getCurrentUserId(), id);
}
function notifyRefreshNeeded(childElement, itemsContainer) {
itemsContainer = itemsContainer || dom.parentWithAttribute(childElement, 'is', 'emby-itemscontainer');
if (itemsContainer) {
itemsContainer.notifyRefreshNeeded(true);
}
}
function showContextMenu(card, options) {
getItem(card).then(item => {
const playlistId = card.getAttribute('data-playlistid');
const collectionId = card.getAttribute('data-collectionid');
if (playlistId) {
const elem = dom.parentWithAttribute(card, 'data-playlistitemid');
item.PlaylistItemId = elem ? elem.getAttribute('data-playlistitemid') : null;
}
import('./itemContextMenu').then((itemContextMenu) => {
ServerConnections.getApiClient(item.ServerId).getCurrentUser().then(user => {
itemContextMenu.show(Object.assign({
item: item,
play: true,
queue: true,
playAllFromHere: !item.IsFolder,
queueAllFromHere: !item.IsFolder,
playlistId: playlistId,
collectionId: collectionId,
user: user
}, options || {})).then(result => {
if (result.command === 'playallfromhere' || result.command === 'queueallfromhere') {
executeAction(card, options.positionTo, result.command);
} else if (result.updated || result.deleted) {
notifyRefreshNeeded(card, options.itemsContainer);
}
});
});
});
});
}
});
}
function getItemInfoFromCard(card) {
return {
Type: card.getAttribute('data-type'),
Id: card.getAttribute('data-id'),
TimerId: card.getAttribute('data-timerid'),
CollectionType: card.getAttribute('data-collectiontype'),
ChannelId: card.getAttribute('data-channelid'),
SeriesId: card.getAttribute('data-seriesid'),
ServerId: card.getAttribute('data-serverid'),
MediaType: card.getAttribute('data-mediatype'),
Path: card.getAttribute('data-path'),
IsFolder: card.getAttribute('data-isfolder') === 'true',
StartDate: card.getAttribute('data-startdate'),
EndDate: card.getAttribute('data-enddate'),
UserData: {
PlaybackPositionTicks: parseInt(card.getAttribute('data-positionticks') || '0', 10)
}
};
}
function playTrailer(item) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
function showPlayMenu(card, target) {
const item = getItemInfoFromCard(card);
apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(trailers => {
playbackManager.play({ items: trailers });
});
}
import('./playmenu').then((playMenu) => {
playMenu.show({
function editItem(item, serverId) {
const apiClient = ServerConnections.getApiClient(serverId);
item: item,
positionTo: target
});
});
}
return new Promise((resolve, reject) => {
const currentServerId = apiClient.serverInfo().Id;
function executeAction(card, target, action) {
target = target || card;
let id = card.getAttribute('data-id');
if (!id) {
card = dom.parentWithAttribute(card, 'data-id');
id = card.getAttribute('data-id');
}
const item = getItemInfoFromCard(card);
const serverId = item.ServerId;
const type = item.Type;
const playableItemId = type === 'Program' ? item.ChannelId : item.Id;
if (item.MediaType === 'Photo' && action === 'link') {
action = 'play';
}
if (action === 'link') {
appRouter.showItem(item, {
context: card.getAttribute('data-context'),
parentId: card.getAttribute('data-parentid')
});
} else if (action === 'programdialog') {
showProgramDialog(item);
} else if (action === 'instantmix') {
playbackManager.instantMix({
Id: playableItemId,
ServerId: serverId
});
} else if (action === 'play' || action === 'resume') {
const startPositionTicks = parseInt(card.getAttribute('data-positionticks') || '0', 10);
if (playbackManager.canPlay(item)) {
playbackManager.play({
ids: [playableItemId],
startPositionTicks: startPositionTicks,
serverId: serverId
if (item.Type === 'Timer') {
if (item.ProgramId) {
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
});
} else {
console.warn('Unable to play item', item);
}
} else if (action === 'queue') {
if (playbackManager.isPlaying()) {
playbackManager.queue({
ids: [playableItemId],
serverId: serverId
});
toast(globalize.translate('MediaQueued'));
} else {
playbackManager.queue({
ids: [playableItemId],
serverId: serverId
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
});
}
} else if (action === 'playallfromhere') {
playAllFromHere(card, serverId);
} else if (action === 'queueallfromhere') {
playAllFromHere(card, serverId, true);
} else if (action === 'setplaylistindex') {
playbackManager.setCurrentPlaylistItem(card.getAttribute('data-playlistitemid'));
} else if (action === 'record') {
onRecordCommand(serverId, id, type, card.getAttribute('data-timerid'), card.getAttribute('data-seriestimerid'));
} else if (action === 'menu') {
const options = target.getAttribute('data-playoptions') === 'false' ?
{
shuffle: false,
instantMix: false,
play: false,
playAllFromHere: false,
queue: false,
queueAllFromHere: false
} :
{};
options.positionTo = target;
showContextMenu(card, options);
} else if (action === 'playmenu') {
showPlayMenu(card, target);
} else if (action === 'edit') {
getItem(target).then(itemToEdit => {
editItem(itemToEdit, serverId);
} else {
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
});
} else if (action === 'playtrailer') {
getItem(target).then(playTrailer);
} else if (action === 'addtoplaylist') {
getItem(target).then(addToPlaylist);
} else if (action === 'custom') {
const customAction = target.getAttribute('data-customaction');
}
});
}
card.dispatchEvent(new CustomEvent(`action-${customAction}`, {
detail: {
playlistItemId: card.getAttribute('data-playlistitemid')
},
cancelable: false,
bubbles: true
}));
function onRecordCommand(serverId, id, type, timerId, seriesTimerId) {
if (type === 'Program' || timerId || seriesTimerId) {
const programId = type === 'Program' ? id : null;
recordingHelper.toggleRecording(serverId, programId, timerId, seriesTimerId);
}
}
export function onClick(e) {
const card = dom.parentWithClass(e.target, 'itemAction');
if (card) {
let actionElement = card;
let action = actionElement.getAttribute('data-action');
if (!action) {
actionElement = dom.parentWithAttribute(actionElement, 'data-action');
if (actionElement) {
action = actionElement.getAttribute('data-action');
}
}
if (action) {
executeAction(card, actionElement, action);
e.preventDefault();
e.stopPropagation();
return false;
}
}
}
function addToPlaylist(item) {
import('./playlisteditor/playlisteditor').then(({ default: playlistEditor }) => {
new playlistEditor().show({
items: [item.Id],
serverId: item.ServerId
function onCommand(e) {
const cmd = e.detail.command;
});
});
}
function playTrailer(item) {
const apiClient = ServerConnections.getApiClient(item.ServerId);
apiClient.getLocalTrailers(apiClient.getCurrentUserId(), item.Id).then(trailers => {
playbackManager.play({ items: trailers });
});
}
function editItem(item, serverId) {
const apiClient = ServerConnections.getApiClient(serverId);
return new Promise((resolve, reject) => {
const currentServerId = apiClient.serverInfo().Id;
if (item.Type === 'Timer') {
if (item.ProgramId) {
import('./recordingcreator/recordingcreator').then(({ default: recordingCreator }) => {
recordingCreator.show(item.ProgramId, currentServerId).then(resolve, reject);
});
} else {
import('./recordingcreator/recordingeditor').then(({ default: recordingEditor }) => {
recordingEditor.show(item.Id, currentServerId).then(resolve, reject);
});
}
} else {
import('./metadataEditor/metadataEditor').then(({ default: metadataEditor }) => {
metadataEditor.show(item.Id, currentServerId).then(resolve, reject);
});
}
});
}
function onRecordCommand(serverId, id, type, timerId, seriesTimerId) {
if (type === 'Program' || timerId || seriesTimerId) {
const programId = type === 'Program' ? id : null;
recordingHelper.toggleRecording(serverId, programId, timerId, seriesTimerId);
}
}
export function onClick(e) {
const card = dom.parentWithClass(e.target, 'itemAction');
if (cmd === 'play' || cmd === 'resume' || cmd === 'record' || cmd === 'menu' || cmd === 'info') {
const target = e.target;
const card = dom.parentWithClass(target, 'itemAction') || dom.parentWithAttribute(target, 'data-id');
if (card) {
let actionElement = card;
let action = actionElement.getAttribute('data-action');
if (!action) {
actionElement = dom.parentWithAttribute(actionElement, 'data-action');
if (actionElement) {
action = actionElement.getAttribute('data-action');
}
}
if (action) {
executeAction(card, actionElement, action);
e.preventDefault();
e.stopPropagation();
return false;
}
e.preventDefault();
e.stopPropagation();
executeAction(card, card, cmd);
}
}
}
function onCommand(e) {
const cmd = e.detail.command;
export function on(context, options) {
options = options || {};
if (cmd === 'play' || cmd === 'resume' || cmd === 'record' || cmd === 'menu' || cmd === 'info') {
const target = e.target;
const card = dom.parentWithClass(target, 'itemAction') || dom.parentWithAttribute(target, 'data-id');
if (card) {
e.preventDefault();
e.stopPropagation();
executeAction(card, card, cmd);
}
}
if (options.click !== false) {
context.addEventListener('click', onClick);
}
export function on(context, options) {
options = options || {};
if (options.command !== false) {
inputManager.on(context, onCommand);
}
}
if (options.click !== false) {
context.addEventListener('click', onClick);
}
export function off(context, options) {
options = options || {};
if (options.command !== false) {
inputManager.on(context, onCommand);
}
context.removeEventListener('click', onClick);
if (options.command !== false) {
inputManager.off(context, onCommand);
}
}
export function getShortcutAttributesHtml(item, serverId) {
let html = `data-id="${item.Id}" data-serverid="${serverId || item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-channelid="${item.ChannelId}" data-isfolder="${item.IsFolder}"`;
const collectionType = item.CollectionType;
if (collectionType) {
html += ` data-collectiontype="${collectionType}"`;
}
export function off(context, options) {
options = options || {};
context.removeEventListener('click', onClick);
if (options.command !== false) {
inputManager.off(context, onCommand);
}
}
export function getShortcutAttributesHtml(item, serverId) {
let html = `data-id="${item.Id}" data-serverid="${serverId || item.ServerId}" data-type="${item.Type}" data-mediatype="${item.MediaType}" data-channelid="${item.ChannelId}" data-isfolder="${item.IsFolder}"`;
const collectionType = item.CollectionType;
if (collectionType) {
html += ` data-collectiontype="${collectionType}"`;
}
return html;
}
/* eslint-enable indent */
return html;
}
export default {
on: on,

View file

@ -0,0 +1,114 @@
import { clearBackdrop } from '../backdrop/backdrop';
import * as mainTabsManager from '../maintabsmanager';
import layoutManager from '../layoutManager';
import '../../elements/emby-tabs/emby-tabs';
import LibraryMenu from '../../scripts/libraryMenu';
function onViewDestroy() {
const tabControllers = this.tabControllers;
if (tabControllers) {
tabControllers.forEach(function (t) {
if (t.destroy) {
t.destroy();
}
});
this.tabControllers = null;
}
this.view = null;
this.params = null;
this.currentTabController = null;
this.initialTabIndex = null;
}
class TabbedView {
constructor(view, params) {
this.tabControllers = [];
this.view = view;
this.params = params;
const self = this;
let currentTabIndex = parseInt(params.tab || this.getDefaultTabIndex(params.parentId), 10);
this.initialTabIndex = currentTabIndex;
function validateTabLoad(index) {
return self.validateTabLoad ? self.validateTabLoad(index) : Promise.resolve();
}
function loadTab(index, previousIndex) {
validateTabLoad(index).then(function () {
self.getTabController(index).then(function (controller) {
const refresh = !controller.refreshed;
controller.onResume({
autoFocus: previousIndex == null && layoutManager.tv,
refresh: refresh
});
controller.refreshed = true;
currentTabIndex = index;
self.currentTabController = controller;
});
});
}
function getTabContainers() {
return view.querySelectorAll('.tabContent');
}
function onTabChange(e) {
const newIndex = parseInt(e.detail.selectedTabIndex, 10);
const previousIndex = e.detail.previousIndex;
const previousTabController = previousIndex == null ? null : self.tabControllers[previousIndex];
if (previousTabController && previousTabController.onPause) {
previousTabController.onPause();
}
loadTab(newIndex, previousIndex);
}
view.addEventListener('viewbeforehide', this.onPause.bind(this));
view.addEventListener('viewbeforeshow', function () {
mainTabsManager.setTabs(view, currentTabIndex, self.getTabs, getTabContainers, null, onTabChange, false);
});
view.addEventListener('viewshow', function (e) {
self.onResume(e.detail);
});
view.addEventListener('viewdestroy', onViewDestroy.bind(this));
}
onResume() {
this.setTitle();
clearBackdrop();
const currentTabController = this.currentTabController;
if (!currentTabController) {
mainTabsManager.selectedTabIndex(this.initialTabIndex);
} else if (currentTabController && currentTabController.onResume) {
currentTabController.onResume({});
}
}
onPause() {
const currentTabController = this.currentTabController;
if (currentTabController && currentTabController.onPause) {
currentTabController.onPause();
}
}
setTitle() {
LibraryMenu.setTitle('');
}
}
export default TabbedView;

View file

@ -10,199 +10,197 @@ import './upnextdialog.scss';
import '../../elements/emby-button/emby-button';
import '../../styles/flexstyles.scss';
/* eslint-disable indent */
const transitionEndEventName = dom.whichTransitionEvent();
const transitionEndEventName = dom.whichTransitionEvent();
function getHtml() {
let html = '';
function getHtml() {
let html = '';
html += '<div class="flex flex-direction-column flex-grow">';
html += '<div class="flex flex-direction-column flex-grow">';
html += '<h2 class="upNextDialog-nextVideoText" style="margin:.25em 0;">&nbsp;</h2>';
html += '<h2 class="upNextDialog-nextVideoText" style="margin:.25em 0;">&nbsp;</h2>';
html += '<h3 class="upNextDialog-title" style="margin:.25em 0 .5em;"></h3>';
html += '<h3 class="upNextDialog-title" style="margin:.25em 0 .5em;"></h3>';
html += '<div class="flex flex-direction-row upNextDialog-mediainfo">';
html += '</div>';
html += '<div class="flex flex-direction-row upNextDialog-mediainfo">';
html += '</div>';
html += '<div class="flex flex-direction-row upNextDialog-buttons" style="margin-top:1em;">';
html += '<div class="flex flex-direction-row upNextDialog-buttons" style="margin-top:1em;">';
html += '<button type="button" is="emby-button" class="raised raised-mini btnStartNow upNextDialog-button">';
html += globalize.translate('HeaderStartNow');
html += '</button>';
html += '<button type="button" is="emby-button" class="raised raised-mini btnStartNow upNextDialog-button">';
html += globalize.translate('HeaderStartNow');
html += '</button>';
html += '<button type="button" is="emby-button" class="raised raised-mini btnHide upNextDialog-button">';
html += globalize.translate('Hide');
html += '</button>';
html += '<button type="button" is="emby-button" class="raised raised-mini btnHide upNextDialog-button">';
html += globalize.translate('Hide');
html += '</button>';
// buttons
html += '</div>';
// buttons
html += '</div>';
// main
html += '</div>';
// main
html += '</div>';
return html;
}
return html;
function setNextVideoText() {
const instance = this;
const elem = instance.options.parent;
const secondsRemaining = Math.max(Math.round(getTimeRemainingMs(instance) / 1000), 0);
console.debug('up next seconds remaining: ' + secondsRemaining);
const timeText = '<span class="upNextDialog-countdownText">' + globalize.translate('HeaderSecondsValue', secondsRemaining) + '</span>';
const nextVideoText = instance.itemType === 'Episode' ?
globalize.translate('HeaderNextEpisodePlayingInValue', timeText) :
globalize.translate('HeaderNextVideoPlayingInValue', timeText);
elem.querySelector('.upNextDialog-nextVideoText').innerHTML = nextVideoText;
}
function fillItem(item) {
const instance = this;
const elem = instance.options.parent;
elem.querySelector('.upNextDialog-mediainfo').innerHTML = mediaInfo.getPrimaryMediaInfoHtml(item, {
criticRating: true,
originalAirDate: false,
starRating: true,
subtitles: false
});
let title = itemHelper.getDisplayName(item);
if (item.SeriesName) {
title = item.SeriesName + ' - ' + title;
}
function setNextVideoText() {
const instance = this;
elem.querySelector('.upNextDialog-title').innerText = title || '';
const elem = instance.options.parent;
instance.itemType = item.Type;
const secondsRemaining = Math.max(Math.round(getTimeRemainingMs(instance) / 1000), 0);
instance.show();
}
console.debug('up next seconds remaining: ' + secondsRemaining);
function clearCountdownTextTimeout(instance) {
if (instance._countdownTextTimeout) {
clearInterval(instance._countdownTextTimeout);
instance._countdownTextTimeout = null;
}
}
const timeText = '<span class="upNextDialog-countdownText">' + globalize.translate('HeaderSecondsValue', secondsRemaining) + '</span>';
async function onStartNowClick() {
const options = this.options;
const nextVideoText = instance.itemType === 'Episode' ?
globalize.translate('HeaderNextEpisodePlayingInValue', timeText) :
globalize.translate('HeaderNextVideoPlayingInValue', timeText);
if (options) {
const player = options.player;
elem.querySelector('.upNextDialog-nextVideoText').innerHTML = nextVideoText;
await this.hide();
playbackManager.nextTrack(player);
}
}
function init(instance, options) {
options.parent.innerHTML = getHtml();
options.parent.classList.add('hide');
options.parent.classList.add('upNextDialog');
options.parent.classList.add('upNextDialog-hidden');
fillItem.call(instance, options.nextItem);
options.parent.querySelector('.btnHide').addEventListener('click', instance.hide.bind(instance));
options.parent.querySelector('.btnStartNow').addEventListener('click', onStartNowClick.bind(instance));
}
function clearHideAnimationEventListeners(instance, elem) {
const fn = instance._onHideAnimationComplete;
if (fn) {
dom.removeEventListener(elem, transitionEndEventName, fn, {
once: true
});
}
}
function onHideAnimationComplete(e) {
const instance = this;
const elem = e.target;
elem.classList.add('hide');
clearHideAnimationEventListeners(instance, elem);
Events.trigger(instance, 'hide');
}
async function hideComingUpNext() {
const instance = this;
clearCountdownTextTimeout(this);
if (!instance.options) {
return;
}
function fillItem(item) {
const instance = this;
const elem = instance.options.parent;
const elem = instance.options.parent;
if (!elem) {
return;
}
elem.querySelector('.upNextDialog-mediainfo').innerHTML = mediaInfo.getPrimaryMediaInfoHtml(item, {
criticRating: false,
originalAirDate: false,
starRating: false,
subtitles: false
clearHideAnimationEventListeners(this, elem);
if (elem.classList.contains('upNextDialog-hidden')) {
return;
}
const fn = onHideAnimationComplete.bind(instance);
instance._onHideAnimationComplete = fn;
const transitionEvent = await new Promise((resolve) => {
dom.addEventListener(elem, transitionEndEventName, resolve, {
once: true
});
let title = itemHelper.getDisplayName(item);
if (item.SeriesName) {
title = item.SeriesName + ' - ' + title;
}
// trigger a reflow to force it to animate again
void elem.offsetWidth;
elem.querySelector('.upNextDialog-title').innerText = title || '';
elem.classList.add('upNextDialog-hidden');
});
instance.itemType = item.Type;
instance._onHideAnimationComplete(transitionEvent);
}
instance.show();
}
function getTimeRemainingMs(instance) {
const options = instance.options;
if (options) {
const runtimeTicks = playbackManager.duration(options.player);
function clearCountdownTextTimeout(instance) {
if (instance._countdownTextTimeout) {
clearInterval(instance._countdownTextTimeout);
instance._countdownTextTimeout = null;
if (runtimeTicks) {
const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000;
return Math.round(timeRemainingTicks / 10000);
}
}
async function onStartNowClick() {
const options = this.options;
return 0;
}
if (options) {
const player = options.player;
function startComingUpNextHideTimer(instance) {
const timeRemainingMs = getTimeRemainingMs(instance);
await this.hide();
playbackManager.nextTrack(player);
}
if (timeRemainingMs <= 0) {
return;
}
function init(instance, options) {
options.parent.innerHTML = getHtml();
setNextVideoText.call(instance);
clearCountdownTextTimeout(instance);
options.parent.classList.add('hide');
options.parent.classList.add('upNextDialog');
options.parent.classList.add('upNextDialog-hidden');
fillItem.call(instance, options.nextItem);
options.parent.querySelector('.btnHide').addEventListener('click', instance.hide.bind(instance));
options.parent.querySelector('.btnStartNow').addEventListener('click', onStartNowClick.bind(instance));
}
function clearHideAnimationEventListeners(instance, elem) {
const fn = instance._onHideAnimationComplete;
if (fn) {
dom.removeEventListener(elem, transitionEndEventName, fn, {
once: true
});
}
}
function onHideAnimationComplete(e) {
const instance = this;
const elem = e.target;
elem.classList.add('hide');
clearHideAnimationEventListeners(instance, elem);
Events.trigger(instance, 'hide');
}
async function hideComingUpNext() {
const instance = this;
clearCountdownTextTimeout(this);
if (!instance.options) {
return;
}
const elem = instance.options.parent;
if (!elem) {
return;
}
clearHideAnimationEventListeners(this, elem);
if (elem.classList.contains('upNextDialog-hidden')) {
return;
}
const fn = onHideAnimationComplete.bind(instance);
instance._onHideAnimationComplete = fn;
const transitionEvent = await new Promise((resolve) => {
dom.addEventListener(elem, transitionEndEventName, resolve, {
once: true
});
// trigger a reflow to force it to animate again
void elem.offsetWidth;
elem.classList.add('upNextDialog-hidden');
});
instance._onHideAnimationComplete(transitionEvent);
}
function getTimeRemainingMs(instance) {
const options = instance.options;
if (options) {
const runtimeTicks = playbackManager.duration(options.player);
if (runtimeTicks) {
const timeRemainingTicks = runtimeTicks - playbackManager.currentTime(options.player) * 10000;
return Math.round(timeRemainingTicks / 10000);
}
}
return 0;
}
function startComingUpNextHideTimer(instance) {
const timeRemainingMs = getTimeRemainingMs(instance);
if (timeRemainingMs <= 0) {
return;
}
setNextVideoText.call(instance);
clearCountdownTextTimeout(instance);
instance._countdownTextTimeout = setInterval(setNextVideoText.bind(instance), 400);
}
instance._countdownTextTimeout = setInterval(setNextVideoText.bind(instance), 400);
}
class UpNextDialog {
constructor(options) {
@ -243,4 +241,3 @@ class UpNextDialog {
export default UpNextDialog;
/* eslint-enable indent */

View file

@ -2,243 +2,252 @@ import { importModule } from '@uupaa/dynamic-import-polyfill';
import './viewManager/viewContainer.scss';
import Dashboard from '../utils/dashboard';
/* eslint-disable indent */
const getMainAnimatedPages = () => {
if (!mainAnimatedPages) {
mainAnimatedPages = document.querySelector('.mainAnimatedPages');
}
function setControllerClass(view, options) {
if (options.controllerFactory) {
return Promise.resolve();
}
let controllerUrl = view.getAttribute('data-controller');
if (controllerUrl) {
if (controllerUrl.indexOf('__plugin/') === 0) {
controllerUrl = controllerUrl.substring('__plugin/'.length);
}
controllerUrl = Dashboard.getPluginUrl(controllerUrl);
const apiUrl = ApiClient.getUrl('/web/' + controllerUrl);
return importModule(apiUrl).then((ControllerFactory) => {
options.controllerFactory = ControllerFactory;
});
}
return mainAnimatedPages;
};
function setControllerClass(view, options) {
if (options.controllerFactory) {
return Promise.resolve();
}
export function loadView(options) {
if (!options.cancel) {
let controllerUrl = view.getAttribute('data-controller');
if (controllerUrl) {
if (controllerUrl.indexOf('__plugin/') === 0) {
controllerUrl = controllerUrl.substring('__plugin/'.length);
}
controllerUrl = Dashboard.getPluginUrl(controllerUrl);
const apiUrl = ApiClient.getUrl('/web/' + controllerUrl);
return importModule(apiUrl).then((ControllerFactory) => {
options.controllerFactory = ControllerFactory;
});
}
return Promise.resolve();
}
export function loadView(options) {
if (!options.cancel) {
const selected = selectedPageIndex;
const previousAnimatable = selected === -1 ? null : allPages[selected];
let pageIndex = selected + 1;
if (pageIndex >= pageContainerCount) {
pageIndex = 0;
}
const isPluginpage = options.url.includes('configurationpage');
const newViewInfo = normalizeNewView(options, isPluginpage);
const newView = newViewInfo.elem;
const currentPage = allPages[pageIndex];
if (currentPage) {
triggerDestroy(currentPage);
}
let view = newView;
if (typeof view == 'string') {
view = document.createElement('div');
view.innerHTML = newView;
}
view.classList.add('mainAnimatedPage');
if (!getMainAnimatedPages()) {
console.warn('[viewContainer] main animated pages element is not present');
return;
}
if (currentPage) {
if (newViewInfo.hasScript && window.$) {
mainAnimatedPages.removeChild(currentPage);
view = $(view).appendTo(mainAnimatedPages)[0];
} else {
mainAnimatedPages.replaceChild(view, currentPage);
}
} else {
if (newViewInfo.hasScript && window.$) {
view = $(view).appendTo(mainAnimatedPages)[0];
} else {
mainAnimatedPages.appendChild(view);
}
}
if (options.type) {
view.setAttribute('data-type', options.type);
}
const properties = [];
if (options.fullscreen) {
properties.push('fullscreen');
}
if (properties.length) {
view.setAttribute('data-properties', properties.join(','));
}
allPages[pageIndex] = view;
return setControllerClass(view, options)
// Timeout for polyfilled CustomElements (webOS 1.2)
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
.then(() => {
if (onBeforeChange) {
onBeforeChange(view, false, options);
}
beforeAnimate(allPages, pageIndex, selected);
selectedPageIndex = pageIndex;
currentUrls[pageIndex] = options.url;
if (!options.cancel && previousAnimatable) {
afterAnimate(allPages, pageIndex);
}
if (window.$) {
$.mobile = $.mobile || {};
$.mobile.activePage = view;
}
return view;
});
}
}
function parseHtml(html, hasScript) {
if (hasScript) {
html = html
.replaceAll('\x3c!--<script', '<script')
.replaceAll('</script>--\x3e', '</script>');
}
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
return wrapper.querySelector('div[data-role="page"]');
}
function normalizeNewView(options, isPluginpage) {
const viewHtml = options.view;
if (viewHtml.indexOf('data-role="page"') === -1) {
return viewHtml;
}
let hasScript = viewHtml.indexOf('<script') !== -1;
const elem = parseHtml(viewHtml, hasScript);
if (hasScript) {
hasScript = elem.querySelector('script') != null;
}
let hasjQuery = false;
let hasjQuerySelect = false;
let hasjQueryChecked = false;
if (isPluginpage) {
hasjQuery = viewHtml.indexOf('jQuery') != -1 || viewHtml.indexOf('$(') != -1 || viewHtml.indexOf('$.') != -1;
hasjQueryChecked = viewHtml.indexOf('.checked(') != -1;
hasjQuerySelect = viewHtml.indexOf('.selectmenu(') != -1;
}
return {
elem: elem,
hasScript: hasScript,
hasjQuerySelect: hasjQuerySelect,
hasjQueryChecked: hasjQueryChecked,
hasjQuery: hasjQuery
};
}
function beforeAnimate(allPages, newPageIndex, oldPageIndex) {
for (let index = 0, length = allPages.length; index < length; index++) {
if (newPageIndex !== index && oldPageIndex !== index) {
allPages[index].classList.add('hide');
}
}
}
function afterAnimate(allPages, newPageIndex) {
for (let index = 0, length = allPages.length; index < length; index++) {
if (newPageIndex !== index) {
allPages[index].classList.add('hide');
}
}
}
export function setOnBeforeChange(fn) {
onBeforeChange = fn;
}
export function tryRestoreView(options) {
const url = options.url;
const index = currentUrls.indexOf(url);
if (index !== -1) {
const animatable = allPages[index];
const view = animatable;
if (view) {
if (options.cancel) {
return;
}
const selected = selectedPageIndex;
const previousAnimatable = selected === -1 ? null : allPages[selected];
let pageIndex = selected + 1;
if (pageIndex >= pageContainerCount) {
pageIndex = 0;
}
const isPluginpage = options.url.includes('configurationpage');
const newViewInfo = normalizeNewView(options, isPluginpage);
const newView = newViewInfo.elem;
const currentPage = allPages[pageIndex];
if (currentPage) {
triggerDestroy(currentPage);
}
let view = newView;
if (typeof view == 'string') {
view = document.createElement('div');
view.innerHTML = newView;
}
view.classList.add('mainAnimatedPage');
if (currentPage) {
if (newViewInfo.hasScript && window.$) {
mainAnimatedPages.removeChild(currentPage);
view = $(view).appendTo(mainAnimatedPages)[0];
} else {
mainAnimatedPages.replaceChild(view, currentPage);
}
} else {
if (newViewInfo.hasScript && window.$) {
view = $(view).appendTo(mainAnimatedPages)[0];
} else {
mainAnimatedPages.appendChild(view);
}
}
if (options.type) {
view.setAttribute('data-type', options.type);
}
const properties = [];
if (options.fullscreen) {
properties.push('fullscreen');
}
if (properties.length) {
view.setAttribute('data-properties', properties.join(','));
}
allPages[pageIndex] = view;
return setControllerClass(view, options)
// Timeout for polyfilled CustomElements (webOS 1.2)
.then(() => new Promise((resolve) => setTimeout(resolve, 0)))
.then(() => {
if (onBeforeChange) {
onBeforeChange(view, false, options);
}
beforeAnimate(allPages, pageIndex, selected);
selectedPageIndex = pageIndex;
currentUrls[pageIndex] = options.url;
if (!options.cancel && previousAnimatable) {
afterAnimate(allPages, pageIndex);
}
if (window.$) {
$.mobile = $.mobile || {};
$.mobile.activePage = view;
}
return view;
});
}
}
function parseHtml(html, hasScript) {
if (hasScript) {
html = html
.replaceAll('\x3c!--<script', '<script')
.replaceAll('</script>--\x3e', '</script>');
}
const wrapper = document.createElement('div');
wrapper.innerHTML = html;
return wrapper.querySelector('div[data-role="page"]');
}
function normalizeNewView(options, isPluginpage) {
const viewHtml = options.view;
if (viewHtml.indexOf('data-role="page"') === -1) {
return viewHtml;
}
let hasScript = viewHtml.indexOf('<script') !== -1;
const elem = parseHtml(viewHtml, hasScript);
if (hasScript) {
hasScript = elem.querySelector('script') != null;
}
let hasjQuery = false;
let hasjQuerySelect = false;
let hasjQueryChecked = false;
if (isPluginpage) {
hasjQuery = viewHtml.indexOf('jQuery') != -1 || viewHtml.indexOf('$(') != -1 || viewHtml.indexOf('$.') != -1;
hasjQueryChecked = viewHtml.indexOf('.checked(') != -1;
hasjQuerySelect = viewHtml.indexOf('.selectmenu(') != -1;
}
return {
elem: elem,
hasScript: hasScript,
hasjQuerySelect: hasjQuerySelect,
hasjQueryChecked: hasjQueryChecked,
hasjQuery: hasjQuery
};
}
function beforeAnimate(allPages, newPageIndex, oldPageIndex) {
for (let index = 0, length = allPages.length; index < length; index++) {
if (newPageIndex !== index && oldPageIndex !== index) {
allPages[index].classList.add('hide');
}
}
}
function afterAnimate(allPages, newPageIndex) {
for (let index = 0, length = allPages.length; index < length; index++) {
if (newPageIndex !== index) {
allPages[index].classList.add('hide');
}
}
}
export function setOnBeforeChange(fn) {
onBeforeChange = fn;
}
export function tryRestoreView(options) {
const url = options.url;
const index = currentUrls.indexOf(url);
if (index !== -1) {
const animatable = allPages[index];
const view = animatable;
if (view) {
if (options.cancel) {
return;
return setControllerClass(view, options).then(() => {
if (onBeforeChange) {
onBeforeChange(view, true, options);
}
const selected = selectedPageIndex;
const previousAnimatable = selected === -1 ? null : allPages[selected];
return setControllerClass(view, options).then(() => {
if (onBeforeChange) {
onBeforeChange(view, true, options);
}
beforeAnimate(allPages, index, selected);
animatable.classList.remove('hide');
selectedPageIndex = index;
beforeAnimate(allPages, index, selected);
animatable.classList.remove('hide');
selectedPageIndex = index;
if (!options.cancel && previousAnimatable) {
afterAnimate(allPages, index);
}
if (!options.cancel && previousAnimatable) {
afterAnimate(allPages, index);
}
if (window.$) {
$.mobile = $.mobile || {};
$.mobile.activePage = view;
}
if (window.$) {
$.mobile = $.mobile || {};
$.mobile.activePage = view;
}
return view;
});
}
return view;
});
}
return Promise.reject();
}
function triggerDestroy(view) {
view.dispatchEvent(new CustomEvent('viewdestroy', {}));
}
return Promise.reject();
}
export function reset() {
allPages = [];
currentUrls = [];
mainAnimatedPages.innerHTML = '';
selectedPageIndex = -1;
}
function triggerDestroy(view) {
view.dispatchEvent(new CustomEvent('viewdestroy', {}));
}
let onBeforeChange;
const mainAnimatedPages = document.querySelector('.mainAnimatedPages');
let allPages = [];
let currentUrls = [];
const pageContainerCount = 3;
let selectedPageIndex = -1;
reset();
mainAnimatedPages.classList.remove('hide');
export function reset() {
allPages = [];
currentUrls = [];
if (mainAnimatedPages) mainAnimatedPages.innerHTML = '';
selectedPageIndex = -1;
}
/* eslint-enable indent */
let onBeforeChange;
let mainAnimatedPages;
let allPages = [];
let currentUrls = [];
const pageContainerCount = 3;
let selectedPageIndex = -1;
reset();
getMainAnimatedPages()?.classList.remove('hide');
export default {
loadView: loadView,