update components

This commit is contained in:
Luke Pulverenti 2017-01-30 13:20:43 -05:00
parent 21fe336eda
commit 688ecab75e
35 changed files with 10679 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
define(['connectionManager', 'confirm', 'embyRouter', 'globalize'], function (connectionManager, confirm, embyRouter, globalize) {
'use strict';
function deleteItem(options) {
var item = options.item;
var itemId = item.Id;
var parentId = item.SeasonId || item.SeriesId || item.ParentId;
var serverId = item.ServerId;
var msg = globalize.translate('sharedcomponents#ConfirmDeleteItem');
var title = globalize.translate('sharedcomponents#HeaderDeleteItem');
var apiClient = connectionManager.getApiClient(item.ServerId);
return confirm({
title: title,
text: msg,
confirmText: globalize.translate('sharedcomponents#Delete'),
primary: 'cancel'
}).then(function () {
return apiClient.deleteItem(itemId).then(function () {
if (options.navigate) {
if (parentId) {
embyRouter.showItem(parentId, serverId);
} else {
embyRouter.goHome();
}
}
});
});
}
return {
deleteItem: deleteItem
};
});

View File

@ -0,0 +1,180 @@
define(['globalize', 'loading', 'alert'], function (globalize, loading, alert) {
'use strict';
function showNewUserInviteMessage(result) {
if (!result.IsNewUserInvitation && !result.IsPending) {
// It was immediately approved
return Promise.resolve();
}
var message = result.IsNewUserInvitation ?
globalize.translate('MessageInvitationSentToNewUser', result.GuestDisplayName) :
globalize.translate('MessageInvitationSentToUser', result.GuestDisplayName);
alert({
text: message,
title: globalize.translate('HeaderInvitationSent')
});
}
function inviteGuest(options) {
var apiClient = options.apiClient;
loading.show();
// Add/Update connect info
return apiClient.ajax({
type: "POST",
url: apiClient.getUrl('Connect/Invite'),
dataType: 'json',
data: options.guestOptions || {}
}).then(function (result) {
loading.hide();
return showNewUserInviteMessage(result);
}, function (response) {
loading.hide();
if (response.status === 404) {
// User doesn't exist
alert({
text: globalize.translate('GuestUserNotFound')
});
} else if ((response.status || 0) >= 500) {
// Unable to reach connect server ?
alert({
text: globalize.translate('ErrorReachingEmbyConnect')
});
} else {
// status 400 = account not activated
// General error
showGuestGeneralErrorMessage();
}
});
}
function showGuestGeneralErrorMessage() {
var html = globalize.translate('ErrorAddingGuestAccount1', '<a href="https://emby.media/connect" target="_blank">https://emby.media/connect</a>');
html += '<br/><br/>' + globalize.translate('ErrorAddingGuestAccount2', 'apps@emby.media');
var text = globalize.translate('ErrorAddingGuestAccount1', 'https://emby.media/connect');
text += '\n\n' + globalize.translate('ErrorAddingGuestAccount2', 'apps@emby.media');
alert({
text: text,
html: html
});
}
function showLinkUserMessage(username) {
var html;
var text;
if (username) {
html = globalize.translate('ErrorAddingEmbyConnectAccount1', '<a href="https://emby.media/connect" target="_blank">https://emby.media/connect</a>');
html += '<br/><br/>' + globalize.translate('ErrorAddingEmbyConnectAccount2', 'apps@emby.media');
text = globalize.translate('ErrorAddingEmbyConnectAccount1', 'https://emby.media/connect');
text += '\n\n' + globalize.translate('ErrorAddingEmbyConnectAccount2', 'apps@emby.media');
} else {
html = text = globalize.translate('DefaultErrorMessage');
}
return alert({
text: text,
html: html
});
}
function updateUserLink(apiClient, user, newConnectUsername) {
var currentConnectUsername = user.ConnectUserName || '';
var enteredConnectUsername = newConnectUsername;
var linkUrl = apiClient.getUrl('Users/' + user.Id + '/Connect/Link');
if (currentConnectUsername && !enteredConnectUsername) {
// Remove connect info
// Add/Update connect info
return apiClient.ajax({
type: "DELETE",
url: linkUrl
}).then(function () {
return alert({
text: globalize.translate('MessageEmbyAccontRemoved'),
title: globalize.translate('HeaderEmbyAccountRemoved'),
}).catch(function () {
return Promise.resolve();
});
}, function () {
return alert({
text: globalize.translate('ErrorRemovingEmbyConnectAccount')
}).then(function () {
return Promise.reject();
});
});
}
else if (currentConnectUsername !== enteredConnectUsername) {
// Add/Update connect info
return apiClient.ajax({
type: "POST",
url: linkUrl,
data: {
ConnectUsername: enteredConnectUsername
},
dataType: 'json'
}).then(function (result) {
var msgKey = result.IsPending ? 'MessagePendingEmbyAccountAdded' : 'MessageEmbyAccountAdded';
return alert({
text: globalize.translate(msgKey),
title: globalize.translate('HeaderEmbyAccountAdded'),
}).catch(function () {
return Promise.resolve();
});
}, function () {
return showLinkUserMessage('.').then(function () {
return Promise.reject();
});
});
} else {
return Promise.reject();
}
}
return {
inviteGuest: inviteGuest,
updateUserLink: updateUserLink
};
});

View File

@ -0,0 +1,12 @@
define([], function () {
'use strict';
return {
fileExists: function (path) {
return Promise.reject();
},
directoryExists: function (path) {
return Promise.reject();
}
};
});

Binary file not shown.

View File

@ -0,0 +1,444 @@
define(['events', 'browser', 'pluginManager', 'apphost', 'appSettings'], function (events, browser, pluginManager, appHost, appSettings) {
"use strict";
return function () {
var self = this;
self.name = 'Html Audio Player';
self.type = 'mediaplayer';
self.id = 'htmlaudioplayer';
// Let any players created by plugins take priority
self.priority = 1;
var mediaElement;
var currentSrc;
var currentPlayOptions;
var started;
var _currentTime;
function getSavedVolume() {
return appSettings.get("volume") || 1;
}
function saveVolume(value) {
if (value) {
appSettings.set("volume", value);
}
}
self.canPlayMediaType = function (mediaType) {
return (mediaType || '').toLowerCase() === 'audio';
};
self.getDeviceProfile = function () {
return new Promise(function (resolve, reject) {
require(['browserdeviceprofile'], function (profileBuilder) {
var profile = profileBuilder({
});
resolve(profile);
});
});
};
self.currentSrc = function () {
return currentSrc;
};
self.play = function (options) {
_currentTime = null;
started = false;
var elem = createMediaElement();
var val = options.url;
var seconds = (options.playerStartPositionTicks || 0) / 10000000;
if (seconds) {
val += '#t=' + seconds;
}
elem.crossOrigin = getCrossOriginValue(options.mediaSource);
elem.title = options.title;
// Opera TV guidelines suggest using source elements, so let's do that if we have a valid mimeType
if (options.mimeType && browser.operaTv) {
// Need to do this or we won't be able to restart a new stream
if (elem.currentSrc) {
elem.src = '';
elem.removeAttribute('src');
}
elem.innerHTML = '<source src="' + val + '" type="' + options.mimeType + '">';
} else {
elem.src = val;
}
currentSrc = val;
currentPlayOptions = options;
return playWithPromise(elem);
};
function playWithPromise(elem) {
try {
var promise = elem.play();
if (promise && promise.then) {
// Chrome now returns a promise
return promise.catch(function (e) {
var 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();
});
} else {
return Promise.resolve();
}
} catch (err) {
console.log('error calling video.play: ' + err);
return Promise.reject();
}
}
function getCrossOriginValue(mediaSource) {
return 'anonymous';
}
// Save this for when playback stops, because querying the time at that point might return 0
self.currentTime = function (val) {
if (mediaElement) {
if (val != null) {
mediaElement.currentTime = val / 1000;
return;
}
if (_currentTime) {
return _currentTime * 1000;
}
return (mediaElement.currentTime || 0) * 1000;
}
};
self.duration = function (val) {
if (mediaElement) {
var duration = mediaElement.duration;
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {
return duration * 1000;
}
}
return null;
};
function supportsFade() {
if (browser.tv) {
// Not working on tizen.
// We could possibly enable on other tv's, but all smart tv browsers tend to be pretty primitive
return false;
}
return true;
}
self.stop = function (destroyPlayer) {
cancelFadeTimeout();
var elem = mediaElement;
var src = currentSrc;
if (elem && src) {
if (!destroyPlayer || !supportsFade()) {
if (!elem.paused) {
elem.pause();
}
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute("src");
onEnded();
return Promise.resolve();
}
var originalVolume = elem.volume;
return fade(elem, elem.volume).then(function () {
if (!elem.paused) {
elem.pause();
}
elem.src = '';
elem.innerHTML = '';
elem.removeAttribute("src");
elem.volume = originalVolume;
onEnded();
});
}
return Promise.resolve();
};
self.destroy = function () {
};
var fadeTimeout;
function fade(elem, startingVolume) {
// Need to record the starting volume on each pass rather than querying elem.volume
// This is due to iOS safari not allowing volume changes and always returning the system volume value
var newVolume = Math.max(0, startingVolume - 0.15);
console.log('fading volume to ' + newVolume);
elem.volume = newVolume;
if (newVolume <= 0) {
return Promise.resolve();
}
return new Promise(function (resolve, reject) {
cancelFadeTimeout();
fadeTimeout = setTimeout(function () {
fade(elem, newVolume).then(resolve, reject);
}, 100);
});
}
function cancelFadeTimeout() {
var timeout = fadeTimeout;
if (timeout) {
clearTimeout(timeout);
fadeTimeout = null;
}
}
self.pause = function () {
if (mediaElement) {
mediaElement.pause();
}
};
// This is a retry after error
self.resume = function () {
if (mediaElement) {
mediaElement.play();
}
};
self.unpause = function () {
if (mediaElement) {
mediaElement.play();
}
};
self.paused = function () {
if (mediaElement) {
return mediaElement.paused;
}
return false;
};
self.setVolume = function (val) {
if (mediaElement) {
mediaElement.volume = val / 100;
}
};
self.getVolume = function () {
if (mediaElement) {
return mediaElement.volume * 100;
}
};
self.volumeUp = function () {
self.setVolume(Math.min(self.getVolume() + 2, 100));
};
self.volumeDown = function () {
self.setVolume(Math.max(self.getVolume() - 2, 0));
};
self.setMute = function (mute) {
if (mediaElement) {
mediaElement.muted = mute;
}
};
self.isMuted = function () {
if (mediaElement) {
return mediaElement.muted;
}
return false;
};
function onEnded() {
var stopInfo = {
src: currentSrc
};
events.trigger(self, 'stopped', [stopInfo]);
_currentTime = null;
currentSrc = null;
currentPlayOptions = null;
}
function onTimeUpdate() {
// Get the player position + the transcoding offset
var time = this.currentTime;
_currentTime = time;
events.trigger(self, 'timeupdate');
}
function onVolumeChange() {
if (!fadeTimeout) {
saveVolume(this.volume);
events.trigger(self, 'volumechange');
}
}
function onPlaying(e) {
if (!started) {
started = true;
this.removeAttribute('controls');
seekOnPlaybackStart(e.target);
}
events.trigger(self, 'playing');
}
function seekOnPlaybackStart(element) {
var seconds = (currentPlayOptions.playerStartPositionTicks || 0) / 10000000;
if (seconds) {
var src = (self.currentSrc() || '').toLowerCase();
// 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 (!browser.chrome || src.indexOf('.m3u8') !== -1) {
var delay = browser.safari ? 2500 : 0;
if (delay) {
setTimeout(function () {
element.currentTime = seconds;
}, delay);
} else {
element.currentTime = seconds;
}
}
}
}
function onPause() {
events.trigger(self, 'pause');
}
function onError() {
var errorCode = this.error ? this.error.code : '';
errorCode = (errorCode || '').toString();
console.log('Media element error code: ' + errorCode);
var type;
switch (errorCode) {
case 1:
// MEDIA_ERR_ABORTED
// This will trigger when changing media while something is playing
return;
case 2:
// MEDIA_ERR_NETWORK
type = 'network';
break;
case 3:
// MEDIA_ERR_DECODE
break;
case 4:
// MEDIA_ERR_SRC_NOT_SUPPORTED
break;
}
//events.trigger(self, 'error', [
//{
// type: type
//}]);
}
function createMediaElement() {
var elem = document.querySelector('.mediaPlayerAudio');
if (!elem) {
elem = document.createElement('audio');
elem.classList.add('mediaPlayerAudio');
elem.classList.add('hide');
document.body.appendChild(elem);
elem.volume = getSavedVolume();
elem.addEventListener('timeupdate', onTimeUpdate);
elem.addEventListener('ended', onEnded);
elem.addEventListener('volumechange', onVolumeChange);
elem.addEventListener('pause', onPause);
elem.addEventListener('playing', onPlaying);
elem.addEventListener('error', onError);
}
mediaElement = elem;
return elem;
}
function onDocumentClick() {
document.removeEventListener('click', onDocumentClick);
var elem = document.createElement('audio');
elem.classList.add('mediaPlayerAudio');
elem.classList.add('hide');
document.body.appendChild(elem);
elem.src = pluginManager.mapPath(self, 'blank.mp3');
elem.play();
setTimeout(function () {
elem.src = '';
elem.removeAttribute("src");
}, 1000);
}
// Mobile browsers don't allow autoplay, so this is a nice workaround
if (!appHost.supports('htmlaudioautoplay')) {
document.addEventListener('click', onDocumentClick);
}
};
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,44 @@
.videoPlayerContainer {
position: fixed !important;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
}
.videoPlayerContainer:not(.videoPlayerContainer-withBackdrop) {
background: #000 !important;
}
.videoPlayerContainer-withBackdrop {
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
background-attachment: fixed;
background-color: #000;
}
.videoPlayerContainer-onTop {
z-index: 1000;
}
.htmlvideoplayer {
margin: 0 !important;
padding: 0 !important;
width: 100%;
height: 100%;
}
.htmlvideoplayer::-webkit-media-text-track-display {
/*Style the text itself*/
margin-top: -2.5em;
}
.htmlvideoplayer::cue {
background-color: transparent;
text-shadow: 2px 2px 2px rgba(0, 0, 0, 1);
-webkit-font-smoothing: antialiased;
font-family: inherit;
}

View File

@ -0,0 +1,323 @@
// from https://github.com/jakearchibald/idb
(function () {
'use strict';
function toArray(arr) {
return Array.prototype.slice.call(arr);
}
function promisifyRequest(request) {
return new Promise(function (resolve, reject) {
request.onsuccess = function () {
resolve(request.result);
};
request.onerror = function () {
reject(request.error);
};
});
}
function promisifyRequestCall(obj, method, args) {
var request;
var p = new Promise(function (resolve, reject) {
request = obj[method].apply(obj, args);
promisifyRequest(request).then(resolve, reject);
});
p.request = request;
return p;
}
function promisifyCursorRequestCall(obj, method, args) {
var p = promisifyRequestCall(obj, method, args);
return p.then(function (value) {
if (!value) {
return;
}
return new Cursor(value, p.request);
});
}
function proxyProperties(ProxyClass, targetProp, properties) {
properties.forEach(function (prop) {
Object.defineProperty(ProxyClass.prototype, prop, {
get: function () {
return this[targetProp][prop];
}
});
});
}
function proxyRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function (prop) {
if (!(prop in Constructor.prototype)) {
return;
}
ProxyClass.prototype[prop] = function () {
return promisifyRequestCall(this[targetProp], prop, arguments);
};
});
}
function proxyMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function (prop) {
if (!(prop in Constructor.prototype)) {
return;
}
ProxyClass.prototype[prop] = function () {
return this[targetProp][prop].apply(this[targetProp], arguments);
};
});
}
function proxyCursorRequestMethods(ProxyClass, targetProp, Constructor, properties) {
properties.forEach(function (prop) {
if (!(prop in Constructor.prototype)) {
return;
}
ProxyClass.prototype[prop] = function () {
return promisifyCursorRequestCall(this[targetProp], prop, arguments);
};
});
}
function Index(index) {
this._index = index;
}
proxyProperties(Index, '_index', [
'name',
'keyPath',
'multiEntry',
'unique'
]);
proxyRequestMethods(Index, '_index', IDBIndex, [
'get',
'getKey',
'getAll',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(Index, '_index', IDBIndex, [
'openCursor',
'openKeyCursor'
]);
function Cursor(cursor, request) {
this._cursor = cursor;
this._request = request;
}
proxyProperties(Cursor, '_cursor', [
'direction',
'key',
'primaryKey',
'value'
]);
proxyRequestMethods(Cursor, '_cursor', IDBCursor, [
'update',
'delete'
]);
// proxy 'next' methods
['advance', 'continue', 'continuePrimaryKey'].forEach(function (methodName) {
if (!(methodName in IDBCursor.prototype)) {
return;
}
Cursor.prototype[methodName] = function () {
var cursor = this;
var args = arguments;
return Promise.resolve().then(function () {
cursor._cursor[methodName].apply(cursor._cursor, args);
return promisifyRequest(cursor._request).then(function (value) {
if (!value) {
return;
}
return new Cursor(value, cursor._request);
});
});
};
});
function ObjectStore(store) {
this._store = store;
}
ObjectStore.prototype.createIndex = function () {
return new Index(this._store.createIndex.apply(this._store, arguments));
};
ObjectStore.prototype.index = function () {
return new Index(this._store.index.apply(this._store, arguments));
};
proxyProperties(ObjectStore, '_store', [
'name',
'keyPath',
'indexNames',
'autoIncrement'
]);
proxyRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'put',
'add',
'delete',
'clear',
'get',
'getAll',
'getAllKeys',
'count'
]);
proxyCursorRequestMethods(ObjectStore, '_store', IDBObjectStore, [
'openCursor',
'openKeyCursor'
]);
proxyMethods(ObjectStore, '_store', IDBObjectStore, [
'deleteIndex'
]);
function Transaction(idbTransaction) {
this._tx = idbTransaction;
this.complete = new Promise(function (resolve, reject) {
idbTransaction.oncomplete = function () {
resolve();
};
idbTransaction.onerror = function () {
reject(idbTransaction.error);
};
idbTransaction.onabort = function () {
reject(idbTransaction.error);
};
});
}
Transaction.prototype.objectStore = function () {
return new ObjectStore(this._tx.objectStore.apply(this._tx, arguments));
};
proxyProperties(Transaction, '_tx', [
'objectStoreNames',
'mode'
]);
proxyMethods(Transaction, '_tx', IDBTransaction, [
'abort'
]);
function UpgradeDB(db, oldVersion, transaction) {
this._db = db;
this.oldVersion = oldVersion;
this.transaction = new Transaction(transaction);
}
UpgradeDB.prototype.createObjectStore = function () {
return new ObjectStore(this._db.createObjectStore.apply(this._db, arguments));
};
proxyProperties(UpgradeDB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(UpgradeDB, '_db', IDBDatabase, [
'deleteObjectStore',
'close'
]);
function DB(db) {
this._db = db;
}
DB.prototype.transaction = function () {
return new Transaction(this._db.transaction.apply(this._db, arguments));
};
proxyProperties(DB, '_db', [
'name',
'version',
'objectStoreNames'
]);
proxyMethods(DB, '_db', IDBDatabase, [
'close'
]);
// Add cursor iterators
// TODO: remove this once browsers do the right thing with promises
['openCursor', 'openKeyCursor'].forEach(function (funcName) {
[ObjectStore, Index].forEach(function (Constructor) {
Constructor.prototype[funcName.replace('open', 'iterate')] = function () {
var args = toArray(arguments);
var callback = args[args.length - 1];
var nativeObject = this._store || this._index;
var request = nativeObject[funcName].apply(nativeObject, args.slice(0, -1));
request.onsuccess = function () {
callback(request.result);
};
};
});
});
// polyfill getAll
[Index, ObjectStore].forEach(function (Constructor) {
if (Constructor.prototype.getAll) {
return;
}
Constructor.prototype.getAll = function (query, count) {
var instance = this;
var items = [];
return new Promise(function (resolve) {
instance.iterateCursor(query, function (cursor) {
if (!cursor) {
resolve(items);
return;
}
items.push(cursor.value);
if (count !== undefined && items.length === count) {
resolve(items);
return;
}
cursor.continue();
});
});
};
});
var exp = {
open: function (name, version, upgradeCallback) {
var p = promisifyRequestCall(indexedDB, 'open', [name, version]);
var request = p.request;
request.onupgradeneeded = function (event) {
if (upgradeCallback) {
upgradeCallback(new UpgradeDB(request.result, event.oldVersion, request.transaction));
}
};
return p.then(function (db) {
return new DB(db);
});
},
delete: function (name) {
return promisifyRequestCall(indexedDB, 'deleteDatabase', [name]);
}
};
if (typeof module !== 'undefined') {
module.exports = exp;
}
else {
self.idb = exp;
}
}());

View File

@ -0,0 +1,16 @@
.imageEditor-buttons {
display: flex;
align-items: center;
margin: 1em 0 1em;
}
.first-imageEditor-buttons {
margin-top: 2em;
}
@media all and (min-width: 1200px) {
.imageEditorCard {
width: 20%;
}
}

View File

@ -0,0 +1,341 @@
// # The MIT License (MIT)
// #
// # Copyright (c) 2016 Microsoft. All rights reserved.
// #
// # Permission is hereby granted, free of charge, to any person obtaining a copy
// # of this software and associated documentation files (the "Software"), to deal
// # in the Software without restriction, including without limitation the rights
// # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// # copies of the Software, and to permit persons to whom the Software is
// # furnished to do so, subject to the following conditions:
// #
// # The above copyright notice and this permission notice shall be included in
// # all copies or substantial portions of the Software.
// #
// # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// # THE SOFTWARE.
(function () {
"use strict";
var _GAMEPAD_A_BUTTON_INDEX = 0,
_GAMEPAD_B_BUTTON_INDEX = 1,
_GAMEPAD_DPAD_UP_BUTTON_INDEX = 12,
_GAMEPAD_DPAD_DOWN_BUTTON_INDEX = 13,
_GAMEPAD_DPAD_LEFT_BUTTON_INDEX = 14,
_GAMEPAD_DPAD_RIGHT_BUTTON_INDEX = 15,
_GAMEPAD_A_KEY = "GamepadA",
_GAMEPAD_B_KEY = "GamepadB",
_GAMEPAD_DPAD_UP_KEY = "GamepadDPadUp",
_GAMEPAD_DPAD_DOWN_KEY = "GamepadDPadDown",
_GAMEPAD_DPAD_LEFT_KEY = "GamepadDPadLeft",
_GAMEPAD_DPAD_RIGHT_KEY = "GamepadDPadRight",
_GAMEPAD_LEFT_THUMBSTICK_UP_KEY = "GamepadLeftThumbStickUp",
_GAMEPAD_LEFT_THUMBSTICK_DOWN_KEY = "GamepadLeftThumbStickDown",
_GAMEPAD_LEFT_THUMBSTICK_LEFT_KEY = "GamepadLeftThumbStickLeft",
_GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEY = "GamepadLeftThumbStickRight",
_GAMEPAD_A_KEYCODE = 0,
_GAMEPAD_B_KEYCODE = 27,
_GAMEPAD_DPAD_UP_KEYCODE = 38,
_GAMEPAD_DPAD_DOWN_KEYCODE = 40,
_GAMEPAD_DPAD_LEFT_KEYCODE = 37,
_GAMEPAD_DPAD_RIGHT_KEYCODE = 39,
_GAMEPAD_LEFT_THUMBSTICK_UP_KEYCODE = 38,
_GAMEPAD_LEFT_THUMBSTICK_DOWN_KEYCODE = 40,
_GAMEPAD_LEFT_THUMBSTICK_LEFT_KEYCODE = 37,
_GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEYCODE = 39,
_THUMB_STICK_THRESHOLD = 0.75;
var _leftThumbstickUpPressed = false,
_leftThumbstickDownPressed = false,
_leftThumbstickLeftPressed = false,
_leftThumbstickRightPressed = false,
_dPadUpPressed = false,
_dPadDownPressed = false,
_dPadLeftPressed = false,
_dPadRightPressed = false,
_gamepadAPressed = false,
_gamepadBPressed = false;
// The set of buttons on the gamepad we listen for.
var ProcessedButtons = [
_GAMEPAD_DPAD_UP_BUTTON_INDEX,
_GAMEPAD_DPAD_DOWN_BUTTON_INDEX,
_GAMEPAD_DPAD_LEFT_BUTTON_INDEX,
_GAMEPAD_DPAD_RIGHT_BUTTON_INDEX,
_GAMEPAD_A_BUTTON_INDEX,
_GAMEPAD_B_BUTTON_INDEX
];
var _ButtonPressedState = {};
_ButtonPressedState.getgamepadA = function () {
return _gamepadAPressed;
};
_ButtonPressedState.setgamepadA = function (newPressedState) {
raiseKeyEvent(_gamepadAPressed, newPressedState, _GAMEPAD_A_KEY, _GAMEPAD_A_KEYCODE, false, true);
_gamepadAPressed = newPressedState;
};
_ButtonPressedState.getgamepadB = function () {
return _gamepadBPressed;
};
_ButtonPressedState.setgamepadB = function (newPressedState) {
raiseKeyEvent(_gamepadBPressed, newPressedState, _GAMEPAD_B_KEY, _GAMEPAD_B_KEYCODE);
_gamepadBPressed = newPressedState;
};
_ButtonPressedState.getleftThumbstickUp = function () {
return _leftThumbstickUpPressed;
};
_ButtonPressedState.setleftThumbstickUp = function (newPressedState) {
raiseKeyEvent(_leftThumbstickUpPressed, newPressedState, _GAMEPAD_LEFT_THUMBSTICK_UP_KEY, _GAMEPAD_LEFT_THUMBSTICK_UP_KEYCODE, true);
_leftThumbstickUpPressed = newPressedState;
};
_ButtonPressedState.getleftThumbstickDown = function () {
return _leftThumbstickDownPressed;
};
_ButtonPressedState.setleftThumbstickDown = function (newPressedState) {
raiseKeyEvent(_leftThumbstickDownPressed, newPressedState, _GAMEPAD_LEFT_THUMBSTICK_DOWN_KEY, _GAMEPAD_LEFT_THUMBSTICK_DOWN_KEYCODE, true);
_leftThumbstickDownPressed = newPressedState;
};
_ButtonPressedState.getleftThumbstickLeft = function () {
return _leftThumbstickLeftPressed;
};
_ButtonPressedState.setleftThumbstickLeft = function (newPressedState) {
raiseKeyEvent(_leftThumbstickLeftPressed, newPressedState, _GAMEPAD_LEFT_THUMBSTICK_LEFT_KEY, _GAMEPAD_LEFT_THUMBSTICK_LEFT_KEYCODE, true);
_leftThumbstickLeftPressed = newPressedState;
};
_ButtonPressedState.getleftThumbstickRight = function () {
return _leftThumbstickRightPressed;
};
_ButtonPressedState.setleftThumbstickRight = function (newPressedState) {
raiseKeyEvent(_leftThumbstickRightPressed, newPressedState, _GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEY, _GAMEPAD_LEFT_THUMBSTICK_RIGHT_KEYCODE, true);
_leftThumbstickRightPressed = newPressedState;
};
_ButtonPressedState.getdPadUp = function () {
return _dPadUpPressed;
};
_ButtonPressedState.setdPadUp = function (newPressedState) {
raiseKeyEvent(_dPadUpPressed, newPressedState, _GAMEPAD_DPAD_UP_KEY, _GAMEPAD_DPAD_UP_KEYCODE, true);
_dPadUpPressed = newPressedState;
};
_ButtonPressedState.getdPadDown = function () {
return _dPadDownPressed;
};
_ButtonPressedState.setdPadDown = function (newPressedState) {
raiseKeyEvent(_dPadDownPressed, newPressedState, _GAMEPAD_DPAD_DOWN_KEY, _GAMEPAD_DPAD_DOWN_KEYCODE, true);
_dPadDownPressed = newPressedState;
};
_ButtonPressedState.getdPadLeft = function () {
return _dPadLeftPressed;
};
_ButtonPressedState.setdPadLeft = function (newPressedState) {
raiseKeyEvent(_dPadLeftPressed, newPressedState, _GAMEPAD_DPAD_LEFT_KEY, _GAMEPAD_DPAD_LEFT_KEYCODE, true);
_dPadLeftPressed = newPressedState;
};
_ButtonPressedState.getdPadRight = function () {
return _dPadRightPressed;
};
_ButtonPressedState.setdPadRight = function (newPressedState) {
raiseKeyEvent(_dPadRightPressed, newPressedState, _GAMEPAD_DPAD_RIGHT_KEY, _GAMEPAD_DPAD_RIGHT_KEYCODE, true);
_dPadRightPressed = newPressedState;
};
var times = {};
function throttle(key) {
var time = times[key] || 0;
var now = new Date().getTime();
if ((now - time) >= 200) {
//times[key] = now;
return true;
}
return false;
}
function resetThrottle(key) {
times[key] = new Date().getTime();
}
function raiseEvent(name, key, keyCode) {
var event = document.createEvent('Event');
event.initEvent(name, true, true);
event.key = key;
event.keyCode = keyCode;
(document.activeElement || document.body).dispatchEvent(event);
}
function raiseKeyEvent(oldPressedState, newPressedState, key, keyCode, enableRepeatKeyDown, clickonKeyUp) {
// No-op if oldPressedState === newPressedState
if (newPressedState === true) {
// button down
var fire = false;
// always fire if this is the initial down press
if (oldPressedState === false) {
fire = true;
resetThrottle(key);
} else if (enableRepeatKeyDown) {
fire = throttle(key);
}
if (fire && keyCode) {
raiseEvent("keydown", key, keyCode);
}
} else if (newPressedState === false && oldPressedState === true) {
resetThrottle(key);
// button up
if (keyCode) {
raiseEvent("keyup", key, keyCode);
}
if (clickonKeyUp) {
(document.activeElement || window).click();
}
}
}
function runInputLoop() {
// Get the latest gamepad state.
var gamepads;
if (navigator.getGamepads) {
gamepads = navigator.getGamepads();
} else if (navigator.webkitGetGamepads) {
gamepads = navigator.webkitGetGamepads();
}
gamepads = gamepads || [];
var i, j, len;
for (i = 0, len = gamepads.length; i < len; i++) {
var gamepad = gamepads[i];
if (gamepad) {
// Iterate through the axes
var axes = gamepad.axes;
var leftStickX = axes[0];
var leftStickY = axes[1];
if (leftStickX > _THUMB_STICK_THRESHOLD) { // Right
_ButtonPressedState.setleftThumbstickRight(true);
} else if (leftStickX < -_THUMB_STICK_THRESHOLD) { // Left
_ButtonPressedState.setleftThumbstickLeft(true);
} else if (leftStickY < -_THUMB_STICK_THRESHOLD) { // Up
_ButtonPressedState.setleftThumbstickUp(true);
} else if (leftStickY > _THUMB_STICK_THRESHOLD) { // Down
_ButtonPressedState.setleftThumbstickDown(true);
} else {
_ButtonPressedState.setleftThumbstickLeft(false);
_ButtonPressedState.setleftThumbstickRight(false);
_ButtonPressedState.setleftThumbstickUp(false);
_ButtonPressedState.setleftThumbstickDown(false);
}
// Iterate through the buttons to see if Left thumbstick, DPad, A and B are pressed.
var buttons = gamepad.buttons;
for (j = 0, len = buttons.length; j < len; j++) {
if (ProcessedButtons.indexOf(j) !== -1) {
if (buttons[j].pressed) {
switch (j) {
case _GAMEPAD_DPAD_UP_BUTTON_INDEX:
_ButtonPressedState.setdPadUp(true);
break;
case _GAMEPAD_DPAD_DOWN_BUTTON_INDEX:
_ButtonPressedState.setdPadDown(true);
break;
case _GAMEPAD_DPAD_LEFT_BUTTON_INDEX:
_ButtonPressedState.setdPadLeft(true);
break;
case _GAMEPAD_DPAD_RIGHT_BUTTON_INDEX:
_ButtonPressedState.setdPadRight(true);
break;
case _GAMEPAD_A_BUTTON_INDEX:
_ButtonPressedState.setgamepadA(true);
break;
case _GAMEPAD_B_BUTTON_INDEX:
_ButtonPressedState.setgamepadB(true);
break;
default:
// No-op
break;
}
} else {
switch (j) {
case _GAMEPAD_DPAD_UP_BUTTON_INDEX:
if (_ButtonPressedState.getdPadUp()) {
_ButtonPressedState.setdPadUp(false);
}
break;
case _GAMEPAD_DPAD_DOWN_BUTTON_INDEX:
if (_ButtonPressedState.getdPadDown()) {
_ButtonPressedState.setdPadDown(false);
}
break;
case _GAMEPAD_DPAD_LEFT_BUTTON_INDEX:
if (_ButtonPressedState.getdPadLeft()) {
_ButtonPressedState.setdPadLeft(false);
}
break;
case _GAMEPAD_DPAD_RIGHT_BUTTON_INDEX:
if (_ButtonPressedState.getdPadRight()) {
_ButtonPressedState.setdPadRight(false);
}
break;
case _GAMEPAD_A_BUTTON_INDEX:
if (_ButtonPressedState.getgamepadA()) {
_ButtonPressedState.setgamepadA(false);
}
break;
case _GAMEPAD_B_BUTTON_INDEX:
if (_ButtonPressedState.getgamepadB()) {
_ButtonPressedState.setgamepadB(false);
}
break;
default:
// No-op
break;
}
}
}
}
}
}
// Schedule the next one
requestAnimationFrame(runInputLoop);
}
runInputLoop();
// The gamepadInputEmulation is a string property that exists in JavaScript UWAs and in WebViews in UWAs.
// It won't exist in Win8.1 style apps or browsers.
if (window.navigator && typeof window.navigator.gamepadInputEmulation === "string") {
// We want the gamepad to provide gamepad VK keyboard events rather than moving a
// mouse like cursor. Set to "keyboard", the gamepad will provide such keyboard events
// and provide input to the DOM navigator.getGamepads API.
window.navigator.gamepadInputEmulation = "gamepad";
}
})();

View File

@ -0,0 +1,113 @@
define(['inputManager', 'focusManager', 'browser', 'layoutManager', 'events', 'dom'], function (inputmanager, focusManager, browser, layoutManager, events, dom) {
'use strict';
var self = {};
var lastMouseInputTime = new Date().getTime();
var isMouseIdle;
function mouseIdleTime() {
return new Date().getTime() - lastMouseInputTime;
}
function notifyApp() {
inputmanager.notifyMouseMove();
}
var lastMouseMoveData;
dom.addEventListener(document, 'mousemove', function (e) {
var eventX = e.screenX;
var eventY = e.screenY;
// if coord don't exist how could it move
if (typeof eventX === "undefined" && typeof eventY === "undefined") {
return;
}
var obj = lastMouseMoveData;
if (!obj) {
lastMouseMoveData = {
x: eventX,
y: eventY
};
return;
}
// if coord are same, it didn't move
if (Math.abs(eventX - obj.x) < 10 && Math.abs(eventY - obj.y) < 10) {
return;
}
obj.x = eventX;
obj.y = eventY;
lastMouseInputTime = new Date().getTime();
notifyApp();
if (isMouseIdle) {
isMouseIdle = false;
document.body.classList.remove('mouseIdle');
events.trigger(self, 'mouseactive');
}
}, {
passive: true
});
function onMouseEnter(e) {
var parent = focusManager.focusableParent(e.target);
if (parent) {
focusManager.focus(e.target);
}
}
function enableFocusWithMouse() {
if (!layoutManager.tv) {
return false;
}
if (browser.xboxOne) {
return true;
}
if (browser.tv) {
return true;
}
return false;
}
function initMouseFocus() {
dom.removeEventListener(document, 'mouseenter', onMouseEnter, {
capture: true,
passive: true
});
if (enableFocusWithMouse()) {
dom.addEventListener(document, 'mouseenter', onMouseEnter, {
capture: true,
passive: true
});
}
}
initMouseFocus();
events.on(layoutManager, 'modechange', initMouseFocus);
setInterval(function () {
if (mouseIdleTime() >= 5000) {
isMouseIdle = true;
document.body.classList.add('mouseIdle');
events.trigger(self, 'mouseidle');
}
}, 5000);
return self;
});

View File

@ -0,0 +1,94 @@
define([], function () {
'use strict';
function LazyLoader(options) {
this.options = options;
}
LazyLoader.prototype.createObserver = function () {
var observerOptions = {};
var options = this.options;
var loadedCount = 0;
var callback = options.callback;
//options.rootMargin = "300%";
var observerId = 'obs' + new Date().getTime();
var self = this;
var observer = new IntersectionObserver(function (entries) {
for (var j = 0, length2 = entries.length; j < length2; j++) {
var entry = entries[j];
var target = entry.target;
observer.unobserve(target);
if (!target[observerId]) {
target[observerId] = 1;
callback(target);
loadedCount++;
if (loadedCount >= self.elementCount) {
self.destroyObserver();
}
}
}
},
observerOptions
);
this.observer = observer;
};
LazyLoader.prototype.addElements = function (elements) {
var observer = this.observer;
if (!observer) {
this.createObserver();
observer = this.observer;
}
this.elementCount = (this.elementCount || 0) + elements.length;
for (var i = 0, length = elements.length; i < length; i++) {
observer.observe(elements[i]);
}
};
LazyLoader.prototype.destroyObserver = function (elements) {
var observer = this.observer;
if (observer) {
observer.disconnect();
this.observer = null;
}
};
LazyLoader.prototype.destroy = function (elements) {
this.destroyObserver();
this.options = null;
};
function unveilElements(elements, root, callback) {
if (!elements.length) {
return;
}
var lazyLoader = new LazyLoader({
callback: callback
});
lazyLoader.addElements(elements);
}
LazyLoader.lazyChildren = function (elem, callback) {
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
};
return LazyLoader;
});

View File

@ -0,0 +1,185 @@
define(['visibleinviewport', 'browser', 'dom'], function (visibleinviewport, browser, dom) {
'use strict';
var thresholdX;
var thresholdY;
var requestIdleCallback = window.requestIdleCallback || function (fn) {
fn();
};
function resetThresholds() {
var x = screen.availWidth;
var y = screen.availHeight;
if (browser.touch) {
x *= 1.5;
y *= 1.5;
}
thresholdX = x;
thresholdY = y;
}
dom.addEventListener(window, "orientationchange", resetThresholds, { passive: true });
dom.addEventListener(window, 'resize', resetThresholds, { passive: true });
resetThresholds();
function isVisible(elem) {
return visibleinviewport(elem, true, thresholdX, thresholdY);
}
var wheelEvent = (document.implementation.hasFeature('Event.wheel', '3.0') ? 'wheel' : 'mousewheel');
var self = {};
function cancelAll(tokens) {
for (var i = 0, length = tokens.length; i < length; i++) {
tokens[i] = true;
}
}
function unveilElementsInternal(instance, callback) {
var unveiledElements = [];
var cancellationTokens = [];
var loadedCount = 0;
function unveilInternal(tokenIndex) {
var anyFound = false;
var out = false;
var elements = instance.elements;
// TODO: This out construct assumes left to right, top to bottom
for (var i = 0, length = elements.length; i < length; i++) {
if (cancellationTokens[tokenIndex]) {
return;
}
if (unveiledElements[i]) {
continue;
}
var elem = elements[i];
if (!out && isVisible(elem)) {
anyFound = true;
unveiledElements[i] = true;
callback(elem);
loadedCount++;
} else {
if (anyFound) {
out = true;
}
}
}
if (loadedCount >= elements.length) {
dom.removeEventListener(document, 'focus', unveil, {
capture: true,
passive: true
});
dom.removeEventListener(document, 'scroll', unveil, {
capture: true,
passive: true
});
dom.removeEventListener(document, wheelEvent, unveil, {
capture: true,
passive: true
});
dom.removeEventListener(window, 'resize', unveil, {
capture: true,
passive: true
});
}
}
function unveil() {
cancelAll(cancellationTokens);
var index = cancellationTokens.length;
cancellationTokens.length++;
setTimeout(function () {
unveilInternal(index);
}, 1);
}
dom.addEventListener(document, 'focus', unveil, {
capture: true,
passive: true
});
dom.addEventListener(document, 'scroll', unveil, {
capture: true,
passive: true
});
dom.addEventListener(document, wheelEvent, unveil, {
capture: true,
passive: true
});
dom.addEventListener(window, 'resize', unveil, {
capture: true,
passive: true
});
unveil();
}
function LazyLoader(options) {
this.options = options;
}
LazyLoader.prototype.createObserver = function () {
unveilElementsInternal(this, this.options.callback);
this.observer = 1;
};
LazyLoader.prototype.addElements = function (elements) {
this.elements = this.elements || [];
for (var i = 0, length = elements.length; i < length; i++) {
this.elements.push(elements[i]);
}
var observer = this.observer;
if (!observer) {
this.createObserver();
}
};
LazyLoader.prototype.destroyObserver = function (elements) {
};
LazyLoader.prototype.destroy = function (elements) {
this.destroyObserver();
this.options = null;
};
function unveilElements(elements, root, callback) {
if (!elements.length) {
return;
}
var lazyLoader = new LazyLoader({
callback: callback
});
lazyLoader.addElements(elements);
}
LazyLoader.lazyChildren = function (elem, callback) {
unveilElements(elem.getElementsByClassName('lazy'), elem, callback);
};
return LazyLoader;
});

View File

@ -0,0 +1,137 @@
# Native Promise Only (NPO)
A polyfill for native ES6 Promises as close as possible (no extensions) to the strict spec definitions.
## Intent
The aim of this project is to be the smallest polyfill for Promises, staying as close as possible to what's specified in both [Promises/A+](http://promisesaplus.com) and the [upcoming ES6 specification](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-promise-objects).
An equally important goal is to avoid exposing any capability for promise-state to be mutated externally. The [Known Limitations](#known-limitations) section below explains the trade-offs of that balance.
## Usage
To use this polyfill in the browser, include the "npo.js" file (see the instructions in [Tests/Compliance section](#testscompliance) below for how to build "npo.js" if you don't have it already) with your site's scripts. It's a polyfill, which means it will not overwrite `Promise` if it exists as a global already, so it's safe to include unconditionally.
To use with AMD, import the "npo.js" file module.
To install the polyfill via bower, run:
```
bower install native-promise-only
```
To install the polyfill via npm, run:
```
npm install native-promise-only
```
Then require the module into your node code:
```js
require("native-promise-only");
```
Notice that using the module in this way, we don't assign the module's public API to any variable. **We don't need to**, because it's a polyfill that intentionally patches the global environment (in this case to the `Promise` name) once included.
If you *want* to also have a reference pointing to the same `Promise` global, you *can also* assign the return value from the `require(..)` statement, but it's strongly recommended that you use the same `Promise` name so as to not create confusion:
```js
var Promise = require("native-promise-only");
// Promise === global.Promise; // true!
```
Other than the below [Known Limitations](#known-limitations) discussion and some browser bugs (such as [these](https://gist.github.com/getify/bd11ccf1eff2efdac0fb)) which **this polyfill doesn't suffer from**, your promises should operate the same in all JS environments.
Exactly like native promises, here's a quick example of how you create and use the polyfilled promises:
```js
var p = new Promise(function(resolve,reject){
setTimeout(function(){
resolve("Yay!");
},100);
});
p.then(function(msg){
console.log(msg); // Yay!
});
```
For more on promises, check these blog posts out:
1. Back-story on the hows and whys behind promises (chaining, errors, etc): [multi-part blog post series "Promises"](http://blog.getify.com/promises-part-1/) by [getify](http://twitter.com/getify) (me).
2. Using and enjoying native promises: [JavaScript Promises](http://www.html5rocks.com/en/tutorials/es6/promises/) by [Jake Archibald](http://twitter.com/jaffathecake).
## Known Limitations
A promise object from this polyfill **will be** an instance of the `Promise` constructor, which makes identification of genuine promises easier:
```js
var p = new Promise(..);
p instanceof Promise; // true
```
However, these promise instances don't inherit (delegate to) a *meaningful* `Promise.prototype` object for their methods (there is one, it's just mostly empty).
Consider:
```js
var p = new Promise(..);
Object.getOwnPropertyNames( p ); // [ then, catch ]
Object.getOwnPropertyNames( Promise.prototype ); // [ constructor ]
```
As such, these promises are not really "sub-classable" in the ES6 `class` / `extends` sense, though theoretically you should be able to do that in ES6 with the built-in Promises.
To read a full explanation of why, read [Part 3: The Trust Problem](http://blog.getify.com/promises-part-3/) of my blog post series on Promises.
Briefly, the reason for this deviation is that there's a choice between having delegated methods on the `.prototype` or having private state. Since **the spirit of promises was always to ensure trustability** -- that promises were immutable (from the outside) to everyone except the initial resolver/deferred -- private state is a critically important feature to preserve.
Many other ES6 promise shims/libs seem to have forgotten that important point, as many of them either expose the state publicly on the object instance or provide public accessor methods which can externally mutate a promise's state. Both of these deviations are **intolerable** in my opinion, so this library chose the opposite trade-off: *no ES6 sub-classing*.
Any trade-off is a shame, but this one is the least of a few evils, and probably won't prove to limit very many, as there are only a limited number of use-cases for `extend`ing `Promise` in the ES6 sub-class sense.
## Still Want More?
This project intentionally adheres pretty strictly to the narrow core of [Promises/A+](http://promisesaplus.com) as adopted/implemented by ES6 into the native `Promise()` mechanism.
But it's quite likely that you will [experience a variety of scenarios](http://blog.getify.com/promises-part-5/) in which using *only* native promises might be tedious, limiting, or more trouble than it's worth. There's good reason why most other **Promises/A+** "compliant" libs are actually superset extensions on the narrow core: **because async flow-control is often quite complex in the real world.**
*Native Promise Only* will **NOT** add any of these extra flourishes. Sorry.
**However, I have another project**: [asynquence](http://github.com/getify/asynquence) (async + sequence). It's an abstraction on top of the promises concept (promises are hidden inside), designed to drastically improve the readability and expressiveness of your async flow-control code.
You simply express your async flow-control and *asynquence* creates and chains all the promises for you underneath. **Super simple.**
*asynquence* has a custom implementation for the internal "promises" it uses, and as such does not need native `Promises`, nor does it need/include this polyfill.
Get your feet wet with native promises first, but then when you go looking for something more, consider [asynquence](http://github.com/getify/asynquence) (which is [vastly more powerful](http://davidwalsh.name/asynquence-part-1) and is still only ~2k!).
## Tests/Compliance
<a href="http://promisesaplus.com/" float="right">
<img src="http://promisesaplus.com/assets/logo-small.png" alt="Promises/A+ logo"
title="Promises/A+ 1.1 compliant" align="right" />
</a>
*Native Promise Only* is "spec compliant" in the sense of passing all tests in the [Promises/A+ Test Suite](https://github.com/promises-aplus/promises-tests).
To run all tests:
1. Either git-clone this repo or run `npm install native-promise-only`, and then switch into that project root.
2. Run `npm install` in the project root to install the dev-dependencies.
3. If you didn't get *native-promise-only* from npm, then from the project root, run `./build.js` or `node build.js` or `npm run build` to generate the minified "npo.js" in the project root.
4. Finally, run `npm test`.
**Note:** Other tests need to be added, such as testing the `Promise()` constructor's behavior, as well as the `Promise.*` static helpers (`resolve(..)`, `reject(..)`, `all(..)`, and `race(..)`), none of which are covered by the Promises/A+ test suite.
Developing a more comprehensive test-suite to augment the Promises/A+ test suite **is now another primary goal** of this project.
## License
The code and all the documentation are released under the MIT license.
http://getify.mit-license.org/

View File

@ -0,0 +1,373 @@
/*! Native Promise Only
v0.8.0-a (c) Kyle Simpson
MIT License: http://getify.mit-license.org
*/
(function UMD(name,context,definition){
// special form of UMD for polyfilling across evironments
context[name] = definition();
if (typeof module != "undefined" && module.exports) { module.exports = context[name]; }
else if (typeof define == "function" && define.amd) { define(function $AMD$(){ return context[name]; }); }
})("Promise",typeof global != "undefined" ? global : this,function DEF(){
/*jshint validthis:true */
"use strict";
var builtInProp, cycle, scheduling_queue,
ToString = Object.prototype.toString,
timer = (typeof setImmediate != "undefined") ?
function timer(fn) { return setImmediate(fn); } :
setTimeout
;
// dammit, IE8.
try {
Object.defineProperty({},"x",{});
builtInProp = function builtInProp(obj,name,val,config) {
return Object.defineProperty(obj,name,{
value: val,
writable: true,
configurable: config !== false
});
};
}
catch (err) {
builtInProp = function builtInProp(obj,name,val) {
obj[name] = val;
return obj;
};
}
// Note: using a queue instead of array for efficiency
scheduling_queue = (function Queue() {
var first, last, item;
function Item(fn,self) {
this.fn = fn;
this.self = self;
this.next = void 0;
}
return {
add: function add(fn,self) {
item = new Item(fn,self);
if (last) {
last.next = item;
}
else {
first = item;
}
last = item;
item = void 0;
},
drain: function drain() {
var f = first;
first = last = cycle = void 0;
while (f) {
f.fn.call(f.self);
f = f.next;
}
}
};
})();
function schedule(fn,self) {
scheduling_queue.add(fn,self);
if (!cycle) {
cycle = timer(scheduling_queue.drain);
}
}
// promise duck typing
function isThenable(o) {
var _then, o_type = typeof o;
if (o != null &&
(
o_type == "object" || o_type == "function"
)
) {
_then = o.then;
}
return typeof _then == "function" ? _then : false;
}
function notify() {
for (var i=0; i<this.chain.length; i++) {
notifyIsolated(
this,
(this.state === 1) ? this.chain[i].success : this.chain[i].failure,
this.chain[i]
);
}
this.chain.length = 0;
}
// NOTE: This is a separate function to isolate
// the `try..catch` so that other code can be
// optimized better
function notifyIsolated(self,cb,chain) {
var ret, _then;
try {
if (cb === false) {
chain.reject(self.msg);
}
else {
if (cb === true) {
ret = self.msg;
}
else {
ret = cb.call(void 0,self.msg);
}
if (ret === chain.promise) {
chain.reject(TypeError("Promise-chain cycle"));
}
else if (_then = isThenable(ret)) {
_then.call(ret,chain.resolve,chain.reject);
}
else {
chain.resolve(ret);
}
}
}
catch (err) {
chain.reject(err);
}
}
function resolve(msg) {
var _then, self = this;
// already triggered?
if (self.triggered) { return; }
self.triggered = true;
// unwrap
if (self.def) {
self = self.def;
}
try {
if (_then = isThenable(msg)) {
schedule(function(){
var def_wrapper = new MakeDefWrapper(self);
try {
_then.call(msg,
function $resolve$(){ resolve.apply(def_wrapper,arguments); },
function $reject$(){ reject.apply(def_wrapper,arguments); }
);
}
catch (err) {
reject.call(def_wrapper,err);
}
})
}
else {
self.msg = msg;
self.state = 1;
if (self.chain.length > 0) {
schedule(notify,self);
}
}
}
catch (err) {
reject.call(new MakeDefWrapper(self),err);
}
}
function reject(msg) {
var self = this;
// already triggered?
if (self.triggered) { return; }
self.triggered = true;
// unwrap
if (self.def) {
self = self.def;
}
self.msg = msg;
self.state = 2;
if (self.chain.length > 0) {
schedule(notify,self);
}
}
function iteratePromises(Constructor,arr,resolver,rejecter) {
for (var idx=0; idx<arr.length; idx++) {
(function IIFE(idx){
Constructor.resolve(arr[idx])
.then(
function $resolver$(msg){
resolver(idx,msg);
},
rejecter
);
})(idx);
}
}
function MakeDefWrapper(self) {
this.def = self;
this.triggered = false;
}
function MakeDef(self) {
this.promise = self;
this.state = 0;
this.triggered = false;
this.chain = [];
this.msg = void 0;
}
function Promise(executor) {
if (typeof executor != "function") {
throw TypeError("Not a function");
}
if (this.__NPO__ !== 0) {
throw TypeError("Not a promise");
}
// instance shadowing the inherited "brand"
// to signal an already "initialized" promise
this.__NPO__ = 1;
var def = new MakeDef(this);
this["then"] = function then(success,failure) {
var o = {
success: typeof success == "function" ? success : true,
failure: typeof failure == "function" ? failure : false
};
// Note: `then(..)` itself can be borrowed to be used against
// a different promise constructor for making the chained promise,
// by substituting a different `this` binding.
o.promise = new this.constructor(function extractChain(resolve,reject) {
if (typeof resolve != "function" || typeof reject != "function") {
throw TypeError("Not a function");
}
o.resolve = resolve;
o.reject = reject;
});
def.chain.push(o);
if (def.state !== 0) {
schedule(notify,def);
}
return o.promise;
};
this["catch"] = function $catch$(failure) {
return this.then(void 0,failure);
};
try {
executor.call(
void 0,
function publicResolve(msg){
resolve.call(def,msg);
},
function publicReject(msg) {
reject.call(def,msg);
}
);
}
catch (err) {
reject.call(def,err);
}
}
var PromisePrototype = builtInProp({},"constructor",Promise,
/*configurable=*/false
);
// Note: Android 4 cannot use `Object.defineProperty(..)` here
Promise.prototype = PromisePrototype;
// built-in "brand" to signal an "uninitialized" promise
builtInProp(PromisePrototype,"__NPO__",0,
/*configurable=*/false
);
builtInProp(Promise,"resolve",function Promise$resolve(msg) {
var Constructor = this;
// spec mandated checks
// note: best "isPromise" check that's practical for now
if (msg && typeof msg == "object" && msg.__NPO__ === 1) {
return msg;
}
return new Constructor(function executor(resolve,reject){
if (typeof resolve != "function" || typeof reject != "function") {
throw TypeError("Not a function");
}
resolve(msg);
});
});
builtInProp(Promise,"reject",function Promise$reject(msg) {
return new this(function executor(resolve,reject){
if (typeof resolve != "function" || typeof reject != "function") {
throw TypeError("Not a function");
}
reject(msg);
});
});
builtInProp(Promise,"all",function Promise$all(arr) {
var Constructor = this;
// spec mandated checks
if (ToString.call(arr) != "[object Array]") {
return Constructor.reject(TypeError("Not an array"));
}
if (arr.length === 0) {
return Constructor.resolve([]);
}
return new Constructor(function executor(resolve,reject){
if (typeof resolve != "function" || typeof reject != "function") {
throw TypeError("Not a function");
}
var len = arr.length, msgs = Array(len), count = 0;
iteratePromises(Constructor,arr,function resolver(idx,msg) {
msgs[idx] = msg;
if (++count === len) {
resolve(msgs);
}
},reject);
});
});
builtInProp(Promise,"race",function Promise$race(arr) {
var Constructor = this;
// spec mandated checks
if (ToString.call(arr) != "[object Array]") {
return Constructor.reject(TypeError("Not an array"));
}
return new Constructor(function executor(resolve,reject){
if (typeof resolve != "function" || typeof reject != "function") {
throw TypeError("Not a function");
}
iteratePromises(Constructor,arr,function resolver(idx,msg){
resolve(msg);
},reject);
});
});
return Promise;
});

View File

@ -0,0 +1,21 @@
// Adapter for "promises-aplus-tests" test runner
var path = require("path");
var Promise = require(path.join(__dirname,"lib","npo.src.js"));
module.exports.deferred = function __deferred__() {
var o = {};
o.promise = new Promise(function __Promise__(resolve,reject){
o.resolve = resolve;
o.reject = reject;
});
return o;
};
module.exports.resolved = function __resolved__(val) {
return Promise.resolve(val);
};
module.exports.rejected = function __rejected__(reason) {
return Promise.reject(reason);
};

View File

@ -0,0 +1,148 @@
define(['appSettings', 'pluginManager'], function (appSettings, pluginManager) {
'use strict';
function packageManager() {
var self = this;
var settingsKey = 'installedpackages1';
var packages = [];
self.packages = function () {
return packages.slice(0);
};
function addPackage(pkg) {
packages = packages.filter(function (p) {
return p.name !== pkg.name;
});
packages.push(pkg);
}
self.install = function (url) {
return loadPackage(url, true).then(function (pkg) {
var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]');
if (manifestUrls.indexOf(url) === -1) {
manifestUrls.push(url);
appSettings.set(settingsKey, JSON.stringify(manifestUrls));
}
return pkg;
});
};
self.uninstall = function (name) {
var pkg = packages.filter(function (p) {
return p.name === name;
})[0];
if (pkg) {
packages = packages.filter(function (p) {
return p.name !== name;
});
removeUrl(pkg.url);
}
return Promise.resolve();
};
function removeUrl(url) {
var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]');
manifestUrls = manifestUrls.filter(function (i) {
return i !== url;
});
appSettings.set(settingsKey, JSON.stringify(manifestUrls));
}
self.init = function () {
var manifestUrls = JSON.parse(appSettings.get(settingsKey) || '[]');
return Promise.all(manifestUrls.map(loadPackage)).then(function () {
return Promise.resolve();
}, function () {
return Promise.resolve();
});
};
function loadPackage(url, throwError) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
var originalUrl = url;
url += url.indexOf('?') === -1 ? '?' : '&';
url += 't=' + new Date().getTime();
xhr.open('GET', url, true);
var onError = function () {
if (throwError === true) {
reject();
} else {
removeUrl(originalUrl);
resolve();
}
};
xhr.onload = function (e) {
if (this.status < 400) {
var pkg = JSON.parse(this.response);
pkg.url = originalUrl;
addPackage(pkg);
var plugins = pkg.plugins || [];
if (pkg.plugin) {
plugins.push(pkg.plugin);
}
var promises = plugins.map(function (pluginUrl) {
return pluginManager.loadPlugin(self.mapPath(pkg, pluginUrl));
});
Promise.all(promises).then(resolve, resolve);
} else {
onError();
}
};
xhr.onerror = onError;
xhr.send();
});
}
self.mapPath = function (pkg, pluginUrl) {
var urlLower = pluginUrl.toLowerCase();
if (urlLower.indexOf('http:') === 0 || urlLower.indexOf('https:') === 0 || urlLower.indexOf('file:') === 0) {
return pluginUrl;
}
var packageUrl = pkg.url;
packageUrl = packageUrl.substring(0, packageUrl.lastIndexOf('/'));
packageUrl += '/';
packageUrl += pluginUrl;
return packageUrl;
};
}
return new packageManager();
});

View File

@ -0,0 +1,63 @@
define([], function () {
'use strict';
function supportsHtmlMediaAutoplay() {
return new Promise(function (resolve, reject) {
var timeout;
var elem = document.createElement('video');
var elemStyle = elem.style;
//skip the test if video itself, or the autoplay
//element on it isn't supported
if (!('autoplay' in elem)) {
reject();
return;
}
elemStyle.position = 'absolute';
elemStyle.height = 0;
elemStyle.width = 0;
elem.setAttribute('autoplay', 'autoplay');
elem.style.display = 'none';
document.body.appendChild(elem);
var testAutoplay = function (arg) {
clearTimeout(timeout);
elem.removeEventListener('playing', testAutoplay);
elem.removeEventListener('play', testAutoplay);
var supported = (arg && arg.type === 'playing') || (arg && arg.type === 'play') || elem.currentTime !== 0;
elem.parentNode.removeChild(elem);
if (supported) {
resolve();
} else {
reject();
}
};
// play needed for firefox
elem.addEventListener('play', testAutoplay);
elem.addEventListener('playing', testAutoplay);
try {
elem.src = 'data:video/mp4;base64,AAAAHGZ0eXBtcDQyAAAAAG1wNDJpc29tYXZjMQAAAz5tb292AAAAbG12aGQAAAAAzaNacc2jWnEAAV+QAAFfkAABAAABAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAGGlvZHMAAAAAEICAgAcAT////3//AAACQ3RyYWsAAABcdGtoZAAAAAHNo1pxzaNacQAAAAEAAAAAAAFfkAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAEAAAAAAEAAAABAAAAAAAd9tZGlhAAAAIG1kaGQAAAAAzaNacc2jWnEAAV+QAAFfkFXEAAAAAAAhaGRscgAAAAAAAAAAdmlkZQAAAAAAAAAAAAAAAAAAAAGWbWluZgAAABR2bWhkAAAAAQAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABVnN0YmwAAACpc3RzZAAAAAAAAAABAAAAmWF2YzEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAEAAQAEgAAABIAAAAAAAAAAEOSlZUL0FWQyBDb2RpbmcAAAAAAAAAAAAAAAAAAAAAAAAY//8AAAAxYXZjQwH0AAr/4QAZZ/QACq609NQYBBkAAAMAAQAAAwAKjxImoAEABWjOAa8gAAAAEmNvbHJuY2xjAAYAAQAGAAAAGHN0dHMAAAAAAAAAAQAAAAUAAEZQAAAAKHN0c3oAAAAAAAAAAAAAAAUAAAIqAAAACAAAAAgAAAAIAAAACAAAAChzdHNjAAAAAAAAAAIAAAABAAAABAAAAAEAAAACAAAAAQAAAAEAAAAYc3RjbwAAAAAAAAACAAADYgAABaQAAAAUc3RzcwAAAAAAAAABAAAAAQAAABFzZHRwAAAAAAREREREAAAAb3VkdGEAAABnbWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcgAAAAAAAAAAAAAAAAAAAAA6aWxzdAAAADKpdG9vAAAAKmRhdGEAAAABAAAAAEhhbmRCcmFrZSAwLjkuOCAyMDEyMDcxODAwAAACUm1kYXQAAAHkBgX/4NxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxMjAgLSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDExIC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9uczogY2FiYWM9MCByZWY9MSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjAgbWU9ZXNhIHN1Ym1lPTkgcHN5PTAgbWl4ZWRfcmVmPTAgbWVfcmFuZ2U9NCBjaHJvbWFfbWU9MSB0cmVsbGlzPTAgOHg4ZGN0PTAgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0wIGNocm9tYV9xcF9vZmZzZXQ9MCB0aHJlYWRzPTYgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MCB3ZWlnaHRwPTAga2V5aW50PTUwIGtleWludF9taW49NSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmM9Y3FwIG1idHJlZT0wIHFwPTAAgAAAAD5liISscR8A+E4ACAACFoAAITAAAgsAAPgYCoKgoC+L4vi+KAvi+L4YfAEAACMzgABF9AAEUGUgABDJiXnf4AAAAARBmiKUAAAABEGaQpQAAAAEQZpilAAAAARBmoKU';
var promise = elem.play();
if (promise && promise.catch) {
promise.catch(reject);
}
timeout = setTimeout(testAutoplay, 500);
}
catch (e) {
reject();
return;
}
});
}
return {
supportsHtmlMediaAutoplay: supportsHtmlMediaAutoplay
};
});

View File

@ -0,0 +1,87 @@
define([], function () {
'use strict';
function getNowPlayingNames(nowPlayingItem, includeNonNameInfo) {
var topItem = nowPlayingItem;
var bottomItem = null;
var topText = nowPlayingItem.Name;
if (nowPlayingItem.AlbumId && nowPlayingItem.MediaType === 'Audio') {
topItem = {
Id: nowPlayingItem.AlbumId,
Name: nowPlayingItem.Album,
Type: 'MusicAlbum',
IsFolder: true
};
}
if (nowPlayingItem.MediaType === 'Video') {
if (nowPlayingItem.IndexNumber != null) {
topText = nowPlayingItem.IndexNumber + " - " + topText;
}
if (nowPlayingItem.ParentIndexNumber != null) {
topText = nowPlayingItem.ParentIndexNumber + "." + topText;
}
}
var bottomText = '';
if (nowPlayingItem.Artists && nowPlayingItem.Artists.length) {
if (nowPlayingItem.ArtistItems && nowPlayingItem.ArtistItems.length) {
bottomItem = {
Id: nowPlayingItem.ArtistItems[0].Id,
Name: nowPlayingItem.ArtistItems[0].Name,
Type: 'MusicArtist',
IsFolder: true
};
bottomText = bottomItem.Name;
} else {
bottomText = nowPlayingItem.Artists[0];
}
}
else if (nowPlayingItem.SeriesName || nowPlayingItem.Album) {
bottomText = topText;
topText = nowPlayingItem.SeriesName || nowPlayingItem.Album;
bottomItem = topItem;
if (nowPlayingItem.SeriesId) {
topItem = {
Id: nowPlayingItem.SeriesId,
Name: nowPlayingItem.SeriesName,
Type: 'Series',
IsFolder: true
};
} else {
topItem = null;
}
}
else if (nowPlayingItem.ProductionYear && includeNonNameInfo !== false) {
bottomText = nowPlayingItem.ProductionYear;
}
var list = [];
list.push({
text: topText,
item: topItem
});
if (bottomText) {
list.push({
text: bottomText,
item: bottomItem
});
}
return list;
}
return {
getNowPlayingNames: getNowPlayingNames
};
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,60 @@
define(['playbackManager'], function (playbackManager) {
"use strict";
return function () {
var self = this;
self.name = 'Playback validation';
self.type = 'preplayintercept';
self.id = 'playbackvalidation';
self.order = -1;
self.intercept = function (options) {
// Don't care about video backdrops, or theme music or any kind of non-fullscreen playback
if (!options.fullscreen) {
return Promise.resolve();
}
return validatePlayback(options);
};
function validatePlayback(options) {
return new Promise(function (resolve, reject) {
require(["registrationServices"], function (registrationServices) {
registrationServices.validateFeature('playback', options).then(function (result) {
if (result && result.enableTimeLimit) {
startAutoStopTimer();
}
resolve();
});
});
});
}
var autoStopTimeout;
var lockedTimeLimitMs = 63000;
function startAutoStopTimer() {
stopAutoStopTimer();
autoStopTimeout = setTimeout(onAutoStopTimeout, lockedTimeLimitMs);
}
function onAutoStopTimeout() {
stopAutoStopTimer();
playbackManager.stop();
}
function stopAutoStopTimer() {
var timeout = autoStopTimeout;
if (timeout) {
clearTimeout(timeout);
autoStopTimeout = null;
}
}
};
});

View File

@ -0,0 +1,215 @@
define(['appSettings', 'events', 'browser', 'loading', 'playbackManager', 'embyRouter', 'globalize', 'apphost'], function (appSettings, events, browser, loading, playbackManager, embyRouter, globalize, appHost) {
'use strict';
var currentDisplayInfo;
function mirrorItem(info, player) {
var item = info.item;
playbackManager.displayContent({
ItemName: item.Name,
ItemId: item.Id,
ItemType: item.Type,
Context: info.context
}, player);
}
function mirrorIfEnabled(info) {
info = info || currentDisplayInfo;
if (info && playbackManager.enableDisplayMirroring()) {
var player = playbackManager.getPlayerInfo();
if (player) {
if (!player.isLocalPlayer && player.supportedCommands.indexOf('DisplayContent') !== -1) {
mirrorItem(info, player);
}
}
}
}
function showPlayerSelection(button) {
var currentPlayerInfo = playbackManager.getPlayerInfo();
if (currentPlayerInfo) {
if (!currentPlayerInfo.isLocalPlayer) {
showActivePlayerMenu(currentPlayerInfo);
return;
}
}
var currentPlayerId = currentPlayerInfo ? currentPlayerInfo.id : null;
loading.show();
playbackManager.getTargets().then(function (targets) {
var menuItems = targets.map(function (t) {
var name = t.name;
if (t.appName && t.appName !== t.name) {
name += " - " + t.appName;
}
return {
name: name,
id: t.id,
selected: currentPlayerId === t.id
};
});
require(['actionsheet'], function (actionsheet) {
loading.hide();
var menuOptions = {
title: globalize.translate('sharedcomponents#HeaderSelectPlayer'),
items: menuItems,
positionTo: button,
resolveOnClick: true
};
// Unfortunately we can't allow the url to change or chromecast will throw a security error
// Might be able to solve this in the future by moving the dialogs to hashbangs
if (!(!browser.chrome || appHost.supports('castmenuhashchange'))) {
menuOptions.enableHistory = false;
}
actionsheet.show(menuOptions).then(function (id) {
var target = targets.filter(function (t) {
return t.id === id;
})[0];
playbackManager.trySetActivePlayer(target.playerName, target);
mirrorIfEnabled();
});
});
});
}
function showActivePlayerMenu(playerInfo) {
require(['dialogHelper', 'dialog', 'emby-checkbox', 'emby-button'], function (dialogHelper) {
showActivePlayerMenuInternal(dialogHelper, playerInfo);
});
}
function showActivePlayerMenuInternal(dialogHelper, playerInfo) {
var html = '';
var dialogOptions = {
removeOnClose: true
};
dialogOptions.modal = false;
dialogOptions.entryAnimationDuration = 160;
dialogOptions.exitAnimationDuration = 160;
dialogOptions.autoFocus = false;
var dlg = dialogHelper.createDialog(dialogOptions);
dlg.classList.add('promptDialog');
html += '<div class="promptDialogContent" style="padding:1.5em;">';
html += '<h2 style="margin-top:.5em;">';
html += (playerInfo.deviceName || playerInfo.name);
html += '</h2>';
html += '<div>';
if (playerInfo.supportedCommands.indexOf('DisplayContent') !== -1) {
html += '<label class="checkboxContainer">';
var checkedHtml = playbackManager.enableDisplayMirroring() ? ' checked' : '';
html += '<input type="checkbox" is="emby-checkbox" class="chkMirror"' + checkedHtml + '/>';
html += '<span>' + globalize.translate('sharedcomponents#EnableDisplayMirroring') + '</span>';
html += '</label>';
}
html += '</div>';
html += '<div style="margin-top:1em;display:flex;justify-content: flex-end;">';
html += '<button is="emby-button" type="button" class="button-flat button-accent-flat btnRemoteControl promptDialogButton">' + globalize.translate('sharedcomponents#HeaderRemoteControl') + '</button>';
html += '<button is="emby-button" type="button" class="button-flat button-accent-flat btnDisconnect promptDialogButton ">' + globalize.translate('sharedcomponents#Disconnect') + '</button>';
html += '<button is="emby-button" type="button" class="button-flat button-accent-flat btnCancel promptDialogButton">' + globalize.translate('sharedcomponents#ButtonCancel') + '</button>';
html += '</div>';
html += '</div>';
dlg.innerHTML = html;
var chkMirror = dlg.querySelector('.chkMirror');
if (chkMirror) {
chkMirror.addEventListener('change', onMirrorChange);
}
var destination = '';
var btnRemoteControl = dlg.querySelector('.btnRemoteControl');
if (btnRemoteControl) {
btnRemoteControl.addEventListener('click', function () {
destination = 'nowplaying.html';
dialogHelper.close(dlg);
});
}
dlg.querySelector('.btnDisconnect').addEventListener('click', function () {
playbackManager.disconnectFromPlayer();
dialogHelper.close(dlg);
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
dialogHelper.open(dlg).then(function () {
if (destination) {
embyRouter.show(destination);
}
});
}
function onMirrorChange() {
playbackManager.enableDisplayMirroring(this.checked);
}
document.addEventListener('viewbeforeshow', function () {
currentDisplayInfo = null;
});
document.addEventListener('viewshow', function (e) {
var state = e.detail.state || {};
var item = state.item;
if (item && item.ServerId) {
mirrorIfEnabled({
item: item
});
return;
}
});
events.on(appSettings, 'change', function (e, name) {
if (name === 'displaymirror') {
mirrorIfEnabled();
}
});
return {
show: showPlayerSelection
};
});

View File

@ -0,0 +1,164 @@
define(['actionsheet', 'datetime', 'playbackManager', 'globalize', 'appSettings', 'qualityoptions'], function (actionsheet, datetime, playbackManager, globalize, appSettings, qualityoptions) {
'use strict';
function showQualityMenu(player, btn) {
var videoStream = playbackManager.currentMediaSource(player).MediaStreams.filter(function (stream) {
return stream.Type === "Video";
})[0];
var videoWidth = videoStream ? videoStream.Width : null;
var options = qualityoptions.getVideoQualityOptions({
currentMaxBitrate: playbackManager.getMaxStreamingBitrate(player),
isAutomaticBitrateEnabled: playbackManager.enableAutomaticBitrateDetection(player),
videoWidth: videoWidth,
enableAuto: true
});
var menuItems = options.map(function (o) {
var opt = {
name: o.name,
id: o.bitrate,
secondaryText: o.secondaryText
};
if (o.selected) {
opt.selected = true;
}
return opt;
});
var selectedId = options.filter(function (o) {
return o.selected;
});
selectedId = selectedId.length ? selectedId[0].bitrate : null;
return actionsheet.show({
items: menuItems,
positionTo: btn
}).then(function (id) {
var bitrate = parseInt(id);
if (bitrate !== selectedId) {
playbackManager.setMaxStreamingBitrate({
enableAutomaticBitrateDetection: bitrate ? false : true,
maxBitrate: bitrate
}, player);
}
});
}
function showSettingsMenu(player, btn) {
}
function getQualitySecondaryText(player) {
return playbackManager.getPlayerState(player).then(function(state) {
var isAutoEnabled = playbackManager.enableAutomaticBitrateDetection(player);
var currentMaxBitrate = playbackManager.getMaxStreamingBitrate(player);
var videoStream = playbackManager.currentMediaSource(player).MediaStreams.filter(function (stream) {
return stream.Type === "Video";
})[0];
var videoWidth = videoStream ? videoStream.Width : null;
var options = qualityoptions.getVideoQualityOptions({
currentMaxBitrate: playbackManager.getMaxStreamingBitrate(player),
isAutomaticBitrateEnabled: playbackManager.enableAutomaticBitrateDetection(player),
videoWidth: videoWidth,
enableAuto: true
});
var menuItems = options.map(function (o) {
var opt = {
name: o.name,
id: o.bitrate,
secondaryText: o.secondaryText
};
if (o.selected) {
opt.selected = true;
}
return opt;
});
var selectedOption = options.filter(function (o) {
return o.selected;
});
if (!selectedOption.length) {
return null;
}
selectedOption = selectedOption[0];
var text = selectedOption.name;
if (selectedOption.autoText) {
if (state.PlayState && state.PlayState.PlayMethod !== 'Transcode') {
text += ' - Direct';
} else {
text += ' ' + selectedOption.autoText;
}
}
return text;
});
}
function show(options) {
var player = options.player;
return getQualitySecondaryText(player).then(function (secondaryQualityText) {
var mediaType = options.mediaType;
//return showQualityMenu(player, options.positionTo);
var menuItems = [];
menuItems.push({
name: globalize.translate('sharedcomponents#Quality'),
id: 'quality',
secondaryText: secondaryQualityText
});
//menuItems.push({
// name: globalize.translate('sharedcomponents#Settings'),
// id: 'settings'
//});
return actionsheet.show({
items: menuItems,
positionTo: options.positionTo
}).then(function (id) {
switch (id) {
case 'quality':
return showQualityMenu(player, options.positionTo);
case 'settings':
return showSettingsMenu(player, options.positionTo);
default:
break;
}
return Promise.reject();
});
});
}
return {
show: show
};
});

View File

@ -0,0 +1,48 @@
define(['events', 'playbackManager'], function (events, playbackManager) {
'use strict';
function transferPlayback(oldPlayer, newPlayer) {
playbackManager.getPlayerState(oldPlayer).then(function (state) {
var item = state.NowPlayingItem;
if (!item) {
return;
}
var playState = state.PlayState || {};
playbackManager.stop(oldPlayer);
var itemId = item.Id;
var resumePositionTicks = playState.PositionTicks || 0;
playbackManager.play({
ids: [itemId],
startPositionTicks: resumePositionTicks
}, newPlayer);
});
}
events.on(playbackManager, 'playerchange', function (e, newPlayer, newTarget, oldPlayer) {
if (!oldPlayer || !newPlayer) {
return;
}
if (!oldPlayer.isLocalPlayer) {
console.log('Skipping remote control autoplay because oldPlayer is not a local player');
return;
}
if (newPlayer.isLocalPlayer) {
console.log('Skipping remote control autoplay because newPlayer is a local player');
return;
}
transferPlayback(oldPlayer, newPlayer);
});
});

View File

@ -0,0 +1,152 @@
define(['events'], function (events) {
'use strict';
function pluginManager() {
var self = this;
var plugins = [];
// In lieu of automatic discovery, plugins will register dynamic objects
// Each object will have the following properties:
// name
// type (skin, screensaver, etc)
self.register = function (obj) {
plugins.push(obj);
events.trigger(self, 'registered', [obj]);
};
self.ofType = function (type) {
return plugins.filter(function (o) {
return o.type === type;
});
};
self.plugins = function () {
return plugins;
};
self.mapRoute = function (plugin, route) {
if (typeof plugin === 'string') {
plugin = plugins.filter(function (p) {
return (p.id || p.packageName) === plugin;
})[0];
}
route = route.path || route;
if (route.toLowerCase().indexOf('http') === 0) {
return route;
}
return '/plugins/' + plugin.id + '/' + route;
};
// TODO: replace with each plugin version
var cacheParam = new Date().getTime();
self.mapPath = function (plugin, path, addCacheParam) {
if (typeof plugin === 'string') {
plugin = plugins.filter(function (p) {
return (p.id || p.packageName) === plugin;
})[0];
}
var url = plugin.baseUrl + '/' + path;
if (addCacheParam) {
url += url.indexOf('?') === -1 ? '?' : '&';
url += 'v=' + cacheParam;
}
return url;
};
function loadStrings(plugin, globalize) {
var strings = plugin.getTranslations ? plugin.getTranslations() : [];
return globalize.loadStrings({
name: plugin.id || plugin.packageName,
strings: strings
});
}
function definePluginRoute(route, plugin) {
route.contentPath = self.mapPath(plugin, route.path);
route.path = self.mapRoute(plugin, route);
Emby.App.defineRoute(route, plugin.id);
}
self.loadPlugin = function (url) {
console.log('Loading plugin: ' + url);
return new Promise(function (resolve, reject) {
require([url, 'globalize', 'embyRouter'], function (pluginFactory, globalize, embyRouter) {
var plugin = new pluginFactory();
// See if it's already installed
var existing = plugins.filter(function (p) {
return p.id === plugin.id;
})[0];
if (existing) {
resolve(url);
return;
}
plugin.installUrl = url;
var urlLower = url.toLowerCase();
if (urlLower.indexOf('http:') === -1 && urlLower.indexOf('https:') === -1 && urlLower.indexOf('file:') === -1) {
if (url.indexOf(embyRouter.baseUrl()) !== 0) {
url = embyRouter.baseUrl() + '/' + url;
}
}
var separatorIndex = Math.max(url.lastIndexOf('/'), url.lastIndexOf('\\'));
plugin.baseUrl = url.substring(0, separatorIndex);
var paths = {};
paths[plugin.id] = plugin.baseUrl;
requirejs.config({
waitSeconds: 0,
paths: paths
});
self.register(plugin);
if (plugin.getRoutes) {
plugin.getRoutes().forEach(function (route) {
definePluginRoute(route, plugin);
});
}
if (plugin.type === 'skin') {
// translations won't be loaded for skins until needed
resolve(plugin);
} else {
loadStrings(plugin, globalize).then(function () {
resolve(plugin);
}, reject);
}
});
});
};
}
var instance = new pluginManager();
window.Emby = window.Emby || {};
window.Emby.PluginManager = instance;
return instance;
});

View File

@ -0,0 +1,44 @@
.recordingButton {
margin-left: 0;
min-width: 10em;
}
.recordingIcon {
font-size: 1.3em !important;
}
.recordingIcon-active {
color: #cc3333;
}
.manageButtonText {
text-transform: none;
}
.recordSeriesContainer {
margin-bottom: .8em;
}
.recordingFields-buttons {
display: flex;
align-items: center;
}
@media all and (max-width: 440px) {
.manageButtonText {
display: none !important;
}
.recordingButton {
width: auto;
margin-right: 1.5em !important;
}
}
@media all and (min-width: 440px) {
.manageButtonIcon {
font-size: 90% !important;
}
}

View File

@ -0,0 +1,20 @@
define([], function () {
'use strict';
function ResourceLockInstance() {
}
ResourceLockInstance.prototype.acquire = function () {
this._isHeld = true;
};
ResourceLockInstance.prototype.isHeld = function () {
return this._isHeld === true;
};
ResourceLockInstance.prototype.release = function () {
this._isHeld = false;
};
return ResourceLockInstance;
});

View File

@ -0,0 +1,37 @@
define([], function () {
'use strict';
function getRequirePromise(deps) {
return new Promise(function (resolve, reject) {
require(deps, resolve);
});
}
function requestResourceLock(resource) {
return getRequirePromise([resource]).then(function (factory) {
return new factory();
});
}
function request(type) {
switch (type) {
case 'wake':
return requestResourceLock('wakeLock');
case 'screen':
return requestResourceLock('screenLock');
case 'network':
return requestResourceLock('networkLock');
default:
return Promise.reject();
}
return Promise.resolve(new ResourceLockInstance(type));
}
return {
request: request
};
});

View File

@ -0,0 +1,100 @@
// From https://github.com/parshap/node-sanitize-filename
define([], function () {
'use strict';
var illegalRe = /[\/\?<>\\:\*\|":]/g;
var controlRe = /[\x00-\x1f\x80-\x9f]/g;
var reservedRe = /^\.+$/;
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
var windowsTrailingRe = /[\. ]+$/;
function isHighSurrogate(codePoint) {
return codePoint >= 0xd800 && codePoint <= 0xdbff;
}
function isLowSurrogate(codePoint) {
return codePoint >= 0xdc00 && codePoint <= 0xdfff;
}
function getByteLength(string) {
if (typeof string !== "string") {
throw new Error("Input must be string");
}
var charLength = string.length;
var byteLength = 0;
var codePoint = null;
var prevCodePoint = null;
for (var i = 0; i < charLength; i++) {
codePoint = string.charCodeAt(i);
// handle 4-byte non-BMP chars
// low surrogate
if (isLowSurrogate(codePoint)) {
// when parsing previous hi-surrogate, 3 is added to byteLength
if (prevCodePoint != null && isHighSurrogate(prevCodePoint)) {
byteLength += 1;
}
else {
byteLength += 3;
}
}
else if (codePoint <= 0x7f) {
byteLength += 1;
}
else if (codePoint >= 0x80 && codePoint <= 0x7ff) {
byteLength += 2;
}
else if (codePoint >= 0x800 && codePoint <= 0xffff) {
byteLength += 3;
}
prevCodePoint = codePoint;
}
return byteLength;
}
function truncate(string, byteLength) {
if (typeof string !== "string") {
throw new Error("Input must be string");
}
var charLength = string.length;
var curByteLength = 0;
var codePoint;
var segment;
for (var i = 0; i < charLength; i += 1) {
codePoint = string.charCodeAt(i);
segment = string[i];
if (isHighSurrogate(codePoint) && isLowSurrogate(string.charCodeAt(i + 1))) {
i += 1;
segment += string[i];
}
curByteLength += getByteLength(segment);
if (curByteLength === byteLength) {
return string.slice(0, i + 1);
}
else if (curByteLength > byteLength) {
return string.slice(0, i - segment.length + 1);
}
}
return string;
}
return {
sanitize: function (input, replacement) {
var sanitized = input
.replace(illegalRe, replacement)
.replace(controlRe, replacement)
.replace(reservedRe, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement);
return truncate(sanitized, 255);
}
};
});

View File

@ -0,0 +1,548 @@
define(['playbackManager', 'events', 'serverNotifications', 'connectionManager'], function (playbackManager, events, serverNotifications, connectionManager) {
'use strict';
function getActivePlayerId() {
var info = playbackManager.getPlayerInfo();
return info ? info.id : null;
}
function sendPlayCommand(apiClient, options, playType) {
var sessionId = getActivePlayerId();
var ids = options.ids || options.items.map(function (i) {
return i.Id;
});
var remoteOptions = {
ItemIds: ids.join(','),
PlayCommand: playType
};
if (options.startPositionTicks) {
remoteOptions.startPositionTicks = options.startPositionTicks;
}
return apiClient.sendPlayCommand(sessionId, remoteOptions);
}
function sendPlayStateCommand(apiClient, command, options) {
var sessionId = getActivePlayerId();
apiClient.sendPlayStateCommand(sessionId, command, options);
}
return function () {
var self = this;
self.name = 'Remote Control';
self.type = 'mediaplayer';
self.isLocalPlayer = false;
self.id = 'remoteplayer';
var currentServerId;
function getCurrentApiClient() {
if (currentServerId) {
return connectionManager.getApiClient(currentServerId);
}
return connectionManager.currentApiClient();
}
function sendCommandByName(name, options) {
var command = {
Name: name
};
if (options) {
command.Arguments = options;
}
self.sendCommand(command);
}
self.sendCommand = function (command) {
var sessionId = getActivePlayerId();
var apiClient = getCurrentApiClient();
apiClient.sendCommand(sessionId, command);
};
self.play = function (options) {
var playOptions = {};
playOptions.ids = options.ids || options.items.map(function (i) {
return i.Id;
});
if (options.startPositionTicks) {
playOptions.startPositionTicks = options.startPositionTicks;
}
return sendPlayCommand(getCurrentApiClient(), playOptions, 'PlayNow');
};
self.shuffle = function (item) {
sendPlayCommand(getCurrentApiClient(), { ids: [item.Id] }, 'PlayShuffle');
};
self.instantMix = function (item) {
sendPlayCommand(getCurrentApiClient(), { ids: [item.Id] }, 'PlayInstantMix');
};
self.queue = function (options) {
sendPlayCommand(getCurrentApiClient(), options, 'PlayNext');
};
self.queueNext = function (options) {
sendPlayCommand(getCurrentApiClient(), options, 'PlayLast');
};
self.canPlayMediaType = function (mediaType) {
mediaType = (mediaType || '').toLowerCase();
return mediaType === 'audio' || mediaType === 'video';
};
self.canQueueMediaType = function (mediaType) {
return self.canPlayMediaType(mediaType);
};
self.stop = function () {
sendPlayStateCommand(getCurrentApiClient(), 'stop');
};
self.nextTrack = function () {
sendPlayStateCommand(getCurrentApiClient(), 'nextTrack');
};
self.previousTrack = function () {
sendPlayStateCommand(getCurrentApiClient(), 'previousTrack');
};
self.seek = function (positionTicks) {
sendPlayStateCommand(getCurrentApiClient(), 'seek',
{
SeekPositionTicks: positionTicks
});
};
self.currentTime = function (val) {
if (val != null) {
return self.seek(val);
}
var state = self.lastPlayerData || {};
state = state.PlayState || {};
return state.PositionTicks;
};
self.duration = function () {
var state = self.lastPlayerData || {};
state = state.NowPlayingItem || {};
return state.RunTimeTicks;
};
self.paused = function () {
var state = self.lastPlayerData || {};
state = state.PlayState || {};
return state.IsPaused;
};
self.getVolume = function () {
var state = self.lastPlayerData || {};
state = state.PlayState || {};
return state.VolumeLevel;
};
self.pause = function () {
sendPlayStateCommand(getCurrentApiClient(), 'Pause');
};
self.unpause = function () {
sendPlayStateCommand(getCurrentApiClient(), 'Unpause');
};
self.setMute = function (isMuted) {
if (isMuted) {
sendCommandByName('Mute');
} else {
sendCommandByName('Unmute');
}
};
self.toggleMute = function () {
sendCommandByName('ToggleMute');
};
self.setVolume = function (vol) {
sendCommandByName('SetVolume', {
Volume: vol
});
};
self.volumeUp = function () {
sendCommandByName('VolumeUp');
};
self.volumeDown = function () {
sendCommandByName('VolumeDown');
};
self.toggleFullscreen = function () {
sendCommandByName('ToggleFullscreen');
};
self.audioTracks = function () {
var state = self.lastPlayerData || {};
state = state.NowPlayingItem || {};
var streams = state.MediaStreams || [];
return streams.filter(function (s) {
return s.Type === 'Audio';
});
};
self.getAudioStreamIndex = function () {
var state = self.lastPlayerData || {};
state = state.PlayState || {};
return state.AudioStreamIndex;
};
self.setAudioStreamIndex = function (index) {
sendCommandByName('SetAudioStreamIndex', {
Index: index
});
};
self.subtitleTracks = function () {
var state = self.lastPlayerData || {};
state = state.NowPlayingItem || {};
var streams = state.MediaStreams || [];
return streams.filter(function (s) {
return s.Type === 'Subtitle';
});
};
self.getSubtitleStreamIndex = function () {
var state = self.lastPlayerData || {};
state = state.PlayState || {};
return state.SubtitleStreamIndex;
};
self.setSubtitleStreamIndex = function (index) {
sendCommandByName('SetSubtitleStreamIndex', {
Index: index
});
};
self.getMaxStreamingBitrate = function () {
};
self.setMaxStreamingBitrate = function (options) {
};
self.isFullscreen = function () {
};
self.toggleFullscreen = function () {
};
self.getRepeatMode = function () {
};
self.setRepeatMode = function (mode) {
sendCommandByName('SetRepeatMode', {
RepeatMode: mode
});
};
self.displayContent = function (options) {
sendCommandByName('DisplayContent', options);
};
self.isPlaying = function () {
var state = self.lastPlayerData || {};
return state.NowPlayingItem != null;
};
self.isPlayingVideo = function () {
var state = self.lastPlayerData || {};
state = state.NowPlayingItem || {};
return state.MediaType === 'Video';
};
self.isPlayingAudio = function () {
var state = self.lastPlayerData || {};
state = state.NowPlayingItem || {};
return state.MediaType === 'Audio';
};
self.getPlaylist = function () {
return Promise.resolve([]);
};
self.getCurrentPlaylistItemId = function () {
};
self.setCurrentPlaylistItem = function (playlistItemId) {
return Promise.resolve();
};
self.removeFromPlaylist = function (playlistItemIds) {
return Promise.resolve();
};
self.getPlayerState = function () {
var apiClient = getCurrentApiClient();
if (apiClient) {
return apiClient.getSessions().then(function (sessions) {
var currentTargetId = getActivePlayerId();
// Update existing data
//updateSessionInfo(popup, msg.Data);
var session = sessions.filter(function (s) {
return s.Id === currentTargetId;
})[0];
if (session) {
session = getPlayerState(session);
}
return session;
});
} else {
return Promise.resolve({});
}
};
var pollInterval;
function onPollIntervalFired() {
var apiClient = getCurrentApiClient();
if (!apiClient.isWebSocketOpen()) {
if (apiClient) {
apiClient.getSessions().then(function (sessions) {
processUpdatedSessions(sessions, apiClient);
});
}
}
}
self.subscribeToPlayerUpdates = function () {
self.isUpdating = true;
var apiClient = getCurrentApiClient();
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SessionsStart", "100,800");
}
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
pollInterval = setInterval(onPollIntervalFired, 5000);
};
function unsubscribeFromPlayerUpdates() {
self.isUpdating = true;
var apiClient = getCurrentApiClient();
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SessionsStop");
}
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
var playerListenerCount = 0;
self.beginPlayerUpdates = function () {
if (playerListenerCount <= 0) {
playerListenerCount = 0;
self.subscribeToPlayerUpdates();
}
playerListenerCount++;
};
self.endPlayerUpdates = function () {
playerListenerCount--;
if (playerListenerCount <= 0) {
unsubscribeFromPlayerUpdates();
playerListenerCount = 0;
}
};
self.getTargets = function () {
var apiClient = getCurrentApiClient();
var sessionQuery = {
ControllableByUserId: apiClient.getCurrentUserId()
};
if (apiClient) {
return apiClient.getSessions(sessionQuery).then(function (sessions) {
return sessions.filter(function (s) {
return s.DeviceId !== apiClient.deviceId();
}).map(function (s) {
return {
name: s.DeviceName,
deviceName: s.DeviceName,
id: s.Id,
playerName: self.name,
appName: s.Client,
playableMediaTypes: s.PlayableMediaTypes,
isLocalPlayer: false,
supportedCommands: s.SupportedCommands
};
});
});
} else {
return Promise.resolve([]);
}
};
self.tryPair = function (target) {
return Promise.resolve();
};
function getPlayerState(session) {
return session;
}
function normalizeImages(state) {
if (state && state.NowPlayingItem) {
var item = state.NowPlayingItem;
if (!item.ImageTags || !item.ImageTags.Primary) {
if (item.PrimaryImageTag) {
item.ImageTags = item.ImageTags || {};
item.ImageTags.Primary = item.PrimaryImageTag;
}
}
if (item.BackdropImageTag && item.BackdropItemId === item.Id) {
item.BackdropImageTags = [item.BackdropImageTag];
}
if (item.BackdropImageTag && item.BackdropItemId !== item.Id) {
item.ParentBackdropImageTags = [item.BackdropImageTag];
item.ParentBackdropItemId = item.BackdropItemId;
}
}
}
function firePlaybackEvent(name, session) {
var state = getPlayerState(session);
normalizeImages(state);
self.lastPlayerData = state;
events.trigger(self, name, [state]);
}
function onWebSocketConnectionChange() {
// Reconnect
if (self.isUpdating) {
self.subscribeToPlayerUpdates();
}
}
function processUpdatedSessions(sessions, apiClient) {
var serverId = apiClient.serverId();
sessions.map(function (s) {
if (s.NowPlayingItem) {
s.NowPlayingItem.ServerId = serverId;
}
});
var currentTargetId = getActivePlayerId();
// Update existing data
//updateSessionInfo(popup, msg.Data);
var session = sessions.filter(function (s) {
return s.Id === currentTargetId;
})[0];
if (session) {
firePlaybackEvent('statechange', session);
firePlaybackEvent('timeupdate', session);
firePlaybackEvent('pause', session);
}
}
events.on(serverNotifications, 'Sessions', function (e, apiClient, data) {
processUpdatedSessions(data, apiClient);
});
events.on(serverNotifications, 'SessionEnded', function (e, apiClient, data) {
console.log("Server reports another session ended");
if (getActivePlayerId() === data.Id) {
playbackManager.setDefaultPlayerActive();
}
});
events.on(serverNotifications, 'PlaybackStart', function (e, apiClient, data) {
if (data.DeviceId !== apiClient.deviceId()) {
if (getActivePlayerId() === data.Id) {
firePlaybackEvent('playbackstart', data);
}
}
});
events.on(serverNotifications, 'PlaybackStopped', function (e, apiClient, data) {
if (data.DeviceId !== apiClient.deviceId()) {
if (getActivePlayerId() === data.Id) {
firePlaybackEvent('playbackstop', data);
}
}
});
};
});

View File

@ -0,0 +1,506 @@
define(['connectionManager', 'serverNotifications', 'events', 'datetime', 'dom', 'imageLoader', 'loading', 'globalize', 'apphost', 'layoutManager', 'scrollHelper', 'dialogHelper', 'shell', 'listViewStyle', 'paper-icon-button-light', 'emby-button', 'formDialogStyle'], function (connectionManager, serverNotifications, events, datetime, dom, imageLoader, loading, globalize, appHost, layoutManager, scrollHelper, dialogHelper, shell) {
'use strict';
function renderJob(context, job, dialogOptions) {
require(['syncDialog'], function (syncDialog) {
syncDialog.renderForm({
elem: context.querySelector('.syncJobFormContent'),
dialogOptions: dialogOptions,
dialogOptionsFn: getTargetDialogOptionsFn(dialogOptions),
showName: true,
readOnlySyncTarget: true
}).then(function () {
fillJobValues(context, job, dialogOptions);
});
});
}
function getTargetDialogOptionsFn(dialogOptions) {
return function (targetId) {
return Promise.resolve(dialogOptions);
};
}
function getJobItemHtml(jobItem, apiClient, index) {
var html = '';
html += '<div class="listItem" data-itemid="' + jobItem.Id + '" data-status="' + jobItem.Status + '" data-remove="' + jobItem.IsMarkedForRemoval + '">';
var hasActions = ['Queued', 'Cancelled', 'Failed', 'ReadyToTransfer', 'Transferring', 'Converting', 'Synced'].indexOf(jobItem.Status) !== -1;
var imgUrl;
if (jobItem.PrimaryImageItemId) {
imgUrl = apiClient.getImageUrl(jobItem.PrimaryImageItemId, {
type: "Primary",
width: 80,
tag: jobItem.PrimaryImageTag,
minScale: 1.5
});
}
if (imgUrl) {
html += '<button type="button" is="emby-button" class="blue mini fab autoSize" icon="sync" style="background-image:url(\'' + imgUrl + '\');background-repeat:no-repeat;background-position:center center;background-size: cover;"><i style="visibility:hidden;" class="md-icon">sync</i></button>';
}
else {
html += '<button type="button" is="emby-button" class="blue mini fab autoSize" icon="sync"><i class="md-icon">sync</i></button>';
}
html += '<div class="listItemBody three-line">';
html += '<h3 class="listItemBodyText">';
html += jobItem.ItemName;
html += '</h3>';
if (jobItem.Status === 'Failed') {
html += '<div class="secondary listItemBodyText" style="color:red;">';
} else {
html += '<div class="secondary listItemBodyText">';
}
html += globalize.translate('sharedcomponents#SyncJobItemStatus' + jobItem.Status);
if (jobItem.Status === 'Synced' && jobItem.IsMarkedForRemoval) {
html += '<br/>';
html += globalize.translate('sharedcomponents#RemovingFromDevice');
}
html += '</div>';
html += '<div class="secondary listItemBodyText" style="padding-top:5px;">';
html += '<div style="background:#e0e0e0;height:4px;"><div style="background:#52B54B;width:' + (jobItem.Progress || 0) + '%;height:100%;"></div></div>';
html += '</div>';
html += '</div>';
var moreIcon = appHost.moreIcon === 'dots-horiz' ? '&#xE5D3;' : '&#xE5D4;';
if (hasActions) {
html += '<button type="button" is="paper-icon-button-light" class="btnJobItemMenu autoSize"><i class="md-icon">' + moreIcon + '</i></button>';
} else {
html += '<button type="button" is="paper-icon-button-light" class="btnJobItemMenu autoSize" disabled><i class="md-icon">' + moreIcon + '</i></button>';
}
html += '</div>';
return html;
}
function renderJobItems(context, items, apiClient) {
var html = '';
html += '<h1>' + globalize.translate('sharedcomponents#Items') + '</h1>';
html += '<div class="paperList">';
var index = 0;
html += items.map(function (i) {
return getJobItemHtml(i, apiClient, index++);
}).join('');
html += '</div>';
var elem = context.querySelector('.jobItems');
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
}
function parentWithClass(elem, className) {
while (!elem.classList || !elem.classList.contains(className)) {
elem = elem.parentNode;
if (!elem) {
return null;
}
}
return elem;
}
function showJobItemMenu(elem, jobId, apiClient) {
var context = parentWithClass(elem, 'page');
var listItem = parentWithClass(elem, 'listItem');
var jobItemId = listItem.getAttribute('data-itemid');
var status = listItem.getAttribute('data-status');
var remove = listItem.getAttribute('data-remove').toLowerCase() === 'true';
var menuItems = [];
if (status === 'Failed' || status === 'Cancelled') {
menuItems.push({
name: globalize.translate('sharedcomponents#Retry'),
id: 'retry'
});
}
else if (status === 'Queued' || status === 'Transferring' || status === 'Converting' || status === 'ReadyToTransfer') {
menuItems.push({
name: globalize.translate('sharedcomponents#CancelDownload'),
id: 'cancel'
});
}
else if (status === 'Synced' && remove) {
menuItems.push({
name: globalize.translate('sharedcomponents#KeepOnDevice'),
id: 'unmarkforremoval'
});
}
else if (status === 'Synced') {
menuItems.push({
name: globalize.translate('sharedcomponents#RemoveFromDevice'),
id: 'markforremoval'
});
}
require(['actionsheet'], function (actionsheet) {
actionsheet.show({
items: menuItems,
positionTo: elem,
callback: function (id) {
switch (id) {
case 'cancel':
cancelJobItem(context, jobId, jobItemId, apiClient);
break;
case 'retry':
retryJobItem(context, jobId, jobItemId, apiClient);
break;
case 'markforremoval':
markForRemoval(context, jobId, jobItemId, apiClient);
break;
case 'unmarkforremoval':
unMarkForRemoval(context, jobId, jobItemId, apiClient);
break;
default:
break;
}
}
});
});
}
function cancelJobItem(context, jobId, jobItemId, apiClient) {
// Need a timeout because jquery mobile will not show a popup while another is in the act of closing
loading.show();
apiClient.ajax({
type: "DELETE",
url: apiClient.getUrl('Sync/JobItems/' + jobItemId)
}).then(function () {
loadJob(context, jobId, apiClient);
});
}
function markForRemoval(context, jobId, jobItemId, apiClient) {
apiClient.ajax({
type: "POST",
url: apiClient.getUrl('Sync/JobItems/' + jobItemId + '/MarkForRemoval')
}).then(function () {
loadJob(context, jobId, apiClient);
});
}
function unMarkForRemoval(context, jobId, jobItemId, apiClient) {
apiClient.ajax({
type: "POST",
url: apiClient.getUrl('Sync/JobItems/' + jobItemId + '/UnmarkForRemoval')
}).then(function () {
loadJob(context, jobId, apiClient);
});
}
function retryJobItem(context, jobId, jobItemId, apiClient) {
apiClient.ajax({
type: "POST",
url: apiClient.getUrl('Sync/JobItems/' + jobItemId + '/Enable')
}).then(function () {
loadJob(context, jobId, apiClient);
});
}
function fillJobValues(context, job, editOptions) {
var txtSyncJobName = context.querySelector('#txtSyncJobName');
if (txtSyncJobName) {
txtSyncJobName.value = job.Name;
}
var selectProfile = context.querySelector('#selectProfile');
if (selectProfile) {
selectProfile.value = job.Profile || '';
}
var selectQuality = context.querySelector('#selectQuality');
if (selectQuality) {
selectQuality.value = job.Quality || '';
}
var chkUnwatchedOnly = context.querySelector('#chkUnwatchedOnly');
if (chkUnwatchedOnly) {
chkUnwatchedOnly.checked = job.UnwatchedOnly;
}
var chkSyncNewContent = context.querySelector('#chkSyncNewContent');
if (chkSyncNewContent) {
chkSyncNewContent.checked = job.SyncNewContent;
}
var txtItemLimit = context.querySelector('#txtItemLimit');
if (txtItemLimit) {
txtItemLimit.value = job.ItemLimit;
}
var txtBitrate = context.querySelector('#txtBitrate');
if (job.Bitrate) {
txtBitrate.value = job.Bitrate / 1000000;
} else {
txtBitrate.value = '';
}
var target = editOptions.Targets.filter(function (t) {
return t.Id === job.TargetId;
})[0];
var targetName = target ? target.Name : '';
var selectSyncTarget = context.querySelector('#selectSyncTarget');
if (selectSyncTarget) {
selectSyncTarget.value = targetName;
}
}
var _jobOptions;
function loadJob(context, id, apiClient) {
loading.show();
apiClient.getJSON(apiClient.getUrl('Sync/Jobs/' + id)).then(function (job) {
apiClient.getJSON(apiClient.getUrl('Sync/Options', {
UserId: job.UserId,
ItemIds: (job.RequestedItemIds && job.RequestedItemIds.length ? job.RequestedItemIds.join('') : null),
ParentId: job.ParentId,
Category: job.Category,
TargetId: job.TargetId
})).then(function (options) {
_jobOptions = options;
renderJob(context, job, options);
loading.hide();
});
});
apiClient.getJSON(apiClient.getUrl('Sync/JobItems', {
JobId: id,
AddMetadata: true
})).then(function (result) {
renderJobItems(context, result.Items, apiClient);
loading.hide();
});
}
function loadJobInfo(context, job, jobItems, apiClient) {
//renderJob(page, job, _jobOptions);
renderJobItems(context, jobItems, apiClient);
loading.hide();
}
function saveJob(context, id, apiClient) {
loading.show();
apiClient.getJSON(apiClient.getUrl('Sync/Jobs/' + id)).then(function (job) {
require(['syncDialog'], function (syncDialog) {
syncDialog.setJobValues(job, context);
apiClient.ajax({
url: apiClient.getUrl('Sync/Jobs/' + id),
type: 'POST',
data: JSON.stringify(job),
contentType: "application/json"
}).then(function () {
loading.hide();
dialogHelper.close(context);
});
});
});
}
function onHelpLinkClick(e) {
shell.openUrl(this.href);
e.preventDefault();
return false;
}
function startListening(apiClient, jobId) {
var startParams = "0,1500";
startParams += "," + jobId;
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SyncJobStart", startParams);
}
}
function stopListening(apiClient) {
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SyncJobStop", "");
}
}
function bindEvents(context, jobId, apiClient) {
context.querySelector('.jobItems').addEventListener('click', function (e) {
var btnJobItemMenu = dom.parentWithClass(e.target, 'btnJobItemMenu');
if (btnJobItemMenu) {
showJobItemMenu(btnJobItemMenu, jobId, apiClient);
}
});
}
function showEditor(options) {
var apiClient = connectionManager.getApiClient(options.serverId);
var id = options.jobId;
var dlgElementOptions = {
removeOnClose: true,
scrollY: false,
autoFocus: false
};
if (layoutManager.tv) {
dlgElementOptions.size = 'fullscreen';
} else {
dlgElementOptions.size = 'medium';
}
var dlg = dialogHelper.createDialog(dlgElementOptions);
dlg.classList.add('formDialog');
var html = '';
html += '<div class="formDialogHeader">';
html += '<button is="paper-icon-button-light" class="btnCancel autoSize" tabindex="-1"><i class="md-icon">&#xE5C4;</i></button>';
html += '<h3 class="formDialogHeaderTitle">';
html += globalize.translate('sharedcomponents#Sync');
html += '</h3>';
html += '<a href="https://github.com/MediaBrowser/Wiki/wiki/Sync" target="_blank" class="clearLink lnkHelp" style="margin-top:0;display:inline-block;vertical-align:middle;margin-left:auto;"><button is="emby-button" type="button" class="button-accent-flat button-flat"><i class="md-icon">info</i><span>' + globalize.translate('sharedcomponents#Help') + '</span></button></a>';
html += '</div>';
html += '<div class="formDialogContent smoothScrollY" style="padding-top:2em;">';
html += '<div class="dialogContentInner dialog-content-centered">';
html += '<form class="syncJobForm" style="margin: auto;">';
html += '<div class="syncJobFormContent"></div>';
html += '<div class="jobItems"></div>';
html += '<div class="formDialogFooter">';
html += '<button is="emby-button" type="submit" class="raised button-submit block formDialogFooterItem"><span>' + globalize.translate('sharedcomponents#Save') + '</span></button>';
html += '</div>';
html += '</form>';
html += '</div>';
html += '</div>';
dlg.innerHTML = html;
dlg.querySelector('.lnkHelp').addEventListener('click', onHelpLinkClick);
var submitted = false;
dlg.querySelector('form').addEventListener('submit', function (e) {
saveJob(dlg, id, apiClient);
e.preventDefault();
return false;
});
dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg);
});
if (layoutManager.tv) {
scrollHelper.centerFocus.on(dlg.querySelector('.formDialogContent'), false);
}
function onSyncJobMessage(e, apiClient, msg) {
loadJobInfo(dlg, msg.Job, msg.JobItems, apiClient);
}
loadJob(dlg, id, apiClient);
bindEvents(dlg, id, apiClient);
var promise = dialogHelper.open(dlg);
startListening(apiClient, id);
events.on(serverNotifications, "SyncJob", onSyncJobMessage);
return promise.then(function () {
stopListening(apiClient);
events.off(serverNotifications, "SyncJob", onSyncJobMessage);
if (layoutManager.tv) {
scrollHelper.centerFocus.off(dlg.querySelector('.formDialogContent'), false);
}
if (submitted) {
return Promise.resolve();
}
return Promise.reject();
});
}
return {
show: showEditor
};
});

View File

@ -0,0 +1,420 @@
define(['serverNotifications', 'events', 'loading', 'connectionManager', 'imageLoader', 'dom', 'globalize', 'registrationServices', 'layoutManager', 'listViewStyle'], function (serverNotifications, events, loading, connectionManager, imageLoader, dom, globalize, registrationServices, layoutManager) {
'use strict';
function onSyncJobsUpdated(e, apiClient, data) {
var listInstance = this;
renderList(listInstance, data, apiClient);
}
function refreshList(listInstance, jobs) {
for (var i = 0, length = jobs.length; i < length; i++) {
var job = jobs[i];
refreshJob(listInstance, job);
}
}
function cancelJob(listInstance, id) {
require(['confirm'], function (confirm) {
var msg = listInstance.options.isLocalSync ?
globalize.translate('sharedcomponents#ConfirmRemoveDownload') :
globalize.translate('sharedcomponents#CancelSyncJobConfirmation');
confirm({
text: msg,
primary: 'cancel'
}).then(function () {
loading.show();
var apiClient = getApiClient(listInstance);
apiClient.ajax({
url: apiClient.getUrl('Sync/Jobs/' + id),
type: 'DELETE'
}).then(function () {
fetchData(listInstance);
});
});
});
}
function refreshJob(listInstance, job) {
var listItem = listInstance.options.element.querySelector('.listItem[data-id=\'' + job.Id + '\']');
if (!listItem) {
return;
}
listItem.querySelector('.jobStatus').innerHTML = getProgressText(job);
}
function getProgressText(job) {
var status = job.Status;
if (status === 'Completed') {
status = 'Synced';
}
var html = globalize.translate('sharedcomponents#SyncJobItemStatus' + status);
if (job.Status === 'Transferring' || job.Status === 'Converting' || job.Status === 'Completed') {
html += ' ';
html += (job.Progress || 0) + '%';
}
return html;
}
function getSyncJobHtml(listInstance, job, apiClient) {
var html = '';
var tagName = layoutManager.tv ? 'button' : 'div';
var typeAttribute = tagName === 'button' ? ' type="button"' : '';
var listItemClass = 'listItem';
if (layoutManager.tv) {
listItemClass += ' listItem-button listItem-focusscale';
}
html += '<' + tagName + typeAttribute + ' class="' + listItemClass + '" data-id="' + job.Id + '" data-status="' + job.Status + '">';
var imgUrl;
if (job.PrimaryImageItemId) {
imgUrl = apiClient.getImageUrl(job.PrimaryImageItemId, {
type: "Primary",
width: 80,
tag: job.PrimaryImageTag,
minScale: 1.5
});
}
if (imgUrl) {
html += '<div class="listItemImage lazy" data-src="' + imgUrl + '" item-icon>';
html += '</div>';
}
else {
html += '<i class="md-icon listItemIcon">file_download</i>';
}
var textLines = [];
if (job.ParentName) {
textLines.push(job.ParentName);
}
textLines.push(job.Name);
if (job.ItemCount === 1) {
textLines.push(globalize.translate('sharedcomponents#ValueOneItem'));
} else {
textLines.push(globalize.translate('sharedcomponents#ItemCount', job.ItemCount));
}
html += '<div class="listItemBody three-line">';
for (var i = 0, length = textLines.length; i < length; i++) {
if (i === 0) {
html += '<h3 class="listItemBodyText">';
html += textLines[i];
html += '</h3>';
} else {
html += '<div class="listItemBodyText secondary">';
html += textLines[i];
html += '</div>';
}
}
html += '<div class="secondary listItemBodyText jobStatus" style="color:green;">';
html += getProgressText(job);
html += '</div>';
html += '</div>';
if (!layoutManager.tv) {
html += '<button type="button" is="paper-icon-button-light" class="btnJobMenu listItemButton"><i class="md-icon">more_vert</i></button>';
}
html += '</' + tagName + '>';
return html;
}
function renderList(listInstance, jobs, apiClient) {
if ((new Date().getTime() - listInstance.lastDataLoad) < 60000) {
refreshList(listInstance, jobs);
return;
}
listInstance.lastDataLoad = new Date().getTime();
var html = '';
var lastTargetName = '';
var isLocalSync = listInstance.options.isLocalSync;
var showTargetName = !isLocalSync;
var hasOpenSection = false;
for (var i = 0, length = jobs.length; i < length; i++) {
var job = jobs[i];
if (showTargetName) {
var targetName = job.TargetName || 'Unknown';
if (targetName !== lastTargetName) {
if (lastTargetName) {
html += '</div>';
html += '<br/>';
hasOpenSection = false;
}
lastTargetName = targetName;
html += '<div class="detailSectionHeader">';
html += '<h1>' + targetName + '</h1>';
html += '</div>';
html += '<div class="itemsContainer vertical-list paperList">';
hasOpenSection = true;
}
}
html += getSyncJobHtml(listInstance, job, apiClient);
}
if (hasOpenSection) {
html += '</div>';
}
var elem = listInstance.options.element.querySelector('.syncJobListContent');
if (!html) {
if (isLocalSync) {
html = '<div style="padding:1em .25em;">' + globalize.translate('sharedcomponents#MessageNoDownloadsFound') + '</div>';
} else {
html = '<div style="padding:1em .25em;">' + globalize.translate('sharedcomponents#MessageNoSyncJobsFound') + '</div>';
}
}
elem.innerHTML = html;
imageLoader.lazyChildren(elem);
}
function fetchData(listInstance) {
listInstance.lastDataLoad = 0;
loading.show();
var options = {};
var apiClient = getApiClient(listInstance);
if (listInstance.options.userId) {
options.UserId = listInstance.options.userId;
}
if (listInstance.options.isLocalSync) {
options.TargetId = apiClient.deviceId();
} else {
options.ExcludeTargetIds = apiClient.deviceId();
}
return apiClient.getJSON(apiClient.getUrl('Sync/Jobs', options)).then(function (response) {
renderList(listInstance, response.Items, apiClient);
loading.hide();
});
}
function startListening(listInstance) {
var startParams = "0,1500";
var apiClient = getApiClient(listInstance);
if (listInstance.options.userId) {
startParams += "," + listInstance.options.userId;
}
if (listInstance.options.isLocalSync) {
startParams += "," + apiClient.deviceId();
}
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SyncJobsStart", startParams);
}
}
function stopListening(listInstance) {
var apiClient = getApiClient(listInstance);
if (apiClient.isWebSocketOpen()) {
apiClient.sendWebSocketMessage("SyncJobsStop", "");
}
}
function getApiClient(listInstance) {
return connectionManager.getApiClient(listInstance.options.serverId);
}
function showJobMenu(listInstance, elem) {
var item = dom.parentWithClass(elem, 'listItem');
var jobId = item.getAttribute('data-id');
var status = item.getAttribute('data-status');
var menuItems = [];
if (status === 'Cancelled') {
menuItems.push({
name: globalize.translate('sharedcomponents#Delete'),
id: 'delete'
});
} else {
var txt = listInstance.options.isLocalSync ?
globalize.translate('sharedcomponents#RemoveDownload') :
globalize.translate('sharedcomponents#ButtonCancelSyncJob');
menuItems.push({
name: txt,
id: 'cancel'
});
}
require(['actionsheet'], function (actionsheet) {
actionsheet.show({
items: menuItems,
positionTo: elem,
callback: function (id) {
switch (id) {
case 'delete':
cancelJob(listInstance, jobId);
break;
case 'cancel':
cancelJob(listInstance, jobId);
break;
default:
break;
}
}
});
});
}
function onElementClick(e) {
var listInstance = this;
var btnJobMenu = dom.parentWithClass(e.target, 'btnJobMenu');
if (btnJobMenu) {
showJobMenu(this, btnJobMenu);
return;
}
var listItem = dom.parentWithClass(e.target, 'listItem');
if (listItem) {
var jobId = listItem.getAttribute('data-id');
// edit job
require(['syncJobEditor'], function (syncJobEditor) {
syncJobEditor.show({
serverId: listInstance.options.serverId,
jobId: jobId
}).then(function () {
fetchData(listInstance);
});
});
}
}
function syncJobList(options) {
this.options = options;
var onSyncJobsUpdatedHandler = onSyncJobsUpdated.bind(this);
this.onSyncJobsUpdatedHandler = onSyncJobsUpdatedHandler;
events.on(serverNotifications, 'SyncJobs', onSyncJobsUpdatedHandler);
var onClickHandler = onElementClick.bind(this);
options.element.addEventListener('click', onClickHandler);
this.onClickHandler = onClickHandler;
options.element.innerHTML = '<div class="syncJobListContent"></div>';
fetchData(this);
startListening(this);
initSupporterInfo(options.element, getApiClient(this));
}
function showSupporterInfo(context) {
var html = '<button is="emby-button" class="raised button-accent block btnSyncSupporter" style="margin:1em 0;">';
html += '<div>';
html += globalize.translate('sharedcomponents#HeaderSyncRequiresSub');
html += '</div>';
html += '<div style="margin-top:.5em;">';
html += globalize.translate('sharedcomponents#LearnMore');
html += '</div>';
html += '</button';
context.insertAdjacentHTML('afterbegin', html);
context.querySelector('.btnSyncSupporter').addEventListener('click', function () {
registrationServices.validateFeature('sync');
});
}
function initSupporterInfo(context, apiClient) {
apiClient.getPluginSecurityInfo().then(function (regInfo) {
if (!regInfo.IsMBSupporter) {
showSupporterInfo(context, apiClient);
}
}, function () {
showSupporterInfo(context, apiClient);
});
}
syncJobList.prototype.destroy = function () {
stopListening(this);
var onSyncJobsUpdatedHandler = this.onSyncJobsUpdatedHandler;
this.onSyncJobsUpdatedHandler = null;
events.off(serverNotifications, 'SyncJobs', onSyncJobsUpdatedHandler);
var onClickHandler = this.onClickHandler;
this.onClickHandler = null;
this.options.element.removeEventListener('click', onClickHandler);
this.options = null;
};
return syncJobList;
});

View File

@ -0,0 +1,407 @@
define(['pluginManager', 'events', 'browser', 'embyRouter'], function (pluginManager, Events, browser, embyRouter) {
"use strict";
return function () {
var self = this;
self.name = 'Youtube Player';
self.type = 'mediaplayer';
self.id = 'youtubeplayer';
// Let any players created by plugins take priority
self.priority = 1;
var videoDialog;
var currentSrc;
var started = false;
var currentYoutubePlayer;
var timeUpdateInterval;
self.canPlayMediaType = function (mediaType) {
mediaType = (mediaType || '').toLowerCase();
return mediaType === 'audio' || mediaType === 'video';
};
self.canPlayItem = function (item) {
// Does not play server items
return false;
};
self.canPlayUrl = function (url) {
return url.toLowerCase().indexOf('youtube.com') !== -1;
};
self.getDeviceProfile = function () {
return Promise.resolve({});
};
self.currentSrc = function () {
return currentSrc;
};
self.play = function (options) {
started = false;
return createMediaElement(options).then(function (elem) {
return setCurrentSrc(elem, options);
});
};
function setCurrentSrc(elem, options) {
return new Promise(function (resolve, reject) {
require(['queryString'], function (queryString) {
currentSrc = options.url;
var params = queryString.parse(options.url.split('?')[1]);
// 3. This function creates an <iframe> (and YouTube player)
// after the API code downloads.
window.onYouTubeIframeAPIReady = function () {
currentYoutubePlayer = new YT.Player('player', {
height: videoDialog.offsetHeight,
width: videoDialog.offsetWidth,
videoId: params.v,
events: {
'onReady': onPlayerReady,
'onStateChange': function (event) {
if (event.data === YT.PlayerState.PLAYING) {
onPlaying(options, resolve);
} else if (event.data === YT.PlayerState.ENDED) {
onEnded();
} else if (event.data === YT.PlayerState.PAUSED) {
onPause();
}
}
},
playerVars: {
controls: 0,
enablejsapi: 1,
modestbranding: 1,
rel: 0,
showinfo: 0,
fs: 0,
playsinline: 1
}
});
window.removeEventListener('resize', onVideoResize);
window.addEventListener('resize', onVideoResize);
window.removeEventListener('orientationChange', onVideoResize);
window.addEventListener('orientationChange', onVideoResize);
};
if (!window.YT) {
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
} else {
window.onYouTubeIframeAPIReady();
}
});
});
}
function onVideoResize() {
var player = currentYoutubePlayer;
var dlg = videoDialog;
if (player && dlg) {
player.setSize(dlg.offsetWidth, dlg.offsetHeight);
}
}
// 4. The API will call this function when the video player is ready.
function onPlayerReady(event) {
event.target.playVideo();
}
self.setSubtitleStreamIndex = function (index) {
};
self.canSetAudioStreamIndex = function () {
return false;
};
self.setAudioStreamIndex = function (index) {
};
// Save this for when playback stops, because querying the time at that point might return 0
self.currentTime = function (val) {
if (currentYoutubePlayer) {
if (val != null) {
currentYoutubePlayer.seekTo(val / 1000, true);
return;
}
return currentYoutubePlayer.getCurrentTime() * 1000;
}
};
self.duration = function (val) {
if (currentYoutubePlayer) {
return currentYoutubePlayer.getDuration() * 1000;
}
return null;
};
self.stop = function (destroyPlayer, reportEnded) {
var src = currentSrc;
if (src) {
if (currentYoutubePlayer) {
currentYoutubePlayer.stopVideo();
}
onEndedInternal(reportEnded);
if (destroyPlayer) {
self.destroy();
}
}
return Promise.resolve();
};
self.destroy = function () {
embyRouter.setTransparency('none');
var dlg = videoDialog;
if (dlg) {
videoDialog = null;
dlg.parentNode.removeChild(dlg);
}
};
self.pause = function () {
if (currentYoutubePlayer) {
currentYoutubePlayer.pauseVideo();
// This needs a delay before the youtube player will report the correct player state
setTimeout(onPause, 200);
}
};
self.unpause = function () {
if (currentYoutubePlayer) {
currentYoutubePlayer.playVideo();
// This needs a delay before the youtube player will report the correct player state
setTimeout(onPlaying, 200);
}
};
self.paused = function () {
if (currentYoutubePlayer) {
console.log(currentYoutubePlayer.getPlayerState());
return currentYoutubePlayer.getPlayerState() === 2;
}
return false;
};
self.volume = function (val) {
if (val != null) {
return self.setVolume(val);
}
return self.getVolume();
};
self.setVolume = function (val) {
if (currentYoutubePlayer) {
if (val != null) {
currentYoutubePlayer.setVolume(val);
}
}
};
self.getVolume = function () {
if (currentYoutubePlayer) {
return currentYoutubePlayer.getVolume();
}
};
self.setMute = function (mute) {
if (mute) {
if (currentYoutubePlayer) {
currentYoutubePlayer.mute();
}
} else {
if (currentYoutubePlayer) {
currentYoutubePlayer.unMute();
}
}
};
self.isMuted = function () {
if (currentYoutubePlayer) {
currentYoutubePlayer.isMuted();
}
};
function onEnded() {
onEndedInternal(true);
}
function clearTimeUpdateInterval() {
if (timeUpdateInterval) {
clearInterval(timeUpdateInterval);
}
timeUpdateInterval = null;
}
function onEndedInternal(triggerEnded) {
clearTimeUpdateInterval();
window.removeEventListener('resize', onVideoResize);
window.removeEventListener('orientationChange', onVideoResize);
if (triggerEnded) {
var stopInfo = {
src: currentSrc
};
Events.trigger(self, 'stopped', [stopInfo]);
}
currentSrc = null;
if (currentYoutubePlayer) {
currentYoutubePlayer.destroy();
}
currentYoutubePlayer = null;
}
function onTimeUpdate(e) {
Events.trigger(self, 'timeupdate');
}
function onVolumeChange() {
Events.trigger(self, 'volumechange');
}
function onPlaying(playOptions, resolve) {
if (!started) {
started = true;
resolve();
clearTimeUpdateInterval();
timeUpdateInterval = setInterval(onTimeUpdate, 500);
if (playOptions.fullscreen) {
embyRouter.showVideoOsd().then(function () {
videoDialog.classList.remove('onTop');
});
} else {
embyRouter.setTransparency('backdrop');
videoDialog.classList.remove('onTop');
}
require(['loading'], function (loading) {
loading.hide();
});
}
Events.trigger(self, 'playing');
}
function onClick() {
Events.trigger(self, 'click');
}
function onDblClick() {
Events.trigger(self, 'dblclick');
}
function onPause() {
Events.trigger(self, 'pause');
}
function onError() {
var errorCode = this.error ? this.error.code : '';
console.log('Media element error code: ' + errorCode);
Events.trigger(self, 'error');
}
function zoomIn(elem, iterations) {
var keyframes = [
{ transform: 'scale3d(.2, .2, .2) ', opacity: '.6', offset: 0 },
{ transform: 'none', opacity: '1', offset: 1 }
];
var timing = { duration: 240, iterations: iterations };
return elem.animate(keyframes, timing);
}
function createMediaElement(options) {
return new Promise(function (resolve, reject) {
var dlg = document.querySelector('.youtubePlayerContainer');
if (!dlg) {
require(['loading', 'css!' + pluginManager.mapPath(self, 'style.css')], function (loading) {
loading.show();
var dlg = document.createElement('div');
dlg.classList.add('youtubePlayerContainer');
if (options.fullscreen) {
dlg.classList.add('onTop');
}
dlg.innerHTML = '<div id="player"></div>';
var videoElement = dlg.querySelector('#player');
document.body.insertBefore(dlg, document.body.firstChild);
videoDialog = dlg;
if (options.fullscreen && dlg.animate && !browser.slow) {
zoomIn(dlg, 1).onfinish = function () {
resolve(videoElement);
};
} else {
resolve(videoElement);
}
});
} else {
resolve(dlg.querySelector('#player'));
}
});
}
};
});

View File

@ -0,0 +1,21 @@
.youtubePlayerContainer {
background: #000 !important;
position: fixed !important;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
align-items: center;
}
.youtubePlayerContainer.onTop {
z-index: 1000;
}
.youtubePlayerContainer video {
margin: 0 !important;
padding: 0 !important;
width: 100%;
height: 100%;
}