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

Merge remote-tracking branch 'upstream/master' into unavailable-yt-video-trailer-bug-fix

This commit is contained in:
Dmitry Lyzo 2020-10-29 21:44:38 +03:00
commit 7774a4aa00
313 changed files with 12147 additions and 10106 deletions

View file

@ -1,5 +1,4 @@
/* eslint-disable indent */
import connectionManager from 'connectionManager';
class BackdropScreensaver {
constructor() {
@ -21,7 +20,7 @@ class BackdropScreensaver {
Limit: 200
};
const apiClient = connectionManager.currentApiClient();
const apiClient = window.connectionManager.currentApiClient();
apiClient.getItems(apiClient.getCurrentUserId(), query).then((result) => {
if (result.Items.length) {
import('slideshow').then(({default: Slideshow}) => {

View file

@ -1,4 +1,4 @@
import connectionManager from 'connectionManager';
import browser from 'browser';
import loading from 'loading';
import keyboardnavigation from 'keyboardnavigation';
import dialogHelper from 'dialogHelper';
@ -19,6 +19,8 @@ export class BookPlayer {
this.onDialogClosed = this.onDialogClosed.bind(this);
this.openTableOfContents = this.openTableOfContents.bind(this);
this.prevChapter = this.prevChapter.bind(this);
this.nextChapter = this.nextChapter.bind(this);
this.onWindowKeyUp = this.onWindowKeyUp.bind(this);
}
@ -123,26 +125,6 @@ export class BookPlayer {
}
}
onTouchStart(e) {
// TODO: depending on the event this can be the document or the rendition itself
const rendition = this._rendition || this;
const book = rendition.book;
// check that the event is from the book or the document
if (!book || this._loaded === false) return;
// epubjs stores pages off the screen or something for preloading
// get the modulus of the touch event to account for the increased width
if (!e.touches || e.touches.length === 0) return;
const touch = e.touches[0].clientX % dom.getWindowSize().innerWidth;
if (touch < dom.getWindowSize().innerWidth / 2) {
book.package.metadata.direction === 'rtl' ? rendition.next() : rendition.prev();
} else {
book.package.metadata.direction === 'rtl' ? rendition.prev() : rendition.next();
}
}
onDialogClosed() {
this.stop();
}
@ -151,27 +133,33 @@ export class BookPlayer {
const elem = this._mediaElement;
elem.addEventListener('close', this.onDialogClosed, {once: true});
elem.querySelector('.btnBookplayerExit').addEventListener('click', this.onDialogClosed, {once: true});
elem.querySelector('.btnBookplayerToc').addEventListener('click', this.openTableOfContents);
elem.querySelector('#btnBookplayerExit').addEventListener('click', this.onDialogClosed, {once: true});
elem.querySelector('#btnBookplayerToc').addEventListener('click', this.openTableOfContents);
if (browser.mobile) {
elem.querySelector('#btnBookplayerPrev').addEventListener('click', this.prevChapter);
elem.querySelector('#btnBookplayerNext').addEventListener('click', this.nextChapter);
}
}
bindEvents() {
this.bindMediaElementEvents();
document.addEventListener('keyup', this.onWindowKeyUp);
document.addEventListener('touchstart', this.onTouchStart);
// FIXME: I don't really get why document keyup event is not triggered when epub is in focus
this._rendition.on('keyup', this.onWindowKeyUp);
this._rendition.on('touchstart', this.onTouchStart);
}
unbindMediaElementEvents() {
const elem = this._mediaElement;
elem.removeEventListener('close', this.onDialogClosed);
elem.querySelector('.btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
elem.querySelector('.btnBookplayerToc').removeEventListener('click', this.openTableOfContents);
elem.querySelector('#btnBookplayerExit').removeEventListener('click', this.onDialogClosed);
elem.querySelector('#btnBookplayerToc').removeEventListener('click', this.openTableOfContents);
if (browser.mobile) {
elem.querySelector('#btnBookplayerPrev').removeEventListener('click', this.prevChapter);
elem.querySelector('#btnBookplayerNext').removeEventListener('click', this.nextChapter);
}
}
unbindEvents() {
@ -180,11 +168,9 @@ export class BookPlayer {
}
document.removeEventListener('keyup', this.onWindowKeyUp);
document.removeEventListener('touchstart', this.onTouchStart);
if (this._rendition) {
this._rendition.off('keyup', this.onWindowKeyUp);
this._rendition.off('touchstart', this.onTouchStart);
}
}
@ -194,6 +180,16 @@ export class BookPlayer {
}
}
prevChapter(e) {
this._rendition.prev();
e.preventDefault();
}
nextChapter(e) {
this._rendition.next();
e.preventDefault();
}
createMediaElement() {
let elem = this._mediaElement;
if (elem) {
@ -214,13 +210,22 @@ export class BookPlayer {
elem.id = 'bookPlayer';
let html = '';
html += '<div class="topRightActionButtons">';
html += '<button is="paper-icon-button-light" class="autoSize bookplayerButton btnBookplayerExit hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon close"></i></button>';
if (browser.mobile) {
html += '<div class="button-wrapper top-button"><button id="btnBookplayerPrev" is="paper-icon-button-light" class="autoSize bookplayerButton hide-mouse-idle-tv"><i class="material-icons bookplayerButtonIcon navigate_before"></i> Prev</button></div>';
}
html += '<div id="viewer">';
html += '<div class="topButtons">';
html += '<button is="paper-icon-button-light" id="btnBookplayerToc" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon toc"></i></button>';
html += '<button is="paper-icon-button-light" id="btnBookplayerExit" class="autoSize bookplayerButton hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon close"></i></button>';
html += '</div>';
html += '<div class="topLeftActionButtons">';
html += '<button is="paper-icon-button-light" class="autoSize bookplayerButton btnBookplayerToc hide-mouse-idle-tv" tabindex="-1"><i class="material-icons bookplayerButtonIcon toc"></i></button>';
html += '</div>';
if (browser.mobile) {
html += '<div class="button-wrapper bottom-button"><button id="btnBookplayerNext" is="paper-icon-button-light" class="autoSize bookplayerButton hide-mouse-idle-tv">Next <i class="material-icons bookplayerButtonIcon navigate_next"></i></button></div>';
}
elem.innerHTML = html;
dialogHelper.open(elem);
@ -231,6 +236,21 @@ export class BookPlayer {
return elem;
}
render(elem, book) {
if (browser.mobile) {
return book.renderTo(elem, {
width: '100%',
height: '100%',
flow: 'scrolled-doc'
});
} else {
return book.renderTo(elem, {
width: '100%',
height: '100%'
});
}
}
setCurrentSrc(elem, options) {
const item = options.items[0];
this._currentItem = item;
@ -243,13 +263,13 @@ export class BookPlayer {
};
const serverId = item.ServerId;
const apiClient = connectionManager.getApiClient(serverId);
const apiClient = window.connectionManager.getApiClient(serverId);
return new Promise((resolve, reject) => {
import('epubjs').then(({default: epubjs}) => {
const downloadHref = apiClient.getItemDownloadUrl(item.Id);
const book = epubjs(downloadHref, {openAs: 'epub'});
const rendition = book.renderTo(elem, {width: '100%', height: '97%'});
const rendition = this.render('viewer', book);
this._currentSrc = downloadHref;
this._rendition = rendition;

View file

@ -7,18 +7,20 @@
background: #fff;
}
.topRightActionButtons {
right: 0.5vh;
.topButtons {
top: 0.5vh;
z-index: 1002;
position: absolute;
position: sticky;
}
.topLeftActionButtons {
left: 0.5vh;
top: 0.5vh;
z-index: 1002;
position: absolute;
#btnBookplayerToc {
float: left;
margin-left: 2vw;
}
#btnBookplayerExit {
float: right;
margin-right: 2vw;
}
.bookplayerButtonIcon {
@ -37,3 +39,31 @@
.bookplayerErrorMsg {
text-align: center;
}
#viewer {
align-items: flex-start;
}
#btnBookplayerPrev {
margin: 0.5vh 0.5vh;
color: black;
}
#btnBookplayerNext {
margin: 0.5vh 0.5vh;
color: black;
}
.button-wrapper {
text-align: center;
position: relative;
height: 0;
}
.top-button {
margin: 0.5vh 2em;
}
.bottom-button {
margin: 2em 0.5vh;
}

View file

@ -1,229 +1,229 @@
define(['events'], function (events) {
'use strict';
import events from 'events';
// LinkParser
// LinkParser
//
// https://github.com/ravisorg/LinkParser
//
// Locate and extract almost any URL within a string. Handles protocol-less domains, IPv4 and
// IPv6, unrecognised TLDs, and more.
//
// This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
// http://creativecommons.org/licenses/by-sa/4.0/
(function () {
// Original URL regex from the Android android.text.util.Linkify function, found here:
// http://stackoverflow.com/a/19696443
//
// https://github.com/ravisorg/LinkParser
// However there were problems with it, most probably related to the fact it was
// written in 2007, and it's been highly modified.
//
// Locate and extract almost any URL within a string. Handles protocol-less domains, IPv4 and
// IPv6, unrecognised TLDs, and more.
// 1) I didn't like the fact that it was tied to specific TLDs, since new ones
// are being added all the time it wouldn't be reasonable to expect developer to
// be continually updating their regular expressions.
//
// This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
// http://creativecommons.org/licenses/by-sa/4.0/
(function () {
// Original URL regex from the Android android.text.util.Linkify function, found here:
// http://stackoverflow.com/a/19696443
//
// However there were problems with it, most probably related to the fact it was
// written in 2007, and it's been highly modified.
//
// 1) I didn't like the fact that it was tied to specific TLDs, since new ones
// are being added all the time it wouldn't be reasonable to expect developer to
// be continually updating their regular expressions.
//
// 2) It didn't allow unicode characters in the domains which are now allowed in
// many languages, (including some IDN TLDs). Again these are constantly being
// added to and it doesn't seem reasonable to hard-code them. Note this ended up
// not being possible in standard JS due to the way it handles multibyte strings.
// It is possible using XRegExp, however a big performance hit results. Disabled
// for now.
//
// 3) It didn't allow for IPv6 hostnames
// IPv6 regex from http://stackoverflow.com/a/17871737
//
// 4) It was very poorly commented
//
// 5) It wasn't as smart as it could have been about what should be part of a
// URL and what should be part of human language.
// 2) It didn't allow unicode characters in the domains which are now allowed in
// many languages, (including some IDN TLDs). Again these are constantly being
// added to and it doesn't seem reasonable to hard-code them. Note this ended up
// not being possible in standard JS due to the way it handles multibyte strings.
// It is possible using XRegExp, however a big performance hit results. Disabled
// for now.
//
// 3) It didn't allow for IPv6 hostnames
// IPv6 regex from http://stackoverflow.com/a/17871737
//
// 4) It was very poorly commented
//
// 5) It wasn't as smart as it could have been about what should be part of a
// URL and what should be part of human language.
var protocols = '(?:(?:http|https|rtsp|ftp):\\/\\/)';
var credentials = "(?:(?:[a-z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-f0-9]{2})){1,64}" // username (1-64 normal or url escaped characters)
+ "(?:\\:(?:[a-z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-f0-9]{2})){1,25})?" // followed by optional password (: + 1-25 normal or url escaped characters)
+ '\\@)';
const protocols = '(?:(?:http|https|rtsp|ftp):\\/\\/)';
const credentials = "(?:(?:[a-z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-f0-9]{2})){1,64}" // username (1-64 normal or url escaped characters)
+ "(?:\\:(?:[a-z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-f0-9]{2})){1,25})?" // followed by optional password (: + 1-25 normal or url escaped characters)
+ '\\@)';
// IPv6 Regex http://forums.intermapper.com/viewtopic.php?t=452
// by Dartware, LLC is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License
// http://intermapper.com/
var ipv6 = '('
+ '(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))'
+ '|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))'
+ '|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))'
+ '|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ ')(%.+)?';
// IPv6 Regex http://forums.intermapper.com/viewtopic.php?t=452
// by Dartware, LLC is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License
// http://intermapper.com/
const ipv6 = '('
+ '(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))'
+ '|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))'
+ '|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))'
+ '|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ '|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))'
+ ')(%.+)?';
var ipv4 = '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[0-9])';
const ipv4 = '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.'
+ '(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[0-9])';
// This would have been a lot cleaner if JS RegExp supported conditionals...
var linkRegExpString =
// This would have been a lot cleaner if JS RegExp supported conditionals...
const linkRegExpString =
// begin match for protocol / username / password / host
'(?:'
// begin match for protocol / username / password / host
'(?:'
// ============================
// If we have a recognized protocol at the beginning of the URL, we're
// more relaxed about what we accept, because we assume the user wants
// this to be a URL, and we're not accidentally matching human language
+ protocols + '?'
// ============================
// If we have a recognized protocol at the beginning of the URL, we're
// more relaxed about what we accept, because we assume the user wants
// this to be a URL, and we're not accidentally matching human language
+ protocols + '?'
// optional username:password@
+ credentials + '?'
// optional username:password@
+ credentials + '?'
// IP address (both v4 and v6)
+ '(?:'
// IP address (both v4 and v6)
+ '(?:'
// IPv6
+ ipv6
// IPv6
+ ipv6
// IPv4
+ '|' + ipv4
// IPv4
+ '|' + ipv4
+ ')'
+ ')'
// end match for protocol / username / password / host
+ ')'
// end match for protocol / username / password / host
+ ')'
// optional port number
+ '(?:\\:\\d{1,5})?'
// optional port number
+ '(?:\\:\\d{1,5})?'
// plus optional path and query params (no unicode allowed here?)
+ '(?:'
+ '\\/(?:'
// some characters we'll accept because it's unlikely human language
// would use them after a URL unless they were part of the url
+ '(?:[a-z0-9\\/\\@\\&\\#\\~\\*\\_\\-\\+])'
+ '|(?:\\%[a-f0-9]{2})'
// some characters are much more likely to be used AFTER a url and
// were not intended to be included in the url itself. Mostly end
// of sentence type things. It's also likely that the URL would
// still work if any of these characters were missing from the end
// because we parsed it incorrectly. For these characters to be accepted
// they must be followed by another character that we're reasonably
// sure is part of the url
+ "|(?:[\\;\\?\\:\\.\\!\\'\\(\\)\\,\\=]+(?=(?:[a-z0-9\\/\\@\\&\\#\\~\\*\\_\\-\\+])|(?:\\%[a-f0-9]{2})))"
+ ')*'
+ '|\\b|\$'
+ ')';
// plus optional path and query params (no unicode allowed here?)
+ '(?:'
+ '\\/(?:'
// some characters we'll accept because it's unlikely human language
// would use them after a URL unless they were part of the url
+ '(?:[a-z0-9\\/\\@\\&\\#\\~\\*\\_\\-\\+])'
+ '|(?:\\%[a-f0-9]{2})'
// some characters are much more likely to be used AFTER a url and
// were not intended to be included in the url itself. Mostly end
// of sentence type things. It's also likely that the URL would
// still work if any of these characters were missing from the end
// because we parsed it incorrectly. For these characters to be accepted
// they must be followed by another character that we're reasonably
// sure is part of the url
+ "|(?:[\\;\\?\\:\\.\\!\\'\\(\\)\\,\\=]+(?=(?:[a-z0-9\\/\\@\\&\\#\\~\\*\\_\\-\\+])|(?:\\%[a-f0-9]{2})))"
+ ')*'
+ '|\\b|\$'
+ ')';
// regex = XRegExp(regex,'gi');
var linkRegExp = RegExp(linkRegExpString, 'gi');
// regex = XRegExp(regex,'gi');
const linkRegExp = RegExp(linkRegExpString, 'gi');
var protocolRegExp = RegExp('^' + protocols, 'i');
const protocolRegExp = RegExp('^' + protocols, 'i');
// if url doesn't begin with a known protocol, add http by default
function ensureProtocol(url) {
if (!url.match(protocolRegExp)) {
url = 'http://' + url;
}
return url;
// if url doesn't begin with a known protocol, add http by default
function ensureProtocol(url) {
if (!url.match(protocolRegExp)) {
url = 'http://' + url;
}
return url;
}
// look for links in the text
var LinkParser = {
parse: function (text) {
var links = [];
var match;
// look for links in the text
const LinkParser = {
parse: function (text) {
const links = [];
let match;
// eslint-disable-next-line no-cond-assign
while (match = linkRegExp.exec(text)) {
console.debug(match);
var txt = match[0];
var pos = match.index;
var len = txt.length;
var url = ensureProtocol(text);
links.push({ 'pos': pos, 'text': txt, 'len': len, 'url': url });
}
return links;
// eslint-disable-next-line no-cond-assign
while (match = linkRegExp.exec(text)) {
console.debug(match);
const txt = match[0];
const pos = match.index;
const len = txt.length;
const url = ensureProtocol(text);
links.push({ 'pos': pos, 'text': txt, 'len': len, 'url': url });
}
};
window.LinkParser = LinkParser;
})();
var cache = {};
function isValidIpAddress(address) {
var links = LinkParser.parse(address);
return links.length == 1;
}
function isLocalIpAddress(address) {
address = address.toLowerCase();
if (address.indexOf('127.0.0.1') !== -1) {
return true;
}
if (address.indexOf('localhost') !== -1) {
return true;
return links;
}
return false;
}
function getServerAddress(apiClient) {
var serverAddress = apiClient.serverAddress();
if (isValidIpAddress(serverAddress) && !isLocalIpAddress(serverAddress)) {
return Promise.resolve(serverAddress);
}
var cachedValue = getCachedValue(serverAddress);
if (cachedValue) {
return Promise.resolve(cachedValue);
}
return apiClient.getEndpointInfo().then(function (endpoint) {
if (endpoint.IsInNetwork) {
return apiClient.getPublicSystemInfo().then(function (info) {
var localAddress = info.LocalAddress;
if (!localAddress) {
console.debug('No valid local address returned, defaulting to external one');
localAddress = serverAddress;
}
addToCache(serverAddress, localAddress);
return localAddress;
});
} else {
addToCache(serverAddress, serverAddress);
return serverAddress;
}
});
}
function clearCache() {
cache = {};
}
function addToCache(key, value) {
cache[key] = {
value: value,
time: new Date().getTime()
};
}
function getCachedValue(key) {
var obj = cache[key];
if (obj && (new Date().getTime() - obj.time) < 180000) {
return obj.value;
}
return null;
}
events.on(ConnectionManager, 'localusersignedin', clearCache);
events.on(ConnectionManager, 'localusersignedout', clearCache);
return {
getServerAddress: getServerAddress
};
});
window.LinkParser = LinkParser;
})();
let cache = {};
// TODO: Replace with isIP (https://www.npmjs.com/package/is-ip)
function isValidIpAddress(address) {
const links = LinkParser.parse(address);
return links.length == 1;
}
// TODO: Add IPv6 support. Potentially replace with isLocalhost (https://www.npmjs.com/package/is-localhost-ip)
function isLocalIpAddress(address) {
address = address.toLowerCase();
if (address.includes('127.0.0.1')) {
return true;
}
if (address.includes('localhost')) {
return true;
}
return false;
}
export function getServerAddress(apiClient) {
const serverAddress = apiClient.serverAddress();
if (isValidIpAddress(serverAddress) && !isLocalIpAddress(serverAddress)) {
return Promise.resolve(serverAddress);
}
const cachedValue = getCachedValue(serverAddress);
if (cachedValue) {
return Promise.resolve(cachedValue);
}
return apiClient.getEndpointInfo().then(function (endpoint) {
if (endpoint.IsInNetwork) {
return apiClient.getPublicSystemInfo().then(function (info) {
let localAddress = info.LocalAddress;
if (!localAddress) {
console.debug('No valid local address returned, defaulting to external one');
localAddress = serverAddress;
}
addToCache(serverAddress, localAddress);
return localAddress;
});
} else {
addToCache(serverAddress, serverAddress);
return serverAddress;
}
});
}
function clearCache() {
cache = {};
}
function addToCache(key, value) {
cache[key] = {
value: value,
time: new Date().getTime()
};
}
function getCachedValue(key) {
const obj = cache[key];
if (obj && (new Date().getTime() - obj.time) < 180000) {
return obj.value;
}
return null;
}
events.on(window.connectionManager, 'localusersignedin', clearCache);
events.on(window.connectionManager, 'localusersignedout', clearCache);
export default {
getServerAddress: getServerAddress
};

View file

@ -1,7 +1,6 @@
import appSettings from 'appSettings';
import * as userSettings from 'userSettings';
import playbackManager from 'playbackManager';
import connectionManager from 'connectionManager';
import globalize from 'globalize';
import events from 'events';
import castSenderApiLoader from 'castSenderApiLoader';
@ -325,11 +324,11 @@ class CastPlayer {
let apiClient;
if (message.options && message.options.ServerId) {
apiClient = connectionManager.getApiClient(message.options.ServerId);
apiClient = window.connectionManager.getApiClient(message.options.ServerId);
} else if (message.options && message.options.items && message.options.items.length) {
apiClient = connectionManager.getApiClient(message.options.items[0].ServerId);
apiClient = window.connectionManager.getApiClient(message.options.items[0].ServerId);
} else {
apiClient = connectionManager.currentApiClient();
apiClient = window.connectionManager.currentApiClient();
}
message = Object.assign(message, {
@ -673,7 +672,7 @@ class ChromecastPlayer {
playWithCommand(options, command) {
if (!options.items) {
const apiClient = connectionManager.getApiClient(options.serverId);
const apiClient = window.connectionManager.getApiClient(options.serverId);
const instance = this;
return apiClient.getItem(apiClient.getCurrentUserId(), options.ids[0]).then(function (item) {
@ -931,9 +930,9 @@ class ChromecastPlayer {
return state.VolumeLevel == null ? 100 : state.VolumeLevel;
}
isPlaying() {
isPlaying(mediaType) {
const state = this.lastPlayerData || {};
return state.NowPlayingItem != null;
return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType);
}
isPlayingVideo() {
@ -950,12 +949,12 @@ class ChromecastPlayer {
currentTime(val) {
if (val != null) {
return this.seek(val);
return this.seek(val * 10000);
}
let state = this.lastPlayerData || {};
state = state.PlayState || {};
return state.PositionTicks;
return state.PositionTicks / 10000;
}
duration() {
@ -985,7 +984,7 @@ class ChromecastPlayer {
}
shuffle(item) {
const apiClient = connectionManager.getApiClient(item.ServerId);
const apiClient = window.connectionManager.getApiClient(item.ServerId);
const userId = apiClient.getCurrentUserId();
const instance = this;
@ -998,7 +997,7 @@ class ChromecastPlayer {
}
instantMix(item) {
const apiClient = connectionManager.getApiClient(item.ServerId);
const apiClient = window.connectionManager.getApiClient(item.ServerId);
const userId = apiClient.getCurrentUserId();
const instance = this;
@ -1036,7 +1035,7 @@ class ChromecastPlayer {
}
const instance = this;
const apiClient = connectionManager.getApiClient(options.serverId);
const apiClient = window.connectionManager.getApiClient(options.serverId);
return getItemsForPlayback(apiClient, {
Ids: options.ids.join(',')

View file

@ -0,0 +1,213 @@
import loading from 'loading';
import dialogHelper from 'dialogHelper';
import keyboardnavigation from 'keyboardnavigation';
import appRouter from 'appRouter';
import * as libarchive from 'libarchive';
export class ComicsPlayer {
constructor() {
this.name = 'Comics Player';
this.type = 'mediaplayer';
this.id = 'comicsplayer';
this.priority = 1;
this.imageMap = new Map();
this.onDialogClosed = this.onDialogClosed.bind(this);
this.onWindowKeyUp = this.onWindowKeyUp.bind(this);
}
play(options) {
this.progress = 0;
const elem = this.createMediaElement();
return this.setCurrentSrc(elem, options);
}
stop() {
this.unbindEvents();
const elem = this.mediaElement;
if (elem) {
dialogHelper.close(elem);
this.mediaElement = null;
}
loading.hide();
}
onDialogClosed() {
this.stop();
}
onWindowKeyUp(e) {
const key = keyboardnavigation.getKeyName(e);
switch (key) {
case 'Escape':
this.stop();
break;
}
}
bindEvents() {
document.addEventListener('keyup', this.onWindowKeyUp);
}
unbindEvents() {
document.removeEventListener('keyup', this.onWindowKeyUp);
}
createMediaElement() {
let elem = this.mediaElement;
if (elem) {
return elem;
}
elem = document.getElementById('comicsPlayer');
if (!elem) {
elem = dialogHelper.createDialog({
exitAnimationDuration: 400,
size: 'fullscreen',
autoFocus: false,
scrollY: false,
exitAnimation: 'fadeout',
removeOnClose: true
});
elem.id = 'comicsPlayer';
elem.classList.add('slideshowDialog');
elem.innerHTML = '<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>';
this.bindEvents();
dialogHelper.open(elem);
}
this.mediaElement = elem;
return elem;
}
setCurrentSrc(elem, options) {
const item = options.items[0];
this.currentItem = item;
loading.show();
const serverId = item.ServerId;
const apiClient = window.connectionManager.getApiClient(serverId);
libarchive.Archive.init({
workerUrl: appRouter.baseUrl() + '/libraries/worker-bundle.js'
});
return new Promise((resolve, reject) => {
const downloadUrl = apiClient.getItemDownloadUrl(item.Id);
const archiveSource = new ArchiveSource(downloadUrl);
const instance = this;
import('swiper').then(({default: Swiper}) => {
archiveSource.load().then(() => {
loading.hide();
this.swiperInstance = new Swiper(elem.querySelector('.slideshowSwiperContainer'), {
direction: 'horizontal',
// loop is disabled due to the lack of support in virtual slides
loop: false,
zoom: {
minRatio: 1,
toggle: true,
containerClass: 'slider-zoom-container'
},
autoplay: false,
keyboard: {
enabled: true
},
preloadImages: true,
slidesPerView: 1,
slidesPerColumn: 1,
initialSlide: 0,
// reduces memory consumption for large libraries while allowing preloading of images
virtual: {
slides: archiveSource.urls,
cache: true,
renderSlide: instance.getImgFromUrl,
addSlidesBefore: 1,
addSlidesAfter: 1
}
});
});
});
});
}
getImgFromUrl(url) {
return `<div class="swiper-slide">
<div class="slider-zoom-container">
<img src="${url}" class="swiper-slide-img">
</div>
</div>`;
}
canPlayMediaType(mediaType) {
return (mediaType || '').toLowerCase() === 'book';
}
canPlayItem(item) {
if (item.Path && (item.Path.endsWith('cbz') || item.Path.endsWith('cbr'))) {
return true;
}
return false;
}
}
class ArchiveSource {
constructor(url) {
this.url = url;
this.files = [];
this.urls = [];
this.loadPromise = this.load();
this.itemsLoaded = 0;
}
async load() {
const res = await fetch(this.url);
if (!res.ok) {
return;
}
const blob = await res.blob();
this.archive = await libarchive.Archive.open(blob);
this.raw = await this.archive.getFilesArray();
this.numberOfFiles = this.raw.length;
await this.archive.extractFiles();
const files = await this.archive.getFilesArray();
files.sort((a, b) => {
if (a.file.name < b.file.name) {
return -1;
} else {
return 1;
}
});
for (const file of files) {
/* eslint-disable-next-line compat/compat */
const url = URL.createObjectURL(file.file);
this.urls.push(url);
}
}
getLength() {
return this.raw.length;
}
async item(index) {
if (this.urls[index]) {
return this.urls[index];
}
await this.loadPromise;
return this.urls[index];
}
}
export default ComicsPlayer;

View file

@ -132,10 +132,7 @@ class HtmlAudioPlayer {
return new Promise(function (resolve, reject) {
requireHlsPlayer(function () {
const hls = new Hls({
manifestLoadingTimeOut: 20000,
xhrSetup: function (xhr, url) {
xhr.withCredentials = true;
}
manifestLoadingTimeOut: 20000
});
hls.loadSource(val);
hls.attachMedia(elem);

View file

@ -5,7 +5,6 @@ import loading from 'loading';
import dom from 'dom';
import playbackManager from 'playbackManager';
import appRouter from 'appRouter';
import connectionManager from 'connectionManager';
import {
bindEventsToHlsPlayer,
destroyHlsPlayer,
@ -115,7 +114,6 @@ function tryRemoveElement(elem) {
return new Promise(resolve => {
const duration = 240;
elem.style.animation = `htmlvideoplayer-zoomin ${duration}ms ease-in normal`;
hidePrePlaybackPage();
dom.addEventListener(elem, dom.whichAnimationEvent(), resolve, {
once: true
});
@ -325,7 +323,7 @@ function tryRemoveElement(elem) {
console.debug(`prefetching hls playlist: ${hlsPlaylistUrl}`);
return connectionManager.getApiClient(item.ServerId).ajax({
return window.connectionManager.getApiClient(item.ServerId).ajax({
type: 'GET',
url: hlsPlaylistUrl
@ -393,10 +391,7 @@ function tryRemoveElement(elem) {
return new Promise((resolve, reject) => {
requireHlsPlayer(() => {
const hls = new Hls({
manifestLoadingTimeOut: 20000,
xhrSetup(xhr) {
xhr.withCredentials = true;
}
manifestLoadingTimeOut: 20000
});
hls.loadSource(url);
hls.attachMedia(elem);
@ -1036,7 +1031,7 @@ function tryRemoveElement(elem) {
*/
renderSsaAss(videoElement, track, item) {
const attachments = this._currentPlayOptions.mediaSource.MediaAttachments || [];
const apiClient = connectionManager.getApiClient(item);
const apiClient = window.connectionManager.getApiClient(item);
const htmlVideoPlayer = this;
const options = {
video: videoElement,
@ -1332,17 +1327,24 @@ function tryRemoveElement(elem) {
this.#videoDialog = dlg;
this.#mediaElement = videoElement;
if (options.fullscreen) {
hidePrePlaybackPage();
}
// don't animate on smart tv's, too slow
if (options.fullscreen && browser.supportsCssAnimation() && !browser.slow) {
return zoomIn(dlg).then(function () {
return videoElement;
});
} else {
hidePrePlaybackPage();
return videoElement;
}
});
} else {
if (options.fullscreen) {
hidePrePlaybackPage();
}
return Promise.resolve(dlg.querySelector('video'));
}
}
@ -1393,7 +1395,12 @@ function tryRemoveElement(elem) {
const list = [];
const video = document.createElement('video');
if (video.webkitSupportsPresentationMode && typeof video.webkitSetPresentationMode === 'function' || document.pictureInPictureEnabled) {
if (
// Check non-standard Safari PiP support
typeof video.webkitSupportsPresentationMode === 'function' && video.webkitSupportsPresentationMode('picture-in-picture') && typeof video.webkitSetPresentationMode === 'function'
// Check standard PiP support
|| document.pictureInPictureEnabled
) {
list.push('PictureInPicture');
} else if (window.Windows) {
if (Windows.UI.ViewManagement.ApplicationView.getForCurrentView().isViewModeSupported(Windows.UI.ViewManagement.ApplicationViewMode.compactOverlay)) {

View file

@ -17,7 +17,11 @@
order: -1;
}
video::-webkit-media-controls {
/* Controls are enabled for devices that don't support autoplay. They will be hidden when playback starts.
In Tizen 2.3 (and probably other old web engines), subtitles are located under '-webkit-media-controls' tree.
Therefore, we hide controls only if they are enabled.
*/
video[controls]::-webkit-media-controls {
display: none !important;
}

View file

@ -1,4 +1,3 @@
import connectionManager from 'connectionManager';
export default class PhotoPlayer {
constructor() {
@ -11,11 +10,11 @@ export default class PhotoPlayer {
play(options) {
return new Promise(function (resolve, reject) {
import('slideshow').then(({default: slideshow}) => {
var index = options.startIndex || 0;
const index = options.startIndex || 0;
var apiClient = connectionManager.currentApiClient();
const apiClient = window.connectionManager.currentApiClient();
apiClient.getCurrentUser().then(function(result) {
var newSlideShow = new slideshow({
const newSlideShow = new slideshow({
showTitle: false,
cover: false,
items: options.items,

View file

@ -1,4 +1,3 @@
import connectionManager from 'connectionManager';
import globalize from 'globalize';
function showErrorMessage() {
@ -25,7 +24,7 @@ class PlayAccessValidation {
return Promise.resolve();
}
return connectionManager.getApiClient(serverId).getCurrentUser().then(function (user) {
return window.connectionManager.getApiClient(serverId).getCurrentUser().then(function (user) {
if (user.Policy.EnableMediaPlayback) {
return Promise.resolve();
}

View file

@ -1,7 +1,6 @@
import playbackManager from 'playbackManager';
import events from 'events';
import serverNotifications from 'serverNotifications';
import connectionManager from 'connectionManager';
function getActivePlayerId() {
const info = playbackManager.getPlayerInfo();
@ -54,10 +53,10 @@ function getCurrentApiClient(instance) {
const currentServerId = instance.currentServerId;
if (currentServerId) {
return connectionManager.getApiClient(currentServerId);
return window.connectionManager.getApiClient(currentServerId);
}
return connectionManager.currentApiClient();
return window.connectionManager.currentApiClient();
}
function sendCommandByName(instance, name, options) {
@ -322,12 +321,12 @@ class SessionPlayer {
currentTime(val) {
if (val != null) {
return this.seek(val);
return this.seek(val * 10000);
}
let state = this.lastPlayerData || {};
state = state.PlayState || {};
return state.PositionTicks;
return state.PositionTicks / 10000;
}
duration() {
@ -466,9 +465,9 @@ class SessionPlayer {
sendCommandByName(this, 'DisplayContent', options);
}
isPlaying() {
isPlaying(mediaType) {
const state = this.lastPlayerData || {};
return state.NowPlayingItem != null;
return state.NowPlayingItem != null && (state.NowPlayingItem.MediaType === mediaType || !mediaType);
}
isPlayingVideo() {