From 1e5bd25291a1a4ecc88a04760ec749fecdceb564 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Mon, 23 Apr 2018 20:41:27 +0200 Subject: [PATCH 01/23] Small changes to the UI --- index.html | 5 +---- static/css/style.css | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 498060b..7fde1e6 100644 --- a/index.html +++ b/index.html @@ -100,16 +100,13 @@
-
Your PCs

Add Host

-
-
Your Games
-
+
diff --git a/static/css/style.css b/static/css/style.css index f2a1a82..d79cd89 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -72,6 +72,7 @@ main { .nav-menu-parent { position: relative; + margin: 0 4px; } .mdl-menu__outline { background-color: #333846; @@ -187,6 +188,7 @@ main { margin: 15px; cursor: pointer; transition: all .2s ease-in-out; + will-change: transform; } #host-grid .mdl-card:hover, #host-grid .mdl-card:focus, #host-grid .mdl-card:active, #game-grid .mdl-card:hover, #game-grid .mdl-card:focus, #game-grid .mdl-card:active { transform: scale(1.1); From 28f0200c7fbf60a0f819959da3ef49b1a708f881 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Mon, 23 Apr 2018 20:48:23 +0200 Subject: [PATCH 02/23] Lazy-load albums --- static/js/index.js | 63 ++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index cf988d0..e5ac380 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -475,49 +475,34 @@ function showApps(host) { const sortedAppList = sortTitles(appList, 'ASC'); sortedAppList.forEach(function (app) { + if ($('#game-' + app.id).length === 0) { + // double clicking the button will cause multiple box arts to appear. + // to mitigate this we ensure we don't add a duplicate. + // This isn't perfect: there's lots of RTTs before the logic prevents anything + var outerDiv = $("
", {class: 'game-container mdl-card mdl-shadow--4dp', id: 'game-'+app.id, role: 'link', tabindex: 0, title: app.title, 'aria-label': app.title }); + + $(outerDiv).append($("
", {class: "game-title", html: $("", {html: app.title} )})); + $("#game-grid").append(outerDiv); + + $('#game-'+app.id).on('click', function () { + startGame(host, app.id); + }); + $('#game-'+app.id).keypress(function(e){ + if(e.keyCode == 13) { + startGame(host, app.id); + } + }); + + // apply CSS stylization to indicate whether the app is active + stylizeBoxArt(host, app.id); + } host.getBoxArt(app.id).then(function (resolvedPromise) { // put the box art into the image holder - if ($('#game-' + app.id).length === 0) { - // double clicking the button will cause multiple box arts to appear. - // to mitigate this we ensure we don't add a duplicate. - // This isn't perfect: there's lots of RTTs before the logic prevents anything - var outerDiv = $("
", {class: 'game-container mdl-card mdl-shadow--4dp', id: 'game-'+app.id, backgroundImage: resolvedPromise, role: 'link', tabindex: 0, title: app.title, 'aria-label': app.title }); - $(outerDiv).append($("", {src: resolvedPromise, id: 'game-'+app.id, name: app.title })); - $(outerDiv).append($("
", {class: "game-title", html: $("", {html: app.title} )})); - $("#game-grid").append(outerDiv); - - $('#game-'+app.id).on('click', function () { - startGame(host, app.id); - }); - $('#game-'+app.id).keypress(function(e){ - if(e.keyCode == 13) { - startGame(host, app.id); - } - }); - - // apply CSS stylization to indicate whether the app is active - stylizeBoxArt(host, app.id); - } + $(outerDiv).append($("", {src: resolvedPromise, name: app.title })); }, function (failedPromise) { - console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); - - if ($('#game-' + app.id).length === 0) { - // double clicking the button will cause multiple box arts to appear. - // to mitigate this we ensure we don't add a duplicate. - // This isn't perfect: there's lots of RTTs before the logic prevents anything - var outerDiv = $("
", {class: 'game-container mdl-card mdl-shadow--4dp', id: 'game-'+app.id, backgroundImage: "static/res/no_app_image.png" }); - $(outerDiv).append($("", {src: "static/res/no_app_image.png", id: 'game-'+app.id, name: app.title })); - $(outerDiv).append($("
", {class: "game-title", html: $("", {html: app.title} )})); - $("#game-grid").append(outerDiv); - - $('#game-'+app.id).on('click', function () { - startGame(host, app.id); - }); - - // apply CSS stylization to indicate whether the app is active - stylizeBoxArt(host, app.id); - } + console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); + $(outerDiv).append($("", {src: "static/res/no_app_image.png", name: app.title })); }); }); }, function (failedAppList) { From 49bdfcc33d167903cf3e794331ec58bd726576f0 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Mon, 23 Apr 2018 21:10:34 +0200 Subject: [PATCH 03/23] Added placeholder while loading albums --- static/css/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/style.css b/static/css/style.css index d79cd89..da19daa 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -178,7 +178,7 @@ main { } #game-grid .mdl-card { position: relative; - background: transparent; + background: url(/icons/icon128.png) rgba(0, 0, 0, 0.5) center no-repeat; } #host-grid .mdl-card, #game-grid .mdl-card { text-align: center; From 28a76fa965c86dea2c69b1b9acb6526edb664c35 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 24 Apr 2018 08:40:39 +0200 Subject: [PATCH 04/23] Fixed indentation usign atom-beautify --- static/js/background.js | 56 +- static/js/common.js | 14 +- static/js/index.js | 1349 ++++++++++++++++++++------------------- static/js/messages.js | 93 +-- static/js/utils.js | 843 ++++++++++++------------ 5 files changed, 1205 insertions(+), 1150 deletions(-) diff --git a/static/js/background.js b/static/js/background.js index 14a3a06..664eb98 100644 --- a/static/js/background.js +++ b/static/js/background.js @@ -1,33 +1,35 @@ function createWindow(state) { - chrome.app.window.create('index.html', { - state: state, - bounds: { - width: 960, - height: 540 - } - }, function(window) { - // workaround: - // state = 'normal' in some cases not work (e.g. starting app from 'chrome://extensions' always open window in fullscreen mode) - // it requires manually restoring window state to 'normal' - if (state == 'normal') { - setTimeout(function() { window.restore(); }, 1000); - } - }); + chrome.app.window.create('index.html', { + state: state, + bounds: { + width: 960, + height: 540 + } + }, function(window) { + // workaround: + // state = 'normal' in some cases not work (e.g. starting app from 'chrome://extensions' always open window in fullscreen mode) + // it requires manually restoring window state to 'normal' + if (state == 'normal') { + setTimeout(function() { + window.restore(); + }, 1000); + } + }); } chrome.app.runtime.onLaunched.addListener(function() { - console.log('Chrome app runtime launched.'); - var windowState = 'normal'; + console.log('Chrome app runtime launched.'); + var windowState = 'normal'; - if (chrome.storage) { - // load stored window state - chrome.storage.sync.get('windowState', function(item) { - windowState = (item && item.windowState) - ? item.windowState - : windowState; - createWindow(windowState); - }); - } else { - createWindow(windowState); - } + if (chrome.storage) { + // load stored window state + chrome.storage.sync.get('windowState', function(item) { + windowState = (item && item.windowState) ? + item.windowState : + windowState; + createWindow(windowState); + }); + } else { + createWindow(windowState); + } }); diff --git a/static/js/common.js b/static/js/common.js index 6b5a5c0..1fd86d0 100644 --- a/static/js/common.js +++ b/static/js/common.js @@ -35,7 +35,7 @@ var common = (function() { } else if (tool == 'pnacl') { mimetype = 'application/x-pnacl'; } - console.log('%c[updateStatus, common.js]', 'color: gray;', 'mimetype: ' + mimetype); + console.log('%c[updateStatus, common.js]', 'color: gray;', 'mimetype: ' + mimetype); return mimetype; } @@ -72,7 +72,7 @@ var common = (function() { */ function createNaClModule(name, tool, path, width, height, attrs) { console.log('%c[createNaClModule, common.js]', 'color: gray;', "name: " + name + ", tool: " + tool + ", path: " + path + ", width: " + width + - ", height: " + height + ", attrs: " + JSON.stringify(attrs)); + ", height: " + height + ", attrs: " + JSON.stringify(attrs)); var moduleEl = document.createElement('embed'); moduleEl.setAttribute('name', 'nacl_module'); moduleEl.setAttribute('id', 'nacl_module'); @@ -115,7 +115,7 @@ var common = (function() { moduleEl.readyState = 4; moduleEl.dispatchEvent(new CustomEvent('load')); moduleEl.dispatchEvent(new CustomEvent('loadend')); - }, 100); // 100 ms + }, 100); // 100 ms } } @@ -158,7 +158,7 @@ var common = (function() { * This event listener is registered in attachDefaultListeners above. */ function handleCrash(event) { - console.log('%c[handleCrash, common.js]', 'color: red;', event); + console.log('%c[handleCrash, common.js]', 'color: red;', event); if (common.naclModule.exitStatus == -1) { updateStatus('CRASHED'); } else { @@ -373,7 +373,7 @@ document.addEventListener('DOMContentLoaded', function() { for (var key_ix = 0; key_ix < pairs.length; key_ix++) { var keyValue = pairs[key_ix].split('='); searchVars[unescape(keyValue[0])] = - keyValue.length > 1 ? unescape(keyValue[1]) : ''; + keyValue.length > 1 ? unescape(keyValue[1]) : ''; } } @@ -393,7 +393,7 @@ document.addEventListener('DOMContentLoaded', function() { } var tc = toolchains.indexOf(searchVars.tc) !== -1 ? - searchVars.tc : toolchains[0]; + searchVars.tc : toolchains[0]; // If the config value is included in the search vars, use that. // Otherwise default to Release if it is valid, or the first value if @@ -411,7 +411,7 @@ document.addEventListener('DOMContentLoaded', function() { isRelease = path.toLowerCase().indexOf('release') != -1; loadFunction(body.dataset.name, tc, path, body.dataset.width, - body.dataset.height, attrs); + body.dataset.height, attrs); } } }); diff --git a/static/js/index.js b/static/js/index.js index e5ac380..2f80823 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,158 +1,160 @@ -var hosts = {}; // hosts is an associative array of NvHTTP objects, keyed by server UID -var activePolls = {}; // hosts currently being polled. An associated array of polling IDs, keyed by server UID +var hosts = {}; // hosts is an associative array of NvHTTP objects, keyed by server UID +var activePolls = {}; // hosts currently being polled. An associated array of polling IDs, keyed by server UID var pairingCert; var myUniqueid; -var api; // `api` should only be set if we're in a host-specific screen. on the initial screen it should always be null. +var api; // `api` should only be set if we're in a host-specific screen. on the initial screen it should always be null. var isInGame = false; // flag indicating whether the game stream started var windowState = 'normal'; // chrome's windowState, possible values: 'normal' or 'fullscreen' // Called by the common.js module. function attachListeners() { - changeUiModeForNaClLoad(); + changeUiModeForNaClLoad(); - $('.resolutionMenu li').on('click', saveResolution); - $('.framerateMenu li').on('click', saveFramerate); - $('#bitrateSlider').on('input', updateBitrateField); // input occurs every notch you slide - //$('#bitrateSlider').on('change', saveBitrate); //FIXME: it seems not working - $("#remoteAudioEnabledSwitch").on('click', saveRemoteAudio); - $('#optimizeGamesSwitch').on('click', saveOptimize); - $('#addHostCell').on('click', addHost); - $('#backIcon').on('click', showHostsAndSettingsMode); - $('#quitCurrentApp').on('click', stopGameWithConfirmation); - $(window).resize(fullscreenNaclModule); - chrome.app.window.current().onMaximized.addListener(fullscreenChromeWindow); + $('.resolutionMenu li').on('click', saveResolution); + $('.framerateMenu li').on('click', saveFramerate); + $('#bitrateSlider').on('input', updateBitrateField); // input occurs every notch you slide + //$('#bitrateSlider').on('change', saveBitrate); //FIXME: it seems not working + $("#remoteAudioEnabledSwitch").on('click', saveRemoteAudio); + $('#optimizeGamesSwitch').on('click', saveOptimize); + $('#addHostCell').on('click', addHost); + $('#backIcon').on('click', showHostsAndSettingsMode); + $('#quitCurrentApp').on('click', stopGameWithConfirmation); + $(window).resize(fullscreenNaclModule); + chrome.app.window.current().onMaximized.addListener(fullscreenChromeWindow); } function fullscreenChromeWindow() { - // when the user clicks the maximize button on the window, - // FIRST restore it to the previous size, then fullscreen it to the whole screen - // this prevents the previous window size from being 'maximized', - // and allows us to functionally retain two window sizes - // so that when the user hits `esc`, they go back to the "restored" size, - // instead of "maximized", which would immediately go to fullscreen - chrome.app.window.current().restore(); - chrome.app.window.current().fullscreen(); + // when the user clicks the maximize button on the window, + // FIRST restore it to the previous size, then fullscreen it to the whole screen + // this prevents the previous window size from being 'maximized', + // and allows us to functionally retain two window sizes + // so that when the user hits `esc`, they go back to the "restored" size, + // instead of "maximized", which would immediately go to fullscreen + chrome.app.window.current().restore(); + chrome.app.window.current().fullscreen(); } function loadWindowState() { - if (!chrome.storage) { return; } + if (!chrome.storage) { + return; + } - chrome.storage.sync.get('windowState', function(item) { - // load stored window state - windowState = (item && item.windowState) - ? item.windowState - : windowState; + chrome.storage.sync.get('windowState', function(item) { + // load stored window state + windowState = (item && item.windowState) ? + item.windowState : + windowState; - // subscribe to chrome's windowState events - chrome.app.window.current().onFullscreened.addListener(onFullscreened); - chrome.app.window.current().onBoundsChanged.addListener(onBoundsChanged); - }); + // subscribe to chrome's windowState events + chrome.app.window.current().onFullscreened.addListener(onFullscreened); + chrome.app.window.current().onBoundsChanged.addListener(onBoundsChanged); + }); } function onFullscreened() { - if (!isInGame && windowState == 'normal') { - storeData('windowState', 'fullscreen', null); - windowState = 'fullscreen'; - } + if (!isInGame && windowState == 'normal') { + storeData('windowState', 'fullscreen', null); + windowState = 'fullscreen'; + } } function onBoundsChanged() { - if (!isInGame && windowState == 'fullscreen') { - storeData('windowState', 'normal', null); - windowState = 'normal'; - } + if (!isInGame && windowState == 'fullscreen') { + storeData('windowState', 'normal', null); + windowState = 'normal'; + } } function changeUiModeForNaClLoad() { - $('#main-navigation').children().hide(); - $("#main-content").children().not("#listener, #naclSpinner").hide(); - $('#naclSpinnerMessage').text('Loading Moonlight plugin...'); - $('#naclSpinner').css('display', 'inline-block'); + $('#main-navigation').children().hide(); + $("#main-content").children().not("#listener, #naclSpinner").hide(); + $('#naclSpinnerMessage').text('Loading Moonlight plugin...'); + $('#naclSpinner').css('display', 'inline-block'); } function startPollingHosts() { - for(var hostUID in hosts) { - beginBackgroundPollingOfHost(hosts[hostUID]); - } + for (var hostUID in hosts) { + beginBackgroundPollingOfHost(hosts[hostUID]); + } } function stopPollingHosts() { - for(var hostUID in hosts) { - stopBackgroundPollingOfHost(hosts[hostUID]); - } + for (var hostUID in hosts) { + stopBackgroundPollingOfHost(hosts[hostUID]); + } } function restoreUiAfterNaClLoad() { - $('#main-navigation').children().not("#quitCurrentApp").show(); - $("#main-content").children().not("#listener, #naclSpinner, #game-grid").show(); - $('#naclSpinner').hide(); - $('#loadingSpinner').css('display', 'none'); - showHostsAndSettingsMode(); + $('#main-navigation').children().not("#quitCurrentApp").show(); + $("#main-content").children().not("#listener, #naclSpinner, #game-grid").show(); + $('#naclSpinner').hide(); + $('#loadingSpinner').css('display', 'none'); + showHostsAndSettingsMode(); - findNvService(function (finder, opt_error) { - if (finder.byService_['_nvstream._tcp']) { - var ips = Object.keys(finder.byService_['_nvstream._tcp']); - for (var i in ips) { - var ip = ips[i]; - if (finder.byService_['_nvstream._tcp'][ip]) { - var mDnsDiscoveredHost = new NvHTTP(ip, myUniqueid); - mDnsDiscoveredHost.pollServer(function(returneMdnsDiscoveredHost) { - // Just drop this if the host doesn't respond - if (!returneMdnsDiscoveredHost.online) { - return; - } - - if (hosts[returneMdnsDiscoveredHost.serverUid] != null) { - // if we're seeing a host we've already seen before, update it for the current local IP. - hosts[returneMdnsDiscoveredHost.serverUid].address = returneMdnsDiscoveredHost.address; - } else { - beginBackgroundPollingOfHost(returneMdnsDiscoveredHost); - addHostToGrid(returneMdnsDiscoveredHost, true); - } - }); - } + findNvService(function(finder, opt_error) { + if (finder.byService_['_nvstream._tcp']) { + var ips = Object.keys(finder.byService_['_nvstream._tcp']); + for (var i in ips) { + var ip = ips[i]; + if (finder.byService_['_nvstream._tcp'][ip]) { + var mDnsDiscoveredHost = new NvHTTP(ip, myUniqueid); + mDnsDiscoveredHost.pollServer(function(returneMdnsDiscoveredHost) { + // Just drop this if the host doesn't respond + if (!returneMdnsDiscoveredHost.online) { + return; } + + if (hosts[returneMdnsDiscoveredHost.serverUid] != null) { + // if we're seeing a host we've already seen before, update it for the current local IP. + hosts[returneMdnsDiscoveredHost.serverUid].address = returneMdnsDiscoveredHost.address; + } else { + beginBackgroundPollingOfHost(returneMdnsDiscoveredHost); + addHostToGrid(returneMdnsDiscoveredHost, true); + } + }); } - }); + } + } + }); } function beginBackgroundPollingOfHost(host) { - if (host.online) { + if (host.online) { + $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + // The host was already online. Just start polling in the background now. + activePolls[host.serverUid] = window.setInterval(function() { + // every 5 seconds, poll at the address we know it was live at + host.pollServer(function() { + if (host.online) { + $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + } else { + $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + } + }); + }, 5000); + } else { + $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + // The host was offline, so poll immediately. + host.pollServer(function() { + if (host.online) { $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); - // The host was already online. Just start polling in the background now. - activePolls[host.serverUid] = window.setInterval(function() { - // every 5 seconds, poll at the address we know it was live at - host.pollServer(function () { - if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); - } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); - } - }); - }, 5000); - } else { + } else { $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); - // The host was offline, so poll immediately. - host.pollServer(function () { - if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); - } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); - } + } - // Now start background polling - activePolls[host.serverUid] = window.setInterval(function() { - // every 5 seconds, poll at the address we know it was live at - host.pollServer(function () { - if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); - } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); - } - }); - }, 5000); + // Now start background polling + activePolls[host.serverUid] = window.setInterval(function() { + // every 5 seconds, poll at the address we know it was live at + host.pollServer(function() { + if (host.online) { + $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + } else { + $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + } }); - } + }, 5000); + }); + } } function stopBackgroundPollingOfHost(host) { @@ -162,254 +164,270 @@ function stopBackgroundPollingOfHost(host) { } function snackbarLog(givenMessage) { - console.log('%c[index.js, snackbarLog]', 'color: green;', givenMessage); - var data = { - message: givenMessage, - timeout: 2000 - }; - document.querySelector('#snackbar').MaterialSnackbar.showSnackbar(data); + console.log('%c[index.js, snackbarLog]', 'color: green;', givenMessage); + var data = { + message: givenMessage, + timeout: 2000 + }; + document.querySelector('#snackbar').MaterialSnackbar.showSnackbar(data); } function snackbarLogLong(givenMessage) { - console.log('%c[index.js, snackbarLog]', 'color: green;', givenMessage); - var data = { - message: givenMessage, - timeout: 5000 - }; - document.querySelector('#snackbar').MaterialSnackbar.showSnackbar(data); + console.log('%c[index.js, snackbarLog]', 'color: green;', givenMessage); + var data = { + message: givenMessage, + timeout: 5000 + }; + document.querySelector('#snackbar').MaterialSnackbar.showSnackbar(data); } function updateBitrateField() { - $('#bitrateField').html($('#bitrateSlider').val() + " Mbps"); - saveBitrate(); + $('#bitrateField').html($('#bitrateSlider').val() + " Mbps"); + saveBitrate(); } function moduleDidLoad() { - // load the HTTP cert and unique ID if we have one. - chrome.storage.sync.get('cert', function(savedCert) { - if (savedCert.cert != null) { // we have a saved cert - pairingCert = savedCert.cert; - } + // load the HTTP cert and unique ID if we have one. + chrome.storage.sync.get('cert', function(savedCert) { + if (savedCert.cert != null) { // we have a saved cert + pairingCert = savedCert.cert; + } - chrome.storage.sync.get('uniqueid', function(savedUniqueid) { - if (savedUniqueid.uniqueid != null) { // we have a saved uniqueid - myUniqueid = savedUniqueid.uniqueid; - } else { - myUniqueid = uniqueid(); - storeData('uniqueid', myUniqueid, null); - } + chrome.storage.sync.get('uniqueid', function(savedUniqueid) { + if (savedUniqueid.uniqueid != null) { // we have a saved uniqueid + myUniqueid = savedUniqueid.uniqueid; + } else { + myUniqueid = uniqueid(); + storeData('uniqueid', myUniqueid, null); + } - if (!pairingCert) { // we couldn't load a cert. Make one. - console.warn('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed to load local cert. Generating new one'); - sendMessage('makeCert', []).then(function (cert) { - storeData('cert', cert, null); - pairingCert = cert; - console.info('%c[index.js, moduleDidLoad]', 'color: green;', 'Generated new cert:', cert); - }, function (failedCert) { - console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed to generate new cert! Returned error was: \n', failedCert); - }).then(function (ret) { - sendMessage('httpInit', [pairingCert.cert, pairingCert.privateKey, myUniqueid]).then(function (ret) { - restoreUiAfterNaClLoad(); - }, function (failedInit) { - console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed httpInit! Returned error was: ', failedInit); - }); - }); - } - else { - sendMessage('httpInit', [pairingCert.cert, pairingCert.privateKey, myUniqueid]).then(function (ret) { - restoreUiAfterNaClLoad(); - }, function (failedInit) { - console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed httpInit! Returned error was: ', failedInit); - }); - } - - // load previously connected hosts, which have been killed into an object, and revive them back into a class - chrome.storage.sync.get('hosts', function(previousValue) { - hosts = previousValue.hosts != null ? previousValue.hosts : {}; - for(var hostUID in hosts) { // programmatically add each new host. - var revivedHost = new NvHTTP(hosts[hostUID].address, myUniqueid, hosts[hostUID].userEnteredAddress); - revivedHost.serverUid = hosts[hostUID].serverUid; - revivedHost.externalIP = hosts[hostUID].externalIP; - revivedHost.hostname = hosts[hostUID].hostname; - addHostToGrid(revivedHost); - } - console.log('%c[index.js]', 'color: green;', 'Loaded previously connected hosts'); - }); + if (!pairingCert) { // we couldn't load a cert. Make one. + console.warn('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed to load local cert. Generating new one'); + sendMessage('makeCert', []).then(function(cert) { + storeData('cert', cert, null); + pairingCert = cert; + console.info('%c[index.js, moduleDidLoad]', 'color: green;', 'Generated new cert:', cert); + }, function(failedCert) { + console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed to generate new cert! Returned error was: \n', failedCert); + }).then(function(ret) { + sendMessage('httpInit', [pairingCert.cert, pairingCert.privateKey, myUniqueid]).then(function(ret) { + restoreUiAfterNaClLoad(); + }, function(failedInit) { + console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed httpInit! Returned error was: ', failedInit); + }); }); + } else { + sendMessage('httpInit', [pairingCert.cert, pairingCert.privateKey, myUniqueid]).then(function(ret) { + restoreUiAfterNaClLoad(); + }, function(failedInit) { + console.error('%c[index.js, moduleDidLoad]', 'color: green;', 'Failed httpInit! Returned error was: ', failedInit); + }); + } + + // load previously connected hosts, which have been killed into an object, and revive them back into a class + chrome.storage.sync.get('hosts', function(previousValue) { + hosts = previousValue.hosts != null ? previousValue.hosts : {}; + for (var hostUID in hosts) { // programmatically add each new host. + var revivedHost = new NvHTTP(hosts[hostUID].address, myUniqueid, hosts[hostUID].userEnteredAddress); + revivedHost.serverUid = hosts[hostUID].serverUid; + revivedHost.externalIP = hosts[hostUID].externalIP; + revivedHost.hostname = hosts[hostUID].hostname; + addHostToGrid(revivedHost); + } + console.log('%c[index.js]', 'color: green;', 'Loaded previously connected hosts'); + }); }); + }); } // pair to the given NvHTTP host object. Returns whether pairing was successful. function pairTo(nvhttpHost, onSuccess, onFailure) { - if(!pairingCert) { - snackbarLog('ERROR: cert has not been generated yet. Is NaCl initialized?'); - console.warn('%c[index.js]', 'color: green;', 'User wants to pair, and we still have no cert. Problem = very yes.'); - onFailure(); - return; + if (!pairingCert) { + snackbarLog('ERROR: cert has not been generated yet. Is NaCl initialized?'); + console.warn('%c[index.js]', 'color: green;', 'User wants to pair, and we still have no cert. Problem = very yes.'); + onFailure(); + return; + } + + nvhttpHost.pollServer(function(ret) { + if (!nvhttpHost.online) { + snackbarLog('Failed to connect to ' + nvhttpHost.hostname + '! Are you sure the host is on?'); + console.error('%c[index.js]', 'color: green;', 'Host declared as offline:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs + onFailure(); + return; } - nvhttpHost.pollServer(function (ret) { - if (!nvhttpHost.online) { - snackbarLog('Failed to connect to ' + nvhttpHost.hostname + '! Are you sure the host is on?'); - console.error('%c[index.js]', 'color: green;', 'Host declared as offline:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs - onFailure(); - return; - } + if (nvhttpHost.paired) { + onSuccess(); + return; + } - if (nvhttpHost.paired) { - onSuccess(); - return; - } + var randomNumber = String("0000" + (Math.random() * 10000 | 0)).slice(-4); + var pairingDialog = document.querySelector('#pairingDialog'); + $('#pairingDialogText').html('Please enter the number ' + randomNumber + ' on the GFE dialog on the computer. This dialog will be dismissed once complete'); + pairingDialog.showModal(); - var randomNumber = String("0000" + (Math.random()*10000|0)).slice(-4); - var pairingDialog = document.querySelector('#pairingDialog'); - $('#pairingDialogText').html('Please enter the number ' + randomNumber + ' on the GFE dialog on the computer. This dialog will be dismissed once complete'); - pairingDialog.showModal(); - - $('#cancelPairingDialog').off('click'); - $('#cancelPairingDialog').on('click', function () { - pairingDialog.close(); - }); - - console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with random number' + randomNumber); - nvhttpHost.pair(randomNumber).then(function (paired) { - if (!paired) { - if (nvhttpHost.currentGame != 0) { - $('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.'); - } else { - $('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.'); - } - console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs - onFailure(); - return; - } - - snackbarLog('Pairing successful'); - pairingDialog.close(); - onSuccess(); - }, function (failedPairing) { - snackbarLog('Failed pairing to: ' + nvhttpHost.hostname); - console.error('%c[index.js]', 'color: green;', 'Pairing failed, and returned:', failedPairing); - console.error('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs - onFailure(); - }); + $('#cancelPairingDialog').off('click'); + $('#cancelPairingDialog').on('click', function() { + pairingDialog.close(); }); + + console.log('%c[index.js]', 'color: green;', 'Sending pairing request to ' + nvhttpHost.hostname + ' with random number' + randomNumber); + nvhttpHost.pair(randomNumber).then(function(paired) { + if (!paired) { + if (nvhttpHost.currentGame != 0) { + $('#pairingDialogText').html('Error: ' + nvhttpHost.hostname + ' is busy. Stop streaming to pair.'); + } else { + $('#pairingDialogText').html('Error: failed to pair with ' + nvhttpHost.hostname + '.'); + } + console.log('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs + onFailure(); + return; + } + + snackbarLog('Pairing successful'); + pairingDialog.close(); + onSuccess(); + }, function(failedPairing) { + snackbarLog('Failed pairing to: ' + nvhttpHost.hostname); + console.error('%c[index.js]', 'color: green;', 'Pairing failed, and returned:', failedPairing); + console.error('%c[index.js]', 'color: green;', 'Failed API object:', nvhttpHost, nvhttpHost.toString()); //Logging both the object and the toString version for text logs + onFailure(); + }); + }); } function hostChosen(host) { - if (!host.online) { - return; - } + if (!host.online) { + return; + } - // Avoid delay from other polling during pairing - stopPollingHosts(); + // Avoid delay from other polling during pairing + stopPollingHosts(); - api = host; - if (!host.paired) { - // Still not paired; go to the pairing flow - pairTo(host, function() { - showApps(host); - saveHosts(); - }, - function(){ - startPollingHosts(); - }); - } else { - // When we queried again, it was paired, so show apps. + api = host; + if (!host.paired) { + // Still not paired; go to the pairing flow + pairTo(host, function() { showApps(host); - } + saveHosts(); + }, + function() { + startPollingHosts(); + }); + } else { + // When we queried again, it was paired, so show apps. + showApps(host); + } } // the `+` was selected on the host grid. // give the user a dialog to input connection details for the PC function addHost() { - var modal = document.querySelector('#addHostDialog'); - modal.showModal(); + var modal = document.querySelector('#addHostDialog'); + modal.showModal(); - // drop the dialog if they cancel - $('#cancelAddHost').off('click'); - $('#cancelAddHost').on('click', function() { - modal.close(); - }); - - // try to pair if they continue - $('#continueAddHost').off('click'); - $('#continueAddHost').on('click', function () { - var inputHost = $('#dialogInputHost').val(); - var _nvhttpHost = new NvHTTP(inputHost, myUniqueid, inputHost); - - pairTo(_nvhttpHost, function() { - // Check if we already have record of this host - if (hosts[_nvhttpHost.serverUid] != null) { - // Just update the addresses - hosts[_nvhttpHost.serverUid].address = _nvhttpHost.address; - hosts[_nvhttpHost.serverUid].userEnteredAddress = _nvhttpHost.userEnteredAddress; - } - else { - beginBackgroundPollingOfHost(_nvhttpHost); - addHostToGrid(_nvhttpHost); - } - saveHosts(); - }, function() { - snackbarLog('pairing to ' + inputHost + ' failed!'); - }); - modal.close(); + // drop the dialog if they cancel + $('#cancelAddHost').off('click'); + $('#cancelAddHost').on('click', function() { + modal.close(); + }); + + // try to pair if they continue + $('#continueAddHost').off('click'); + $('#continueAddHost').on('click', function() { + var inputHost = $('#dialogInputHost').val(); + var _nvhttpHost = new NvHTTP(inputHost, myUniqueid, inputHost); + + pairTo(_nvhttpHost, function() { + // Check if we already have record of this host + if (hosts[_nvhttpHost.serverUid] != null) { + // Just update the addresses + hosts[_nvhttpHost.serverUid].address = _nvhttpHost.address; + hosts[_nvhttpHost.serverUid].userEnteredAddress = _nvhttpHost.userEnteredAddress; + } else { + beginBackgroundPollingOfHost(_nvhttpHost); + addHostToGrid(_nvhttpHost); + } + saveHosts(); + }, function() { + snackbarLog('pairing to ' + inputHost + ' failed!'); }); + modal.close(); + }); } // host is an NvHTTP object function addHostToGrid(host, ismDNSDiscovered) { - var outerDiv = $("
", {class: 'host-container mdl-card mdl-shadow--4dp', id: 'host-container-' + host.serverUid, role: 'link', tabindex: 0, 'aria-label': host.hostname }); - var cell = $("
", {class: 'mdl-card__title mdl-card--expand', id: 'hostgrid-' + host.serverUid }); - $(cell).prepend($("

", {class: "mdl-card__title-text", html: host.hostname})); - var removalButton = $("
", {class: "remove-host", id: "removeHostButton-" + host.serverUid, role: 'button', tabindex: 0, 'aria-label': 'Remove host ' + host.hostname}); - removalButton.off('click'); - removalButton.click(function () { - removeClicked(host); - }); - cell.off('click'); - cell.click(function () { - hostChosen(host); - }); - outerDiv.keypress(function(e){ - if(e.keyCode == 13) { - hostChosen(host); - } - }); - $(outerDiv).append(cell); - if (!ismDNSDiscovered) { - // we don't have the option to delete mDNS hosts. So don't show it to the user. - $(outerDiv).append(removalButton); + var outerDiv = $("
", { + class: 'host-container mdl-card mdl-shadow--4dp', + id: 'host-container-' + host.serverUid, + role: 'link', + tabindex: 0, + 'aria-label': host.hostname + }); + var cell = $("
", { + class: 'mdl-card__title mdl-card--expand', + id: 'hostgrid-' + host.serverUid + }); + $(cell).prepend($("

", { + class: "mdl-card__title-text", + html: host.hostname + })); + var removalButton = $("
", { + class: "remove-host", + id: "removeHostButton-" + host.serverUid, + role: 'button', + tabindex: 0, + 'aria-label': 'Remove host ' + host.hostname + }); + removalButton.off('click'); + removalButton.click(function() { + removeClicked(host); + }); + cell.off('click'); + cell.click(function() { + hostChosen(host); + }); + outerDiv.keypress(function(e) { + if (e.keyCode == 13) { + hostChosen(host); } - $('#host-grid').append(outerDiv); - hosts[host.serverUid] = host; + }); + $(outerDiv).append(cell); + if (!ismDNSDiscovered) { + // we don't have the option to delete mDNS hosts. So don't show it to the user. + $(outerDiv).append(removalButton); + } + $('#host-grid').append(outerDiv); + hosts[host.serverUid] = host; } function removeClicked(host) { - var deleteHostDialog = document.querySelector('#deleteHostDialog'); - document.getElementById('deleteHostDialogText').innerHTML = + var deleteHostDialog = document.querySelector('#deleteHostDialog'); + document.getElementById('deleteHostDialogText').innerHTML = ' Are you sure you want to delete ' + host.hostname + '?'; - deleteHostDialog.showModal(); + deleteHostDialog.showModal(); - $('#cancelDeleteHost').off('click'); - $('#cancelDeleteHost').on('click', function () { - deleteHostDialog.close(); - }); + $('#cancelDeleteHost').off('click'); + $('#cancelDeleteHost').on('click', function() { + deleteHostDialog.close(); + }); - // locally remove the hostname/ip from the saved `hosts` array. - // note: this does not make the host forget the pairing to us. - // this means we can re-add the host, and will still be paired. - $('#continueDeleteHost').off('click'); - $('#continueDeleteHost').on('click', function () { - var deleteHostDialog = document.querySelector('#deleteHostDialog'); - $('#host-container-' + host.serverUid).remove(); - delete hosts[host.serverUid]; // remove the host from the array; - saveHosts(); - deleteHostDialog.close(); - }); + // locally remove the hostname/ip from the saved `hosts` array. + // note: this does not make the host forget the pairing to us. + // this means we can re-add the host, and will still be paired. + $('#continueDeleteHost').off('click'); + $('#continueDeleteHost').on('click', function() { + var deleteHostDialog = document.querySelector('#deleteHostDialog'); + $('#host-container-' + host.serverUid).remove(); + delete hosts[host.serverUid]; // remove the host from the array; + saveHosts(); + deleteHostDialog.close(); + }); } // puts the CSS style for current app on the app that's currently running @@ -418,17 +436,17 @@ function removeClicked(host) { // the function was made like this so that we can remove duplicated code, but // not do N*N stylizations of the box art, or make the code not flow very well function stylizeBoxArt(freshApi, appIdToStylize) { - if (freshApi.currentGame === appIdToStylize){ // stylize the currently running game - // destylize it, if it has the not-current-game style - if ($('#game-'+ appIdToStylize).hasClass("not-current-game")) $('#game-'+ appIdToStylize).removeClass("not-current-game"); - // add the current-game style - $('#game-'+ appIdToStylize).addClass("current-game"); - } else { - // destylize it, if it has the current-game style - if ($('#game-'+ appIdToStylize).hasClass("current-game")) $('#game-'+ appIdToStylize).removeClass("current-game"); - // add the not-current-game style - $('#game-'+ appIdToStylize).addClass('not-current-game'); - } + if (freshApi.currentGame === appIdToStylize) { // stylize the currently running game + // destylize it, if it has the not-current-game style + if ($('#game-' + appIdToStylize).hasClass("not-current-game")) $('#game-' + appIdToStylize).removeClass("not-current-game"); + // add the current-game style + $('#game-' + appIdToStylize).addClass("current-game"); + } else { + // destylize it, if it has the current-game style + if ($('#game-' + appIdToStylize).hasClass("current-game")) $('#game-' + appIdToStylize).removeClass("current-game"); + // add the not-current-game style + $('#game-' + appIdToStylize).addClass('not-current-game'); + } } function sortTitles(list, sortOrder) { @@ -438,329 +456,358 @@ function sortTitles(list, sortOrder) { // A - Z if (sortOrder === 'ASC') { - if (titleA < titleB) { return -1; } - if (titleA > titleB) { return 1; } + if (titleA < titleB) { + return -1; + } + if (titleA > titleB) { + return 1; + } return 0; } // Z - A if (sortOrder === 'DESC') { - if (titleA < titleB) { return 1; } - if (titleA > titleB) { return -1; } - return 0; } + if (titleA < titleB) { + return 1; + } + if (titleA > titleB) { + return -1; + } + return 0; + } }); } // show the app list function showApps(host) { - if(!host || !host.paired) { // safety checking. shouldn't happen. - console.log('%c[index.js, showApps]', 'color: green;', 'Moved into showApps, but `host` did not initialize properly! Failing.'); - return; - } - console.log('%c[index.js, showApps]', 'color: green;', 'Current host object:', host, host.toString()); //Logging both object (for console) and toString-ed object (for text logs) - $('#quitCurrentApp').show(); - $("#gameList .game-container").remove(); + if (!host || !host.paired) { // safety checking. shouldn't happen. + console.log('%c[index.js, showApps]', 'color: green;', 'Moved into showApps, but `host` did not initialize properly! Failing.'); + return; + } + console.log('%c[index.js, showApps]', 'color: green;', 'Current host object:', host, host.toString()); //Logging both object (for console) and toString-ed object (for text logs) + $('#quitCurrentApp').show(); + $("#gameList .game-container").remove(); - // Show a spinner while the applist loads - $('#naclSpinnerMessage').text('Loading apps...'); - $('#naclSpinner').css('display', 'inline-block'); + // Show a spinner while the applist loads + $('#naclSpinnerMessage').text('Loading apps...'); + $('#naclSpinner').css('display', 'inline-block'); - host.getAppList().then(function (appList) { - // if game grid is populated, empty it - $("div.game-container").remove(); + host.getAppList().then(function(appList) { + // if game grid is populated, empty it + $("div.game-container").remove(); - $('#naclSpinner').hide(); - $("#game-grid").show(); + $('#naclSpinner').hide(); + $("#game-grid").show(); - const sortedAppList = sortTitles(appList, 'ASC'); + const sortedAppList = sortTitles(appList, 'ASC'); - sortedAppList.forEach(function (app) { - if ($('#game-' + app.id).length === 0) { - // double clicking the button will cause multiple box arts to appear. - // to mitigate this we ensure we don't add a duplicate. - // This isn't perfect: there's lots of RTTs before the logic prevents anything - var outerDiv = $("
", {class: 'game-container mdl-card mdl-shadow--4dp', id: 'game-'+app.id, role: 'link', tabindex: 0, title: app.title, 'aria-label': app.title }); - - $(outerDiv).append($("
", {class: "game-title", html: $("", {html: app.title} )})); - $("#game-grid").append(outerDiv); - - $('#game-'+app.id).on('click', function () { - startGame(host, app.id); - }); - $('#game-'+app.id).keypress(function(e){ - if(e.keyCode == 13) { - startGame(host, app.id); - } - }); - - // apply CSS stylization to indicate whether the app is active - stylizeBoxArt(host, app.id); - } - host.getBoxArt(app.id).then(function (resolvedPromise) { - // put the box art into the image holder - $(outerDiv).append($("", {src: resolvedPromise, name: app.title })); - - }, function (failedPromise) { - console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); - $(outerDiv).append($("", {src: "static/res/no_app_image.png", name: app.title })); - }); + sortedAppList.forEach(function(app) { + if ($('#game-' + app.id).length === 0) { + // double clicking the button will cause multiple box arts to appear. + // to mitigate this we ensure we don't add a duplicate. + // This isn't perfect: there's lots of RTTs before the logic prevents anything + var outerDiv = $("
", { + class: 'game-container mdl-card mdl-shadow--4dp', + id: 'game-' + app.id, + role: 'link', + tabindex: 0, + title: app.title, + 'aria-label': app.title }); - }, function (failedAppList) { - $('#naclSpinner').hide(); - console.log('%c[index.js, showApps]', 'color: green;', 'Failed to get applist from host: ' + host.hostname, '\n Host object:', host, host.toString()); + $(outerDiv).append($("
", { + class: "game-title", + html: $("", { + html: app.title + }) + })); + $("#game-grid").append(outerDiv); + + $('#game-' + app.id).on('click', function() { + startGame(host, app.id); + }); + $('#game-' + app.id).keypress(function(e) { + if (e.keyCode == 13) { + startGame(host, app.id); + } + }); + + // apply CSS stylization to indicate whether the app is active + stylizeBoxArt(host, app.id); + } + host.getBoxArt(app.id).then(function(resolvedPromise) { + // put the box art into the image holder + $(outerDiv).append($("", { + src: resolvedPromise, + name: app.title + })); + + }, function(failedPromise) { + console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); + $(outerDiv).append($("", { + src: "static/res/no_app_image.png", + name: app.title + })); + }); }); + }, function(failedAppList) { + $('#naclSpinner').hide(); - showAppsMode(); + console.log('%c[index.js, showApps]', 'color: green;', 'Failed to get applist from host: ' + host.hostname, '\n Host object:', host, host.toString()); + }); + + showAppsMode(); } // set the layout to the initial mode you see when you open moonlight function showHostsAndSettingsMode() { - console.log('%c[index.js]', 'color: green;', 'Entering "Show apps and hosts" mode'); - $("#main-navigation").show(); - $(".nav-menu-parent").show(); - $("#externalAudioBtn").show(); - $("#main-content").children().not("#listener, #loadingSpinner, #naclSpinner").show(); - $('#game-grid').hide(); - $('#backIcon').hide(); - $('#quitCurrentApp').hide(); - $("#main-content").removeClass("fullscreen"); - $("#listener").removeClass("fullscreen"); + console.log('%c[index.js]', 'color: green;', 'Entering "Show apps and hosts" mode'); + $("#main-navigation").show(); + $(".nav-menu-parent").show(); + $("#externalAudioBtn").show(); + $("#main-content").children().not("#listener, #loadingSpinner, #naclSpinner").show(); + $('#game-grid').hide(); + $('#backIcon').hide(); + $('#quitCurrentApp').hide(); + $("#main-content").removeClass("fullscreen"); + $("#listener").removeClass("fullscreen"); - startPollingHosts(); + startPollingHosts(); } function showAppsMode() { - console.log('%c[index.js]', 'color: green;', 'Entrering "Show apps" mode'); - $('#backIcon').show(); - $("#main-navigation").show(); - $("#main-content").children().not("#listener, #loadingSpinner, #naclSpinner").show(); - $("#streamSettings").hide(); - $(".nav-menu-parent").hide(); - $("#externalAudioBtn").hide(); - $("#host-grid").hide(); - $("#settings").hide(); - $("#main-content").removeClass("fullscreen"); - $("#listener").removeClass("fullscreen"); + console.log('%c[index.js]', 'color: green;', 'Entrering "Show apps" mode'); + $('#backIcon').show(); + $("#main-navigation").show(); + $("#main-content").children().not("#listener, #loadingSpinner, #naclSpinner").show(); + $("#streamSettings").hide(); + $(".nav-menu-parent").hide(); + $("#externalAudioBtn").hide(); + $("#host-grid").hide(); + $("#settings").hide(); + $("#main-content").removeClass("fullscreen"); + $("#listener").removeClass("fullscreen"); - // FIXME: We want to eventually poll on the app screen but we can't now - // because it slows down box art loading and we don't update the UI live - // anyway. - stopPollingHosts(); + // FIXME: We want to eventually poll on the app screen but we can't now + // because it slows down box art loading and we don't update the UI live + // anyway. + stopPollingHosts(); } // start the given appID. if another app is running, offer to quit it. // if the given app is already running, just resume it. function startGame(host, appID) { - if(!host || !host.paired) { - console.error('%c[index.js, startGame]', 'color: green;', 'Attempted to start a game, but `host` did not initialize properly. Host object: ', host); - return; - } + if (!host || !host.paired) { + console.error('%c[index.js, startGame]', 'color: green;', 'Attempted to start a game, but `host` did not initialize properly. Host object: ', host); + return; + } - // refresh the server info, because the user might have quit the game. - host.refreshServerInfo().then(function (ret) { - host.getAppById(appID).then(function (appToStart) { + // refresh the server info, because the user might have quit the game. + host.refreshServerInfo().then(function(ret) { + host.getAppById(appID).then(function(appToStart) { - if(host.currentGame != 0 && host.currentGame != appID) { - host.getAppById(host.currentGame).then(function (currentApp) { - var quitAppDialog = document.querySelector('#quitAppDialog'); - document.getElementById('quitAppDialogText').innerHTML = - currentApp.title + ' is already running. Would you like to quit ' + - currentApp.title + '?'; - quitAppDialog.showModal(); - $('#cancelQuitApp').off('click'); - $('#cancelQuitApp').on('click', function () { - quitAppDialog.close(); - console.log('[index.js, startGame]','color: green;', 'Closing app dialog, and returning'); - }); - $('#continueQuitApp').off('click'); - $('#continueQuitApp').on('click', function () { - console.log('[index.js, startGame]','color: green;', 'Stopping game, and closing app dialog, and returning'); - stopGame(host, function () { - // please oh please don't infinite loop with recursion - startGame(host, appID); - }); - quitAppDialog.close(); - }); - - return; - }, function (failedCurrentApp) { - console.error('[index.js, startGame]','color: green;', 'Failed to get the current running app from host! Returned error was:' + failedCurrentApp, '\n Host object:', host, host.toString()); - return; - }); - return; - } - - var frameRate = $('#selectFramerate').data('value').toString(); - var optimize = $("#optimizeGamesSwitch").parent().hasClass('is-checked') ? 1 : 0; - var streamWidth = $('#selectResolution').data('value').split(':')[0]; - var streamHeight = $('#selectResolution').data('value').split(':')[1]; - // we told the user it was in Mbps. We're dirty liars and use Kbps behind their back. - var bitrate = parseInt($("#bitrateSlider").val()) * 1000; - console.log('%c[index.js, startGame]','color:green;', 'startRequest:' + host.address + ":" + streamWidth + ":" + streamHeight + ":" + frameRate + ":" + bitrate + ":" + optimize); - - var rikey = generateRemoteInputKey(); - var rikeyid = generateRemoteInputKeyId(); - var gamepadMask = getConnectedGamepadMask(); - - $('#loadingMessage').text('Starting ' + appToStart.title + '...'); - playGameMode(); - - if(host.currentGame == appID) { // if user wants to launch the already-running app, then we resume it. - return host.resumeApp( - rikey, rikeyid, 0x030002 // Surround channel mask << 16 | Surround channel count - ).then(function (ret) { - sendMessage('startRequest', [host.address, streamWidth, streamHeight, frameRate, - bitrate.toString(), rikey, rikeyid.toString(), host.appVersion]); - }, function (failedResumeApp) { - console.eror('%c[index.js, startGame]', 'color:green;', 'Failed to resume the app! Returned error was' + failedResumeApp); - return; - }); - } - - var remote_audio_enabled = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked') ? 1 : 0; - - host.launchApp(appID, - streamWidth + "x" + streamHeight + "x" + frameRate, - optimize, // DON'T Allow GFE (0) to optimize game settings, or ALLOW (1) to optimize game settings - rikey, rikeyid, - remote_audio_enabled, // Play audio locally too? - 0x030002, // Surround channel mask << 16 | Surround channel count - gamepadMask - ).then(function (ret) { - sendMessage('startRequest', [host.address, streamWidth, streamHeight, frameRate, - bitrate.toString(), rikey, rikeyid.toString(), host.appVersion]); - }, function (failedLaunchApp) { - console.error('%c[index.js, launchApp]','color: green;','Failed to launch app width id: ' + appID + '\nReturned error was: ' + failedLaunchApp); - return; + if (host.currentGame != 0 && host.currentGame != appID) { + host.getAppById(host.currentGame).then(function(currentApp) { + var quitAppDialog = document.querySelector('#quitAppDialog'); + document.getElementById('quitAppDialogText').innerHTML = + currentApp.title + ' is already running. Would you like to quit ' + + currentApp.title + '?'; + quitAppDialog.showModal(); + $('#cancelQuitApp').off('click'); + $('#cancelQuitApp').on('click', function() { + quitAppDialog.close(); + console.log('[index.js, startGame]', 'color: green;', 'Closing app dialog, and returning'); + }); + $('#continueQuitApp').off('click'); + $('#continueQuitApp').on('click', function() { + console.log('[index.js, startGame]', 'color: green;', 'Stopping game, and closing app dialog, and returning'); + stopGame(host, function() { + // please oh please don't infinite loop with recursion + startGame(host, appID); }); + quitAppDialog.close(); + }); + return; + }, function(failedCurrentApp) { + console.error('[index.js, startGame]', 'color: green;', 'Failed to get the current running app from host! Returned error was:' + failedCurrentApp, '\n Host object:', host, host.toString()); + return; }); + return; + } + + var frameRate = $('#selectFramerate').data('value').toString(); + var optimize = $("#optimizeGamesSwitch").parent().hasClass('is-checked') ? 1 : 0; + var streamWidth = $('#selectResolution').data('value').split(':')[0]; + var streamHeight = $('#selectResolution').data('value').split(':')[1]; + // we told the user it was in Mbps. We're dirty liars and use Kbps behind their back. + var bitrate = parseInt($("#bitrateSlider").val()) * 1000; + console.log('%c[index.js, startGame]', 'color:green;', 'startRequest:' + host.address + ":" + streamWidth + ":" + streamHeight + ":" + frameRate + ":" + bitrate + ":" + optimize); + + var rikey = generateRemoteInputKey(); + var rikeyid = generateRemoteInputKeyId(); + var gamepadMask = getConnectedGamepadMask(); + + $('#loadingMessage').text('Starting ' + appToStart.title + '...'); + playGameMode(); + + if (host.currentGame == appID) { // if user wants to launch the already-running app, then we resume it. + return host.resumeApp( + rikey, rikeyid, 0x030002 // Surround channel mask << 16 | Surround channel count + ).then(function(ret) { + sendMessage('startRequest', [host.address, streamWidth, streamHeight, frameRate, + bitrate.toString(), rikey, rikeyid.toString(), host.appVersion + ]); + }, function(failedResumeApp) { + console.eror('%c[index.js, startGame]', 'color:green;', 'Failed to resume the app! Returned error was' + failedResumeApp); + return; + }); + } + + var remote_audio_enabled = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked') ? 1 : 0; + + host.launchApp(appID, + streamWidth + "x" + streamHeight + "x" + frameRate, + optimize, // DON'T Allow GFE (0) to optimize game settings, or ALLOW (1) to optimize game settings + rikey, rikeyid, + remote_audio_enabled, // Play audio locally too? + 0x030002, // Surround channel mask << 16 | Surround channel count + gamepadMask + ).then(function(ret) { + sendMessage('startRequest', [host.address, streamWidth, streamHeight, frameRate, + bitrate.toString(), rikey, rikeyid.toString(), host.appVersion + ]); + }, function(failedLaunchApp) { + console.error('%c[index.js, launchApp]', 'color: green;', 'Failed to launch app width id: ' + appID + '\nReturned error was: ' + failedLaunchApp); + return; + }); + }); + }); } function playGameMode() { - console.log('%c[index.js, playGameMode]', 'color:green;', 'Entering play game mode'); - isInGame = true; + console.log('%c[index.js, playGameMode]', 'color:green;', 'Entering play game mode'); + isInGame = true; - $("#main-navigation").hide(); - $("#main-content").children().not("#listener, #loadingSpinner").hide(); - $("#main-content").addClass("fullscreen"); + $("#main-navigation").hide(); + $("#main-content").children().not("#listener, #loadingSpinner").hide(); + $("#main-content").addClass("fullscreen"); - chrome.app.window.current().fullscreen(); - fullscreenNaclModule(); - $('#loadingSpinner').css('display', 'inline-block'); + chrome.app.window.current().fullscreen(); + fullscreenNaclModule(); + $('#loadingSpinner').css('display', 'inline-block'); } // Maximize the size of the nacl module by scaling and resizing appropriately function fullscreenNaclModule() { - var streamWidth = $('#selectResolution').data('value').split(':')[0]; - var streamHeight = $('#selectResolution').data('value').split(':')[1]; - var screenWidth = window.innerWidth; - var screenHeight = window.innerHeight; + var streamWidth = $('#selectResolution').data('value').split(':')[0]; + var streamHeight = $('#selectResolution').data('value').split(':')[1]; + var screenWidth = window.innerWidth; + var screenHeight = window.innerHeight; - var xRatio = screenWidth / streamWidth; - var yRatio = screenHeight / streamHeight; + var xRatio = screenWidth / streamWidth; + var yRatio = screenHeight / streamHeight; - var zoom = Math.min(xRatio, yRatio); + var zoom = Math.min(xRatio, yRatio); - var module = $("#nacl_module")[0]; - module.width = zoom * streamWidth; - module.height = zoom * streamHeight; - module.style.paddingTop = ((screenHeight - module.height) / 2) + "px"; + var module = $("#nacl_module")[0]; + module.width = zoom * streamWidth; + module.height = zoom * streamHeight; + module.style.paddingTop = ((screenHeight - module.height) / 2) + "px"; } function stopGameWithConfirmation() { - if (api.currentGame === 0) { - snackbarLog('Nothing was running'); - } else { - api.getAppById(api.currentGame).then(function (currentGame) { - var quitAppDialog = document.querySelector('#quitAppDialog'); - document.getElementById('quitAppDialogText').innerHTML = - ' Are you sure you would like to quit ' + - currentGame.title + '? Unsaved progress will be lost.'; - quitAppDialog.showModal(); - $('#cancelQuitApp').off('click'); - $('#cancelQuitApp').on('click', function () { - console.log('%c[index.js, stopGameWithConfirmation]', 'color:green;', 'Closing app dialog, and returning'); - quitAppDialog.close(); - }); - $('#continueQuitApp').off('click'); - $('#continueQuitApp').on('click', function () { - console.log('%c[index.js, stopGameWithConfirmation]', 'color:green;', 'Stopping game, and closing app dialog, and returning'); - stopGame(api); - quitAppDialog.close(); - }); + if (api.currentGame === 0) { + snackbarLog('Nothing was running'); + } else { + api.getAppById(api.currentGame).then(function(currentGame) { + var quitAppDialog = document.querySelector('#quitAppDialog'); + document.getElementById('quitAppDialogText').innerHTML = + ' Are you sure you would like to quit ' + + currentGame.title + '? Unsaved progress will be lost.'; + quitAppDialog.showModal(); + $('#cancelQuitApp').off('click'); + $('#cancelQuitApp').on('click', function() { + console.log('%c[index.js, stopGameWithConfirmation]', 'color:green;', 'Closing app dialog, and returning'); + quitAppDialog.close(); + }); + $('#continueQuitApp').off('click'); + $('#continueQuitApp').on('click', function() { + console.log('%c[index.js, stopGameWithConfirmation]', 'color:green;', 'Stopping game, and closing app dialog, and returning'); + stopGame(api); + quitAppDialog.close(); + }); - }); - } + }); + } } function stopGame(host, callbackFunction) { - isInGame = false; + isInGame = false; - if (!host.paired) { + if (!host.paired) { + return; + } + + host.refreshServerInfo().then(function(ret) { + host.getAppById(host.currentGame).then(function(runningApp) { + if (!runningApp) { + snackbarLog('Nothing was running'); return; - } - - host.refreshServerInfo().then(function (ret) { - host.getAppById(host.currentGame).then(function (runningApp) { - if (!runningApp) { - snackbarLog('Nothing was running'); - return; - } - var appName = runningApp.title; - snackbarLog('Stopping ' + appName); - host.quitApp().then(function (ret2) { - host.refreshServerInfo().then(function (ret3) { // refresh to show no app is currently running. - showAppsMode(); - stylizeBoxArt(host, runningApp.id); - if (typeof(callbackFunction) === "function") callbackFunction(); - }, function (failedRefreshInfo2) { - console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to refresh server info! Returned error was:' + failedRefreshInfo + ' and failed server was:', host, host.toString()); - }); - }, function (failedQuitApp) { - console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to quit app! Returned error was:' + failedQuitApp); - }); - }, function (failedGetApp) { - console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to get app ID! Returned error was:' + failedRefreshInfo); + } + var appName = runningApp.title; + snackbarLog('Stopping ' + appName); + host.quitApp().then(function(ret2) { + host.refreshServerInfo().then(function(ret3) { // refresh to show no app is currently running. + showAppsMode(); + stylizeBoxArt(host, runningApp.id); + if (typeof(callbackFunction) === "function") callbackFunction(); + }, function(failedRefreshInfo2) { + console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to refresh server info! Returned error was:' + failedRefreshInfo + ' and failed server was:', host, host.toString()); }); - }, function (failedRefreshInfo) { - console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to refresh server info! Returned error was:' + failedRefreshInfo); + }, function(failedQuitApp) { + console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to quit app! Returned error was:' + failedQuitApp); + }); + }, function(failedGetApp) { + console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to get app ID! Returned error was:' + failedRefreshInfo); }); + }, function(failedRefreshInfo) { + console.error('%c[index.js, stopGame]', 'color:green;', 'Failed to refresh server info! Returned error was:' + failedRefreshInfo); + }); } function storeData(key, data, callbackFunction) { - var obj = {}; - obj[key] = data; - if(chrome.storage) - chrome.storage.sync.set(obj, callbackFunction); + var obj = {}; + obj[key] = data; + if (chrome.storage) + chrome.storage.sync.set(obj, callbackFunction); } function saveResolution() { - var chosenResolution = $(this).data('value'); - $('#selectResolution').text($(this).text()).data('value', chosenResolution); - storeData('resolution', chosenResolution, null); - updateDefaultBitrate(); + var chosenResolution = $(this).data('value'); + $('#selectResolution').text($(this).text()).data('value', chosenResolution); + storeData('resolution', chosenResolution, null); + updateDefaultBitrate(); } function saveOptimize() { - // MaterialDesignLight uses the mouseup trigger, so we give it some time to change the class name before - // checking the new state - setTimeout(function() { - var chosenOptimize = $("#optimizeGamesSwitch").parent().hasClass('is-checked'); - console.log('%c[index.js, saveOptimize]', 'color: green;', 'Saving optimize state : ' + chosenOptimize); - storeData('optimize', chosenOptimize, null); - }, 100); + // MaterialDesignLight uses the mouseup trigger, so we give it some time to change the class name before + // checking the new state + setTimeout(function() { + var chosenOptimize = $("#optimizeGamesSwitch").parent().hasClass('is-checked'); + console.log('%c[index.js, saveOptimize]', 'color: green;', 'Saving optimize state : ' + chosenOptimize); + storeData('optimize', chosenOptimize, null); + }, 100); } function saveFramerate() { - var chosenFramerate = $(this).data('value'); - $('#selectFramerate').text($(this).text()).data('value', chosenFramerate); - storeData('frameRate', chosenFramerate, null); - updateDefaultBitrate(); + var chosenFramerate = $(this).data('value'); + $('#selectFramerate').text($(this).text()).data('value', chosenFramerate); + storeData('frameRate', chosenFramerate, null); + updateDefaultBitrate(); } @@ -769,111 +816,111 @@ function saveFramerate() { // unfortunately, objects with function instances (classes) are stripped of their function instances when converted to a raw object // so we cannot forget to revive the object after we load it. function saveHosts() { - storeData('hosts', hosts, null); + storeData('hosts', hosts, null); } function saveBitrate() { - storeData('bitrate', $('#bitrateSlider').val(), null); + storeData('bitrate', $('#bitrateSlider').val(), null); } function saveRemoteAudio() { - // MaterialDesignLight uses the mouseup trigger, so we give it some time to change the class name before - // checking the new state - setTimeout(function() { - var remoteAudioState = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked'); - console.log('%c[index.js, saveRemoteAudio]', 'color: green;', 'Saving remote audio state : ' + remoteAudioState); - storeData('remoteAudio', remoteAudioState, null); - }, 100); + // MaterialDesignLight uses the mouseup trigger, so we give it some time to change the class name before + // checking the new state + setTimeout(function() { + var remoteAudioState = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked'); + console.log('%c[index.js, saveRemoteAudio]', 'color: green;', 'Saving remote audio state : ' + remoteAudioState); + storeData('remoteAudio', remoteAudioState, null); + }, 100); } function updateDefaultBitrate() { - var res = $('#selectResolution').data('value'); - var frameRate = $('#selectFramerate').data('value').toString(); + var res = $('#selectResolution').data('value'); + var frameRate = $('#selectFramerate').data('value').toString(); - if (res ==="1920:1080") { - if (frameRate === "30") { // 1080p, 30fps - $('#bitrateSlider')[0].MaterialSlider.change('10'); - } else { // 1080p, 60fps - $('#bitrateSlider')[0].MaterialSlider.change('20'); - } - } else if (res === "1280:720") { - if (frameRate === "30") { // 720, 30fps - $('#bitrateSlider')[0].MaterialSlider.change('5'); - } else { // 720, 60fps - $('#bitrateSlider')[0].MaterialSlider.change('10'); - } - } else if (res === "3840:2160") { - if (frameRate === "30") { // 2160p, 30fps - $('#bitrateSlider')[0].MaterialSlider.change('40'); - } else { // 2160p, 60fps - $('#bitrateSlider')[0].MaterialSlider.change('80'); - } - } else { // unrecognized option. In case someone screws with the JS to add custom resolutions - $('#bitrateSlider')[0].MaterialSlider.change('10'); + if (res === "1920:1080") { + if (frameRate === "30") { // 1080p, 30fps + $('#bitrateSlider')[0].MaterialSlider.change('10'); + } else { // 1080p, 60fps + $('#bitrateSlider')[0].MaterialSlider.change('20'); } + } else if (res === "1280:720") { + if (frameRate === "30") { // 720, 30fps + $('#bitrateSlider')[0].MaterialSlider.change('5'); + } else { // 720, 60fps + $('#bitrateSlider')[0].MaterialSlider.change('10'); + } + } else if (res === "3840:2160") { + if (frameRate === "30") { // 2160p, 30fps + $('#bitrateSlider')[0].MaterialSlider.change('40'); + } else { // 2160p, 60fps + $('#bitrateSlider')[0].MaterialSlider.change('80'); + } + } else { // unrecognized option. In case someone screws with the JS to add custom resolutions + $('#bitrateSlider')[0].MaterialSlider.change('10'); + } - updateBitrateField(); - saveBitrate(); + updateBitrateField(); + saveBitrate(); } -function onWindowLoad(){ - console.log('%c[index.js]', 'color: green;', 'Moonlight\'s main window loaded'); - // don't show the game selection div - $('#gameSelection').css('display', 'none'); +function onWindowLoad() { + console.log('%c[index.js]', 'color: green;', 'Moonlight\'s main window loaded'); + // don't show the game selection div + $('#gameSelection').css('display', 'none'); - loadWindowState(); + loadWindowState(); - if(chrome.storage) { - // load stored resolution prefs - chrome.storage.sync.get('resolution', function(previousValue) { - if(previousValue.resolution != null) { - $('.resolutionMenu li').each(function () { - if ($(this).data('value') === previousValue.resolution) { - $('#selectResolution').text($(this).text()).data('value', previousValue.resolution); - } - }); - } + if (chrome.storage) { + // load stored resolution prefs + chrome.storage.sync.get('resolution', function(previousValue) { + if (previousValue.resolution != null) { + $('.resolutionMenu li').each(function() { + if ($(this).data('value') === previousValue.resolution) { + $('#selectResolution').text($(this).text()).data('value', previousValue.resolution); + } }); + } + }); - // Load stored remote audio prefs - chrome.storage.sync.get('remoteAudio', function(previousValue) { - if(previousValue.remoteAudio == null) { - document.querySelector('#externalAudioBtn').MaterialIconToggle.uncheck(); - } else if (previousValue.remoteAudio == false) { - document.querySelector('#externalAudioBtn').MaterialIconToggle.uncheck(); - } else { - document.querySelector('#externalAudioBtn').MaterialIconToggle.check(); - } - }); + // Load stored remote audio prefs + chrome.storage.sync.get('remoteAudio', function(previousValue) { + if (previousValue.remoteAudio == null) { + document.querySelector('#externalAudioBtn').MaterialIconToggle.uncheck(); + } else if (previousValue.remoteAudio == false) { + document.querySelector('#externalAudioBtn').MaterialIconToggle.uncheck(); + } else { + document.querySelector('#externalAudioBtn').MaterialIconToggle.check(); + } + }); - // load stored framerate prefs - chrome.storage.sync.get('frameRate', function(previousValue) { - if(previousValue.frameRate != null) { - $('.framerateMenu li').each(function () { - if ($(this).data('value') === previousValue.frameRate) { - $('#selectFramerate').text($(this).text()).data('value', previousValue.frameRate); - } - }); - } + // load stored framerate prefs + chrome.storage.sync.get('frameRate', function(previousValue) { + if (previousValue.frameRate != null) { + $('.framerateMenu li').each(function() { + if ($(this).data('value') === previousValue.frameRate) { + $('#selectFramerate').text($(this).text()).data('value', previousValue.frameRate); + } }); + } + }); - // load stored optimization prefs - chrome.storage.sync.get('optimize', function(previousValue) { - if (previousValue.optimize == null) { - document.querySelector('#optimizeGamesBtn').MaterialIconToggle.check(); - } else if (previousValue.optimize == false) { - document.querySelector('#optimizeGamesBtn').MaterialIconToggle.uncheck(); - } else { - document.querySelector('#optimizeGamesBtn').MaterialIconToggle.check(); - } - }); + // load stored optimization prefs + chrome.storage.sync.get('optimize', function(previousValue) { + if (previousValue.optimize == null) { + document.querySelector('#optimizeGamesBtn').MaterialIconToggle.check(); + } else if (previousValue.optimize == false) { + document.querySelector('#optimizeGamesBtn').MaterialIconToggle.uncheck(); + } else { + document.querySelector('#optimizeGamesBtn').MaterialIconToggle.check(); + } + }); - // load stored bitrate prefs - chrome.storage.sync.get('bitrate', function(previousValue) { - $('#bitrateSlider')[0].MaterialSlider.change(previousValue.bitrate != null ? previousValue.bitrate : '10'); - updateBitrateField(); - }); - } + // load stored bitrate prefs + chrome.storage.sync.get('bitrate', function(previousValue) { + $('#bitrateSlider')[0].MaterialSlider.change(previousValue.bitrate != null ? previousValue.bitrate : '10'); + updateBitrateField(); + }); + } } diff --git a/static/js/messages.js b/static/js/messages.js index 4533466..9493a90 100644 --- a/static/js/messages.js +++ b/static/js/messages.js @@ -9,66 +9,69 @@ var callbacks_ids = 1; * @return {void} The NaCl module calls back trought the handleMessage method */ var sendMessage = function(method, params) { - return new Promise(function(resolve, reject) { - var id = callbacks_ids++; - callbacks[id] = {'resolve': resolve, 'reject': reject}; + return new Promise(function(resolve, reject) { + var id = callbacks_ids++; + callbacks[id] = { + 'resolve': resolve, + 'reject': reject + }; - common.naclModule.postMessage({ - 'callbackId': id, - 'method': method, - 'params': params - }); + common.naclModule.postMessage({ + 'callbackId': id, + 'method': method, + 'params': params }); + }); } /** * handleMessage - Handles messages from the NaCl module * - * @param {Object} msg An object given by the NaCl module + * @param {Object} msg An object given by the NaCl module * @return {void} */ function handleMessage(msg) { - if (msg.data.callbackId && callbacks[msg.data.callbackId]) { // if it's a callback, treat it as such - callbacks[msg.data.callbackId][msg.data.type](msg.data.ret); - delete callbacks[msg.data.callbackId] - } else { // else, it's just info, or an event - console.log('%c[messages.js, handleMessage]', 'color:gray;', 'Message data: ', msg.data) - if(msg.data === 'streamTerminated') { // if it's a recognized event, notify the appropriate function - $('#loadingSpinner').css('display', 'none'); // This is a fallback for RTSP handshake failing, which immediately terminates the stream. - $('body').css('backgroundColor', '#282C38'); + if (msg.data.callbackId && callbacks[msg.data.callbackId]) { // if it's a callback, treat it as such + callbacks[msg.data.callbackId][msg.data.type](msg.data.ret); + delete callbacks[msg.data.callbackId] + } else { // else, it's just info, or an event + console.log('%c[messages.js, handleMessage]', 'color:gray;', 'Message data: ', msg.data) + if (msg.data === 'streamTerminated') { // if it's a recognized event, notify the appropriate function + $('#loadingSpinner').css('display', 'none'); // This is a fallback for RTSP handshake failing, which immediately terminates the stream. + $('body').css('backgroundColor', '#282C38'); - // Release our keep awake request - chrome.power.releaseKeepAwake(); + // Release our keep awake request + chrome.power.releaseKeepAwake(); - api.refreshServerInfo().then(function (ret) { // refresh the serverinfo to acknowledge the currently running app - api.getAppList().then(function (appList) { - appList.forEach(function (app) { - stylizeBoxArt(api, app.id); // and reapply stylization to indicate what's currently running - }); - }); - showApps(api); + api.refreshServerInfo().then(function(ret) { // refresh the serverinfo to acknowledge the currently running app + api.getAppList().then(function(appList) { + appList.forEach(function(app) { + stylizeBoxArt(api, app.id); // and reapply stylization to indicate what's currently running + }); + }); + showApps(api); - isInGame = false; + isInGame = false; - // restore main window from 'fullscreen' to 'normal' mode (if required) - (windowState == 'normal') && chrome.app.window.current().restore(); - }); + // restore main window from 'fullscreen' to 'normal' mode (if required) + (windowState == 'normal') && chrome.app.window.current().restore(); + }); - } else if(msg.data === 'Connection Established') { - $('#loadingSpinner').css('display', 'none'); - $('body').css('backgroundColor', 'black'); + } else if (msg.data === 'Connection Established') { + $('#loadingSpinner').css('display', 'none'); + $('body').css('backgroundColor', 'black'); - // Keep the display awake while streaming - chrome.power.requestKeepAwake("display"); - } else if(msg.data.indexOf('ProgressMsg: ') === 0) { - $('#loadingMessage').text(msg.data.replace('ProgressMsg: ', '')); - } else if(msg.data.indexOf('TransientMsg: ') === 0) { - snackbarLog(msg.data.replace('TransientMsg: ', '')); - } else if(msg.data.indexOf('DialogMsg: ') === 0) { - // FIXME: Really use a dialog - snackbarLogLong(msg.data.replace('DialogMsg: ', '')); - } else if(msg.data === 'displayVideo') { - $("#listener").addClass("fullscreen"); - } + // Keep the display awake while streaming + chrome.power.requestKeepAwake("display"); + } else if (msg.data.indexOf('ProgressMsg: ') === 0) { + $('#loadingMessage').text(msg.data.replace('ProgressMsg: ', '')); + } else if (msg.data.indexOf('TransientMsg: ') === 0) { + snackbarLog(msg.data.replace('TransientMsg: ', '')); + } else if (msg.data.indexOf('DialogMsg: ') === 0) { + // FIXME: Really use a dialog + snackbarLogLong(msg.data.replace('DialogMsg: ', '')); + } else if (msg.data === 'displayVideo') { + $("#listener").addClass("fullscreen"); } + } } diff --git a/static/js/utils.js b/static/js/utils.js index e29022a..e0d3f83 100644 --- a/static/js/utils.js +++ b/static/js/utils.js @@ -1,468 +1,471 @@ function guuid() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { - var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); - return v.toString(16); - }); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, + v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); } function uniqueid() { - return 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { - var r = Math.random()*16|0; - return r.toString(16); - }); + return 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { + var r = Math.random() * 16 | 0; + return r.toString(16); + }); } function generateRemoteInputKey() { - return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { - var r = Math.random()*16|0; - return r.toString(16); - }); + return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { + var r = Math.random() * 16 | 0; + return r.toString(16); + }); } function generateRemoteInputKeyId() { - return ((Math.random()-0.5) * 0x7FFFFFFF)|0; + return ((Math.random() - 0.5) * 0x7FFFFFFF) | 0; } function getConnectedGamepadMask() { - var count = 0; - var mask = 0; - var gamepads = navigator.getGamepads ? navigator.getGamepads() : []; + var count = 0; + var mask = 0; + var gamepads = navigator.getGamepads ? navigator.getGamepads() : []; - for (var i = 0; i < gamepads.length; i++) { - var gamepad = gamepads[i]; - if (gamepad) { - // See logic in gamepad.cpp - // These must stay in sync! + for (var i = 0; i < gamepads.length; i++) { + var gamepad = gamepads[i]; + if (gamepad) { + // See logic in gamepad.cpp + // These must stay in sync! - if (!gamepad.connected) { - // Not connected - continue; - } + if (!gamepad.connected) { + // Not connected + continue; + } - if (gamepad.timestamp == 0) { - // On some platforms, Chrome returns "connected" pads that - // really aren't, so timestamp stays at zero. To work around this, - // we'll only count gamepads that have a non-zero timestamp in our - // controller index. - continue; - } + if (gamepad.timestamp == 0) { + // On some platforms, Chrome returns "connected" pads that + // really aren't, so timestamp stays at zero. To work around this, + // we'll only count gamepads that have a non-zero timestamp in our + // controller index. + continue; + } - mask |= 1 << count++; - } + mask |= 1 << count++; } + } - console.log('%c[utils.js, getConnectedGamepadMask]', 'color:gray;', 'Detected '+count+' gamepads'); - return mask; + console.log('%c[utils.js, getConnectedGamepadMask]', 'color:gray;', 'Detected ' + count + ' gamepads'); + return mask; } String.prototype.toHex = function() { - var hex = ''; - for(var i = 0; i < this.length; i++) { - hex += '' + this.charCodeAt(i).toString(16); - } - return hex; + var hex = ''; + for (var i = 0; i < this.length; i++) { + hex += '' + this.charCodeAt(i).toString(16); + } + return hex; } function NvHTTP(address, clientUid, userEnteredAddress = '') { - console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this); - this.address = address; - this.paired = false; - this.currentGame = 0; - this.serverMajorVersion = 0; - this.appVersion = ''; - this.clientUid = clientUid; - this._pollCount = 0; - this._consecutivePollFailures = 0; - this.online = false; + console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this); + this.address = address; + this.paired = false; + this.currentGame = 0; + this.serverMajorVersion = 0; + this.appVersion = ''; + this.clientUid = clientUid; + this._pollCount = 0; + this._consecutivePollFailures = 0; + this.online = false; - this.userEnteredAddress = userEnteredAddress; // if the user entered an address, we keep it on hand to try when polling - this.serverUid = ''; - this.GfeVersion = ''; - this.supportedDisplayModes = {}; // key: y-resolution:x-resolution, value: array of supported framerates (only ever seen 30 or 60, here) - this.gputype = ''; - this.numofapps = 0; - this.hostname = address; - this.externalIP = ''; - this._pollCompletionCallbacks = []; + this.userEnteredAddress = userEnteredAddress; // if the user entered an address, we keep it on hand to try when polling + this.serverUid = ''; + this.GfeVersion = ''; + this.supportedDisplayModes = {}; // key: y-resolution:x-resolution, value: array of supported framerates (only ever seen 30 or 60, here) + this.gputype = ''; + this.numofapps = 0; + this.hostname = address; + this.externalIP = ''; + this._pollCompletionCallbacks = []; - _self = this; + _self = this; }; -function _arrayBufferToBase64( buffer ) { - var binary = ''; - var bytes = new Uint8Array( buffer ); - var len = bytes.byteLength; - for (var i = 0; i < len; i++) { - binary += String.fromCharCode( bytes[ i ] ); - } - return window.btoa( binary ); +function _arrayBufferToBase64(buffer) { + var binary = ''; + var bytes = new Uint8Array(buffer); + var len = bytes.byteLength; + for (var i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); } function _base64ToArrayBuffer(base64) { - var binary_string = window.atob(base64); - var len = binary_string.length; - var bytes = new Uint8Array( len ); - for (var i = 0; i < len; i++) { - bytes[i] = binary_string.charCodeAt(i); - } - return bytes.buffer; + var binary_string = window.atob(base64); + var len = binary_string.length; + var bytes = new Uint8Array(len); + for (var i = 0; i < len; i++) { + bytes[i] = binary_string.charCodeAt(i); + } + return bytes.buffer; } NvHTTP.prototype = { - refreshServerInfo: function () { - // try HTTPS first - return sendMessage('openUrl', [ this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { - if (!this._parseServerInfo(ret)) { // if that fails - // try HTTP as a failover. Useful to clients who aren't paired yet - return sendMessage('openUrl', [ this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { - this._parseServerInfo(retHttp); - }.bind(this)); - } + refreshServerInfo: function() { + // try HTTPS first + return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { + if (!this._parseServerInfo(ret)) { // if that fails + // try HTTP as a failover. Useful to clients who aren't paired yet + return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { + this._parseServerInfo(retHttp); }.bind(this)); - }, + } + }.bind(this)); + }, - // refreshes the server info using a given address. This is useful for testing whether we can successfully ping a host at a given address - refreshServerInfoAtAddress: function(givenAddress) { - // try HTTPS first - return sendMessage('openUrl', [ 'https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { - if (!this._parseServerInfo(ret)) { // if that fails - console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP'); - // try HTTP as a failover. Useful to clients who aren't paired yet - return sendMessage('openUrl', [ 'http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { - return this._parseServerInfo(retHttp); - }.bind(this)); - } + // refreshes the server info using a given address. This is useful for testing whether we can successfully ping a host at a given address + refreshServerInfoAtAddress: function(givenAddress) { + // try HTTPS first + return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { + if (!this._parseServerInfo(ret)) { // if that fails + console.log('%c[utils.js, utils.js, refreshServerInfoAtAddress]', 'color: gray;', 'Failed to parse serverinfo from HTTPS, falling back to HTTP'); + // try HTTP as a failover. Useful to clients who aren't paired yet + return sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) { + return this._parseServerInfo(retHttp); }.bind(this)); - }, + } + }.bind(this)); + }, - // called every few seconds to poll the server for updated info - pollServer: function(onComplete) { - // Pend this callback on completion - this._pollCompletionCallbacks.push(onComplete); + // called every few seconds to poll the server for updated info + pollServer: function(onComplete) { + // Pend this callback on completion + this._pollCompletionCallbacks.push(onComplete); - // Check if a poll was already in progress - if (this._pollCompletionCallbacks.length > 1) { - // Don't start another. The one in progress will - // alert our caller too. + // Check if a poll was already in progress + if (this._pollCompletionCallbacks.length > 1) { + // Don't start another. The one in progress will + // alert our caller too. + return; + } + + this.selectServerAddress(function(successfulAddress) { + // Successfully determined server address. Update base URL. + this.address = successfulAddress; + this._baseUrlHttps = 'https://' + successfulAddress + ':47984'; + this._baseUrlHttp = 'http://' + successfulAddress + ':47989'; + + // Poll for the app list every 10 successful serverinfo polls. + // Not including the first one to avoid PCs taking a while to show + // as online initially + if (this._pollCount++ % 10 == 1) { + this.getAppListWithCacheFlush(); + } + + this._consecutivePollFailures = 0; + this.online = true; + + // Call all pending completion callbacks + var completion; + while ((completion = this._pollCompletionCallbacks.pop())) { + completion(this); + } + }.bind(this), function() { + if (++this._consecutivePollFailures >= 2) { + this.online = false; + } + + // Call all pending completion callbacks + var completion; + while ((completion = this._pollCompletionCallbacks.pop())) { + completion(this); + } + }.bind(this)); + }, + + // initially pings the server to try and figure out if it's routable by any means. + selectServerAddress: function(onSuccess, onFailure) { + // TODO: Deduplicate the addresses + this.refreshServerInfoAtAddress(this.address).then(function(successPrevAddr) { + onSuccess(this.address); + }.bind(this), function(successPrevAddr) { + this.refreshServerInfoAtAddress(this.hostname + '.local').then(function(successLocal) { + onSuccess(this.hostname + '.local'); + }.bind(this), function(failureLocal) { + this.refreshServerInfoAtAddress(this.externalIP).then(function(successExternal) { + onSuccess(this.externalIP); + }.bind(this), function(failureExternal) { + this.refreshServerInfoAtAddress(this.userEnteredAddress).then(function(successUserEntered) { + onSuccess(this.userEnteredAddress); + }.bind(this), function(failureUserEntered) { + console.warn('%c[utils.js, utils.js, selectServerAddress]', 'color: gray;', 'Failed to contact host ' + this.hostname, this); + onFailure(); + }.bind(this)); + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + toString: function() { + var string = ''; + string += 'server address: ' + this.address + '\r\n'; + string += 'server UID: ' + this.serverUid + '\r\n'; + string += 'is paired: ' + this.paired + '\r\n'; + string += 'current game: ' + this.currentGame + '\r\n'; + string += 'server major version: ' + this.serverMajorVersion + '\r\n'; + string += 'appversion: ' + this.appVersion + '\r\n'; + string += 'GFE version: ' + this.GfeVersion + '\r\n'; + string += 'gpu type: ' + this.gputype + '\r\n'; + string += 'number of apps: ' + this.numofapps + '\r\n'; + string += 'supported display modes: ' + '\r\n'; + for (var displayMode in this.supportedDisplayModes) { + string += '\t' + displayMode + ': ' + this.supportedDisplayModes[displayMode] + '\r\n'; + } + return string; + }, + + _parseServerInfo: function(xmlStr) { + $xml = this._parseXML(xmlStr); + $root = $xml.find('root'); + + if ($root.attr("status_code") != 200) { + return false; + } + + if (this.serverUid != $root.find('uniqueid').text().trim() && this.serverUid != "") { + // if we received a UID that isn't the one we expected, fail. + return false; + } + + console.log('%c[utils.js, _parseServerInfo]', 'color:gray;', 'Parsing server info:', $root); + + this.paired = $root.find("PairStatus").text().trim() == 1; + this.currentGame = parseInt($root.find("currentgame").text().trim(), 10); + this.appVersion = $root.find("appversion").text().trim(); + this.serverMajorVersion = parseInt(this.appVersion.substring(0, 1), 10); + this.serverUid = $root.find('uniqueid').text().trim(); + this.hostname = $root.find('hostname').text().trim(); + this.externalIP = $root.find('ExternalIP').text().trim(); + try { // these aren't critical for functionality, and don't necessarily exist in older GFE versions. + this.GfeVersion = $root.find('GfeVersion').text().trim(); + this.gputype = $root.find('gputype').text().trim(); + this.numofapps = $root.find('numofapps').text().trim(); + // now for the hard part: parsing the supported streaming + $root.find('DisplayMode').each(function(index, value) { // for each resolution:FPS object + var yres = parseInt($(value).find('Height').text()); + var xres = parseInt($(value).find('Width').text()); + var fps = parseInt($(value).find('RefreshRate').text()); + if (!this.supportedDisplayModes[yres + ':' + xres]) { + this.supportedDisplayModes[yres + ':' + xres] = []; + } + if (!this.supportedDisplayModes[yres + ':' + xres].includes(fps)) { + this.supportedDisplayModes[yres + ':' + xres].push(fps); + } + }.bind(this)); + } catch (err) { + // we don't need this data, so no error handling necessary + } + + + // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer + // has the semantics that its name would indicate. To contain the effects of this change as much + // as possible, we'll force the current game to zero if the server isn't in a streaming session. + if (!$root.find("state").text().trim().endsWith("_SERVER_BUSY")) { + this.currentGame = 0; + } + + return true; + }, + + getAppById: function(appId) { + return this.getAppList().then(function(list) { + var retApp = null; + + list.some(function(app) { + if (app.id == appId) { + retApp = app; + return true; + } + + return false; + }); + + return retApp; + }); + }, + + getAppByName: function(appName) { + return this.getAppList().then(function(list) { + var retApp = null; + + list.some(function(app) { + if (app.title == appName) { + retApp = app; + return true; + } + + return false; + }); + + return retApp; + }); + }, + + getAppListWithCacheFlush: function() { + return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), false]).then(function(ret) { + $xml = this._parseXML(ret); + $root = $xml.find("root"); + + if ($root.attr("status_code") != 200) { + // TODO: Bubble up an error here + console.error('%c[utils.js, utils.js, getAppListWithCacheFlush]', 'color: gray;', 'Applist request failed', $root.attr("status_code")); + return []; + } + + var rootElement = $xml.find("root")[0]; + var appElements = rootElement.getElementsByTagName("App"); + var appList = []; + + for (var i = 0, len = appElements.length; i < len; i++) { + appList.push({ + title: appElements[i].getElementsByTagName("AppTitle")[0].innerHTML.trim(), + id: parseInt(appElements[i].getElementsByTagName("ID")[0].innerHTML.trim(), 10) + }); + } + + this._memCachedApplist = appList; + + return appList; + }.bind(this)); + }, + + getAppList: function() { + if (this._memCachedApplist) { + return new Promise(function(resolve, reject) { + console.log('%c[utils.js, utils.js]', 'color: gray;', 'Returning memory-cached apps list'); + resolve(this._memCachedApplist); + return; + }.bind(this)); + } + + return this.getAppListWithCacheFlush(); + }, + + // returns the box art of the given appID. + // three layers of response time are possible: memory cached (in javascript), storage cached (in chrome.storage.local), and streamed (host sends binary over the network) + getBoxArt: function(appId) { + if (chrome.storage) { + // This may be bad practice to push/pull this much data through local storage? + return new Promise(function(resolve, reject) { + chrome.storage.local.get('boxart-' + appId, function(storageData) { + // if we already have it, load it. + if (storageData !== undefined && Object.keys(storageData).length !== 0 && storageData['boxart-' + appId].constructor !== Object) { + console.log('%c[utils.js, getBoxArt]', 'color: gray;', 'Returning storage-cached box art for app: ', appId); + resolve(storageData['boxart-' + appId]); return; - } + } - this.selectServerAddress(function(successfulAddress) { - // Successfully determined server address. Update base URL. - this.address = successfulAddress; - this._baseUrlHttps = 'https://' + successfulAddress + ':47984'; - this._baseUrlHttp = 'http://' + successfulAddress + ':47989'; - - // Poll for the app list every 10 successful serverinfo polls. - // Not including the first one to avoid PCs taking a while to show - // as online initially - if (this._pollCount++ % 10 == 1) { - this.getAppListWithCacheFlush(); - } - - this._consecutivePollFailures = 0; - this.online = true; - - // Call all pending completion callbacks - var completion; - while ((completion = this._pollCompletionCallbacks.pop())) { - completion(this); - } - }.bind(this), function() { - if (++this._consecutivePollFailures >= 2) { - this.online = false; - } - - // Call all pending completion callbacks - var completion; - while ((completion = this._pollCompletionCallbacks.pop())) { - completion(this); - } - }.bind(this)); - }, - - // initially pings the server to try and figure out if it's routable by any means. - selectServerAddress: function(onSuccess, onFailure) { - // TODO: Deduplicate the addresses - this.refreshServerInfoAtAddress(this.address).then(function(successPrevAddr) { - onSuccess(this.address); - }.bind(this), function(successPrevAddr) { - this.refreshServerInfoAtAddress(this.hostname + '.local').then(function(successLocal) { - onSuccess(this.hostname + '.local'); - }.bind(this), function(failureLocal) { - this.refreshServerInfoAtAddress(this.externalIP).then(function(successExternal) { - onSuccess(this.externalIP); - }.bind(this), function(failureExternal) { - this.refreshServerInfoAtAddress(this.userEnteredAddress).then(function(successUserEntered) { - onSuccess(this.userEnteredAddress); - }.bind(this), function(failureUserEntered) { - console.warn('%c[utils.js, utils.js, selectServerAddress]', 'color: gray;', 'Failed to contact host ' + this.hostname, this); - onFailure(); - }.bind(this)); - }.bind(this)); - }.bind(this)); - }.bind(this)); - }, - - toString: function() { - var string = ''; - string += 'server address: ' + this.address + '\r\n'; - string += 'server UID: ' + this.serverUid + '\r\n'; - string += 'is paired: ' + this.paired + '\r\n'; - string += 'current game: ' + this.currentGame + '\r\n'; - string += 'server major version: ' + this.serverMajorVersion + '\r\n'; - string += 'appversion: ' + this.appVersion + '\r\n'; - string += 'GFE version: ' + this.GfeVersion + '\r\n'; - string += 'gpu type: ' + this.gputype + '\r\n'; - string += 'number of apps: ' + this.numofapps + '\r\n'; - string += 'supported display modes: ' + '\r\n'; - for(var displayMode in this.supportedDisplayModes) { - string += '\t' + displayMode + ': ' + this.supportedDisplayModes[displayMode] + '\r\n'; - } - return string; - }, - - _parseServerInfo: function(xmlStr) { - $xml = this._parseXML(xmlStr); - $root = $xml.find('root'); - - if($root.attr("status_code") != 200) { - return false; - } - - if(this.serverUid != $root.find('uniqueid').text().trim() && this.serverUid != "") { - // if we received a UID that isn't the one we expected, fail. - return false; - } - - console.log('%c[utils.js, _parseServerInfo]', 'color:gray;', 'Parsing server info:', $root); - - this.paired = $root.find("PairStatus").text().trim() == 1; - this.currentGame = parseInt($root.find("currentgame").text().trim(), 10); - this.appVersion = $root.find("appversion").text().trim(); - this.serverMajorVersion = parseInt(this.appVersion.substring(0, 1), 10); - this.serverUid = $root.find('uniqueid').text().trim(); - this.hostname = $root.find('hostname').text().trim(); - this.externalIP = $root.find('ExternalIP').text().trim(); - try { // these aren't critical for functionality, and don't necessarily exist in older GFE versions. - this.GfeVersion = $root.find('GfeVersion').text().trim(); - this.gputype = $root.find('gputype').text().trim(); - this.numofapps = $root.find('numofapps').text().trim(); - // now for the hard part: parsing the supported streaming - $root.find('DisplayMode').each(function(index, value) { // for each resolution:FPS object - var yres = parseInt($(value).find('Height').text()); - var xres = parseInt($(value).find('Width').text()); - var fps = parseInt($(value).find('RefreshRate').text()); - if(!this.supportedDisplayModes[yres + ':' + xres]) { - this.supportedDisplayModes[yres + ':' + xres] = []; - } - if(!this.supportedDisplayModes[yres + ':' + xres].includes(fps)) { - this.supportedDisplayModes[yres + ':' + xres].push(fps); - } - }.bind(this)); - } catch (err) { - // we don't need this data, so no error handling necessary - } - - - // GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer - // has the semantics that its name would indicate. To contain the effects of this change as much - // as possible, we'll force the current game to zero if the server isn't in a streaming session. - if (!$root.find("state").text().trim().endsWith("_SERVER_BUSY")) { - this.currentGame = 0; - } - - return true; - }, - - getAppById: function (appId) { - return this.getAppList().then(function (list) { - var retApp = null; - - list.some(function (app) { - if (app.id == appId) { - retApp = app; - return true; - } - - return false; - }); - - return retApp; - }); - }, - - getAppByName: function (appName) { - return this.getAppList().then(function (list) { - var retApp = null; - - list.some(function (app) { - if (app.title == appName) { - retApp = app; - return true; - } - - return false; - }); - - return retApp; - }); - }, - - getAppListWithCacheFlush: function () { - return sendMessage('openUrl', [this._baseUrlHttps + '/applist?' + this._buildUidStr(), false]).then(function (ret) { - $xml = this._parseXML(ret); - $root = $xml.find("root"); - - if ($root.attr("status_code") != 200) { - // TODO: Bubble up an error here - console.error('%c[utils.js, utils.js, getAppListWithCacheFlush]', 'color: gray;', 'Applist request failed', $root.attr("status_code")); - return []; - } - - var rootElement = $xml.find("root")[0]; - var appElements = rootElement.getElementsByTagName("App"); - var appList = []; - - for (var i = 0, len = appElements.length; i < len; i++) { - appList.push({ - title: appElements[i].getElementsByTagName("AppTitle")[0].innerHTML.trim(), - id: parseInt(appElements[i].getElementsByTagName("ID")[0].innerHTML.trim(), 10) - }); - } - - this._memCachedApplist = appList; - - return appList; - }.bind(this)); - }, - - getAppList: function () { - if (this._memCachedApplist) { - return new Promise(function (resolve, reject) { - console.log('%c[utils.js, utils.js]', 'color: gray;', 'Returning memory-cached apps list'); - resolve(this._memCachedApplist); - return; - }.bind(this)); - } - - return this.getAppListWithCacheFlush(); - }, - - // returns the box art of the given appID. - // three layers of response time are possible: memory cached (in javascript), storage cached (in chrome.storage.local), and streamed (host sends binary over the network) - getBoxArt: function (appId) { - if (chrome.storage) { - // This may be bad practice to push/pull this much data through local storage? - return new Promise(function (resolve, reject) { - chrome.storage.local.get('boxart-'+appId, function(storageData) { - // if we already have it, load it. - if (storageData !== undefined && Object.keys(storageData).length !== 0 && storageData['boxart-'+appId].constructor !== Object) { - console.log('%c[utils.js, getBoxArt]', 'color: gray;', 'Returning storage-cached box art for app: ', appId); - resolve(storageData['boxart-'+appId]); - return; - } - - // otherwise, put it in our cache, then return it - sendMessage('openUrl', [ - this._baseUrlHttps + - '/appasset?'+this._buildUidStr() + - '&appid=' + appId + - '&AssetType=2&AssetIdx=0', - true - ]).then(function(boxArtBuffer) { - var reader = new FileReader(); - reader.onloadend = function() { - var obj = {}; - obj['boxart-'+appId] = this.result; - chrome.storage.local.set(obj, function(onSuccess) {}); - console.log('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Returning network-fetched box art'); - resolve(this.result); - } - reader.readAsDataURL(new Blob([boxArtBuffer], {type: "image/png"})); - }.bind(this), function(error) { - console.error('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Box-art request failed!', error); - reject(error); - return; - }.bind(this)); - }.bind(this)); - }.bind(this)); - - } else { // shouldn't run because we always have chrome.storage, but I'm not going to antagonize other browsers - console.warn('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'chrome.storage not detected! Box art will not be saved!'); - return sendMessage('openUrl', [ - this._baseUrlHttps + - '/appasset?'+this._buildUidStr() + - '&appid=' + appId + - '&AssetType=2&AssetIdx=0', - true - ]); - } - }, - - launchApp: function (appId, mode, sops, rikey, rikeyid, localAudio, surroundAudioInfo, gamepadMask) { - return sendMessage('openUrl', [ + // otherwise, put it in our cache, then return it + sendMessage('openUrl', [ this._baseUrlHttps + - '/launch?' + this._buildUidStr() + + '/appasset?' + this._buildUidStr() + '&appid=' + appId + - '&mode=' + mode + - '&additionalStates=1&sops=' + sops + - '&rikey=' + rikey + - '&rikeyid=' + rikeyid + - '&localAudioPlayMode=' + localAudio + - '&surroundAudioInfo=' + surroundAudioInfo + - '&remoteControllersBitmap=' + gamepadMask + - '&gcmap=' + gamepadMask, - false - ]).then(function (ret) { - return true; - }); - }, - - resumeApp: function (rikey, rikeyid, surroundAudioInfo) { - return sendMessage('openUrl', [ - this._baseUrlHttps + - '/resume?' + this._buildUidStr() + - '&rikey=' + rikey + - '&rikeyid=' + rikeyid + - '&surroundAudioInfo=' + surroundAudioInfo, - false - ]).then(function (ret) { - return true; - }); - }, - - quitApp: function () { - return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), false]) - // Refresh server info after quitting because it may silently fail if the - // session belongs to a different client. - // TODO: We should probably bubble this up to our caller. - .then(this.refreshServerInfo()); - }, - - pair: function(randomNumber) { - return this.refreshServerInfo().then(function () { - if (this.paired) - return true; - - if (this.currentGame != 0) - return false; - - return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function (pairStatus) { - return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', false]).then(function (ret) { - $xml = this._parseXML(ret); - this.paired = $xml.find('paired').html() == "1"; - return this.paired; - }.bind(this)); - }.bind(this)); + '&AssetType=2&AssetIdx=0', + true + ]).then(function(boxArtBuffer) { + var reader = new FileReader(); + reader.onloadend = function() { + var obj = {}; + obj['boxart-' + appId] = this.result; + chrome.storage.local.set(obj, function(onSuccess) {}); + console.log('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Returning network-fetched box art'); + resolve(this.result); + } + reader.readAsDataURL(new Blob([boxArtBuffer], { + type: "image/png" + })); + }.bind(this), function(error) { + console.error('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Box-art request failed!', error); + reject(error); + return; + }.bind(this)); }.bind(this)); - }, + }.bind(this)); - _buildUidStr: function () { - return 'uniqueid=' + this.clientUid + '&uuid=' + guuid(); - }, + } else { // shouldn't run because we always have chrome.storage, but I'm not going to antagonize other browsers + console.warn('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'chrome.storage not detected! Box art will not be saved!'); + return sendMessage('openUrl', [ + this._baseUrlHttps + + '/appasset?' + this._buildUidStr() + + '&appid=' + appId + + '&AssetType=2&AssetIdx=0', + true + ]); + } + }, - _parseXML: function (xmlData) { - return $($.parseXML(xmlData.toString())); - }, + launchApp: function(appId, mode, sops, rikey, rikeyid, localAudio, surroundAudioInfo, gamepadMask) { + return sendMessage('openUrl', [ + this._baseUrlHttps + + '/launch?' + this._buildUidStr() + + '&appid=' + appId + + '&mode=' + mode + + '&additionalStates=1&sops=' + sops + + '&rikey=' + rikey + + '&rikeyid=' + rikeyid + + '&localAudioPlayMode=' + localAudio + + '&surroundAudioInfo=' + surroundAudioInfo + + '&remoteControllersBitmap=' + gamepadMask + + '&gcmap=' + gamepadMask, + false + ]).then(function(ret) { + return true; + }); + }, + + resumeApp: function(rikey, rikeyid, surroundAudioInfo) { + return sendMessage('openUrl', [ + this._baseUrlHttps + + '/resume?' + this._buildUidStr() + + '&rikey=' + rikey + + '&rikeyid=' + rikeyid + + '&surroundAudioInfo=' + surroundAudioInfo, + false + ]).then(function(ret) { + return true; + }); + }, + + quitApp: function() { + return sendMessage('openUrl', [this._baseUrlHttps + '/cancel?' + this._buildUidStr(), false]) + // Refresh server info after quitting because it may silently fail if the + // session belongs to a different client. + // TODO: We should probably bubble this up to our caller. + .then(this.refreshServerInfo()); + }, + + pair: function(randomNumber) { + return this.refreshServerInfo().then(function() { + if (this.paired) + return true; + + if (this.currentGame != 0) + return false; + + return sendMessage('pair', [this.serverMajorVersion.toString(), this.address, randomNumber]).then(function(pairStatus) { + return sendMessage('openUrl', [this._baseUrlHttps + '/pair?uniqueid=' + this.clientUid + '&devicename=roth&updateState=1&phrase=pairchallenge', false]).then(function(ret) { + $xml = this._parseXML(ret); + this.paired = $xml.find('paired').html() == "1"; + return this.paired; + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + _buildUidStr: function() { + return 'uniqueid=' + this.clientUid + '&uuid=' + guuid(); + }, + + _parseXML: function(xmlData) { + return $($.parseXML(xmlData.toString())); + }, }; From fe06f6d8fffa1413d0e67cb39e35c7d4bca4bfa1 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 24 Apr 2018 12:46:45 +0200 Subject: [PATCH 05/23] Fixed html identation --- index.html | 251 ++++++++++++++++++++++++++--------------------------- 1 file changed, 125 insertions(+), 126 deletions(-) diff --git a/index.html b/index.html index 7fde1e6..8554e33 100644 --- a/index.html +++ b/index.html @@ -1,181 +1,180 @@ - - - - Moonlight - - - - + + + + Moonlight + + + + -
+
-
-
-
-

Add Host

-
-
-
-
-
- -
-
-
-
-
-
+
+
+
+

Add Host

+
+
+
+
+
+ +
+
+
+
+
+
-
- - - - - - - - - +
+ + + + + + + + +

Pairing

-

- Please enter the number XXXX on the GFE dialog on the computer. This dialog will be dismissed once complete -

+

+ Please enter the number XXXX on the GFE dialog on the computer. This dialog will be dismissed once complete +

- +
-
- + +

Quit Running App?

-

- Y is already running. Would you like to quit Y? -

+

+ Y is already running. Would you like to quit Y? +

- - + +
-
- + +

Delete PC

-

- Are you sure you want to delete this host? -

+

+ Are you sure you want to delete this host? +

- - + +
-
- + +

Add Host Manually

-
- - -
+
+ + +
- - + +
-
-
+ +
- -
+ + +
From 78c32035f804c68687e697103d8c59e28365f52f Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Thu, 26 Apr 2018 13:50:30 +0200 Subject: [PATCH 06/23] Added fade-in for images --- static/css/style.css | 6 ++++++ static/js/index.js | 9 ++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index da19daa..fa9685c 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -205,6 +205,12 @@ main { #game-grid .mdl-card img { height: 100%; width: 100%; + opacity: 0; + transition: opacity .3s; + z-index: -1; +} +#game-grid .mdl-card img.fade-in { + opacity: 1; } #game-grid .game-title { position: absolute; diff --git a/static/js/index.js b/static/js/index.js index 2f80823..338bcc8 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -537,11 +537,10 @@ function showApps(host) { } host.getBoxArt(app.id).then(function(resolvedPromise) { // put the box art into the image holder - $(outerDiv).append($("", { - src: resolvedPromise, - name: app.title - })); - + var img = new Image(); + img.src = resolvedPromise; + img.onload = e => img.classList.add('fade-in'); + $(outerDiv).append(img); }, function(failedPromise) { console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); $(outerDiv).append($("", { From 492ee0dc613253ea920cfbcd38bf402b15608b89 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 1 May 2018 09:50:51 +0200 Subject: [PATCH 07/23] Added message when gamelist empty --- static/js/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/js/index.js b/static/js/index.js index 338bcc8..258389d 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -493,6 +493,11 @@ function showApps(host) { $('#naclSpinner').css('display', 'inline-block'); host.getAppList().then(function(appList) { + if(appList.length == 0) { // TODO: Add placeholder graphic + console.error('%c[index.js, showApps]', 'User\'s applist is empty') + snackbarLog('Your game list is empty') + return; // We stop the function right here + } // if game grid is populated, empty it $("div.game-container").remove(); From 447ab4a74c8cbb5824ea9781414e1301ac98e0d3 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 1 May 2018 22:25:25 +0200 Subject: [PATCH 08/23] Simplified the stylizeBoxArt function --- static/js/index.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 258389d..486d236 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -436,17 +436,9 @@ function removeClicked(host) { // the function was made like this so that we can remove duplicated code, but // not do N*N stylizations of the box art, or make the code not flow very well function stylizeBoxArt(freshApi, appIdToStylize) { - if (freshApi.currentGame === appIdToStylize) { // stylize the currently running game - // destylize it, if it has the not-current-game style - if ($('#game-' + appIdToStylize).hasClass("not-current-game")) $('#game-' + appIdToStylize).removeClass("not-current-game"); - // add the current-game style - $('#game-' + appIdToStylize).addClass("current-game"); - } else { - // destylize it, if it has the current-game style - if ($('#game-' + appIdToStylize).hasClass("current-game")) $('#game-' + appIdToStylize).removeClass("current-game"); - // add the not-current-game style - $('#game-' + appIdToStylize).addClass('not-current-game'); - } + // If the running game is the good one then style it + var el = document.querySelector("#game-" + appIdToStylize); + return (freshApi.currentGame === appIdToStylize) ? el.classList.add('current-game') : el.classList.remove('current-game') } function sortTitles(list, sortOrder) { From 8f5f383ead5400e5346336e0232d6613d8a53664 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Wed, 2 May 2018 10:44:26 +0200 Subject: [PATCH 09/23] Simplifed options text --- index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 8554e33..ba03ad6 100644 --- a/index.html +++ b/index.html @@ -75,7 +75,7 @@ volume_up
- Play audio on the host PC speakers + Play audio on the host
@@ -85,7 +85,7 @@ timeline
- Allow GeForce Experience to optimize game settings for streaming + Allow game optimisations
From 29a105d0168c8f76f29af0688cde90a78beef709 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Thu, 3 May 2018 13:28:31 +0200 Subject: [PATCH 10/23] Replaced error box art with new one + lazyload --- static/js/index.js | 12 ++++-------- static/res/no_app_image.png | Bin 1622 -> 0 bytes static/res/placeholder_error.svg | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 static/res/no_app_image.png create mode 100644 static/res/placeholder_error.svg diff --git a/static/js/index.js b/static/js/index.js index 486d236..9e92e70 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -532,19 +532,15 @@ function showApps(host) { // apply CSS stylization to indicate whether the app is active stylizeBoxArt(host, app.id); } + var img = new Image(); host.getBoxArt(app.id).then(function(resolvedPromise) { - // put the box art into the image holder - var img = new Image(); img.src = resolvedPromise; - img.onload = e => img.classList.add('fade-in'); - $(outerDiv).append(img); }, function(failedPromise) { console.log('%c[index.js, showApps]', 'color: green;', 'Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise, '\n Host object:', host, host.toString()); - $(outerDiv).append($("", { - src: "static/res/no_app_image.png", - name: app.title - })); + img.src = 'static/res/placeholder_error.svg' }); + img.onload = e => img.classList.add('fade-in'); + $(outerDiv).append(img); }); }, function(failedAppList) { $('#naclSpinner').hide(); diff --git a/static/res/no_app_image.png b/static/res/no_app_image.png deleted file mode 100644 index 4cbad3e0439718265ea3c96656475d0c83fd4a49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1622 zcma)7eKb^Q93Iz=>-!ePwxb;nth!;%kJrq-E+@--+Q0)d!FC>d*9!E?&&=q zbREt0ngjwthq24W8_Z$?K}8c$15f^X>wg53+97wk3qiSbUAbNQ13)yQcLl~02wM6} zLxqr=Zv;T~?-)#)`mlyEOwx$R9IPP_R>>GHpMR0iuNYKZ$lYa>)FXK~q>LMR;7UVt zJ!vr~BAL(lj&^y~UeEKJ7++~W^kEL@+-T%19@7ZvG2{_o2vWh5FpNY(Q3QcuXleWx z^jGA+qL!gM#(OLMwq&vvM)%y=VSUMcD)E!Jpf<0KM$(=;y#t_X)3;;E( zawqAyTdyt6&7xnhSTd=JPjsgoOwIBp>hkoIWJt=smIX8%FL%XH=D;mMomQf%`i`zm zsFRB%?!5P$bS)d7$ZF>=bTjHOVMj-q#P=254CSQlxY=%nZg^nJyg59soJ}sogb}5^ z7H5itX|-B-w1Y0sIV<#|HrZ{{fm@hhwV+Ue!soc}W|Z?d^|fg2NDa>feZTe|r|GO9%2*qZj%pgF z_uA@mv232y+l|y|UE=NLC8-7UTCgeQ<(~6#l~};TxS*`icj11Gt6J){ThaN&_gBY% zL6N2>uM^UX%nLdCSFzaxh0`2ue8MMq{Asr`>9PhE~YPvU}Rm@%kniqA1wS85~x6^YW@FgEe z+AfVwrFKP`8z7Pg&tpf9vcFHovMb6hz1_g_h4z`OKlo%1>r%KSzM{|aOwmLVj8o#N z8(4J%Ib1DnM4`h*bjZk#Pp*;qkvi!aMdp!Y10=SU;!^nBRR^bpeI~*Jx@QuBurdqF z?y`GBt&@YuxFAECmysQu?Bz^qXm!jyADYt_jZ^$3aao}+F@*bWqwmGgR}xSNuhoQ4 zwpne6#10Mcuywn84f5{aD?WmX^J{hB!z@$wj1?3Z?R1${KPbAklN9^N9u;S}mk+WH zTPcq7$Vjfj6d6eoL=~HBh7s}3z*%RK!`R0I3j55qVw()Cs=%Z4w@CX9SCX(X(Dc-< zf*|-%BaRJ)1HE`vyc2Aie7b*54a$+vI~}PnusMy5we!?)GTxQm?F;--Wry-9lF&l5dzjhBh!wp zVK5-cFp@*S(oHz8hd&8XL2>fLUK{w0E77lQwK&7kK989$dr>w?XEvrx;jlWakAn_gugWucp)?=uK*QapmyEv z(ZIDsWj{>?22e~_)HPA \ No newline at end of file From d062346db2e310eee6a35ae0ea0aa3df4cb9beab Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Thu, 3 May 2018 16:06:54 +0200 Subject: [PATCH 11/23] Editied the album placeholder --- static/css/style.css | 2 +- static/res/placeholder_game.svg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 static/res/placeholder_game.svg diff --git a/static/css/style.css b/static/css/style.css index fa9685c..e717391 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -178,7 +178,7 @@ main { } #game-grid .mdl-card { position: relative; - background: url(/icons/icon128.png) rgba(0, 0, 0, 0.5) center no-repeat; + background: url('../res/placeholder_game.svg') rgba(29, 29, 29, 1) center/cover no-repeat; } #host-grid .mdl-card, #game-grid .mdl-card { text-align: center; diff --git a/static/res/placeholder_game.svg b/static/res/placeholder_game.svg new file mode 100644 index 0000000..916a547 --- /dev/null +++ b/static/res/placeholder_game.svg @@ -0,0 +1 @@ + \ No newline at end of file From c15ddb85979f38dee8814f4fab74ba4148038c0a Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Fri, 4 May 2018 18:35:19 +0200 Subject: [PATCH 12/23] Added error when failed to get gamelist --- static/js/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 9e92e70..fb05cad 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -543,9 +543,9 @@ function showApps(host) { $(outerDiv).append(img); }); }, function(failedAppList) { - $('#naclSpinner').hide(); - - console.log('%c[index.js, showApps]', 'color: green;', 'Failed to get applist from host: ' + host.hostname, '\n Host object:', host, host.toString()); + $('#naclSpinner').hide(); // TODO: Add placeholder graphic + snackbarLog('Unable to get your games') + console.error('%c[index.js, showApps]', 'Failed to get applist from host: ' + host.hostname, '\n Host object:', host, host.toString()); }); showAppsMode(); From 3234ffd4eae4d48ac37bebc9586c046c890622e6 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sat, 5 May 2018 16:52:25 +0200 Subject: [PATCH 13/23] Added graphic when gamelist error --- static/js/index.js | 8 ++++++-- static/res/applist_error.svg | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 static/res/applist_error.svg diff --git a/static/js/index.js b/static/js/index.js index fb05cad..078f128 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -484,6 +484,8 @@ function showApps(host) { $('#naclSpinnerMessage').text('Loading apps...'); $('#naclSpinner').css('display', 'inline-block'); + $("div.game-container").remove(); + host.getAppList().then(function(appList) { if(appList.length == 0) { // TODO: Add placeholder graphic console.error('%c[index.js, showApps]', 'User\'s applist is empty') @@ -491,7 +493,6 @@ function showApps(host) { return; // We stop the function right here } // if game grid is populated, empty it - $("div.game-container").remove(); $('#naclSpinner').hide(); $("#game-grid").show(); @@ -543,7 +544,10 @@ function showApps(host) { $(outerDiv).append(img); }); }, function(failedAppList) { - $('#naclSpinner').hide(); // TODO: Add placeholder graphic + $('#naclSpinner').hide(); + var img = new Image(); + img.src = 'static/res/applist_error.svg' + $("#game-grid").html(img) snackbarLog('Unable to get your games') console.error('%c[index.js, showApps]', 'Failed to get applist from host: ' + host.hostname, '\n Host object:', host, host.toString()); }); diff --git a/static/res/applist_error.svg b/static/res/applist_error.svg new file mode 100644 index 0000000..0088948 --- /dev/null +++ b/static/res/applist_error.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7815f3fa5f8041385406af9dcc8492837b3dc143 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sat, 5 May 2018 17:05:19 +0200 Subject: [PATCH 14/23] Added graphic when gamelist empty --- static/js/index.js | 12 +++++++----- static/res/applist_empty.svg | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 static/res/applist_empty.svg diff --git a/static/js/index.js b/static/js/index.js index 078f128..b1019cf 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -487,16 +487,18 @@ function showApps(host) { $("div.game-container").remove(); host.getAppList().then(function(appList) { - if(appList.length == 0) { // TODO: Add placeholder graphic + $('#naclSpinner').hide(); + $("#game-grid").show(); + + if(appList.length == 0) { console.error('%c[index.js, showApps]', 'User\'s applist is empty') + var img = new Image() + img.src = 'static/res/applist_empty.svg' + document.getElementById('game-grid').appendChild(img) snackbarLog('Your game list is empty') return; // We stop the function right here } // if game grid is populated, empty it - - $('#naclSpinner').hide(); - $("#game-grid").show(); - const sortedAppList = sortTitles(appList, 'ASC'); sortedAppList.forEach(function(app) { diff --git a/static/res/applist_empty.svg b/static/res/applist_empty.svg new file mode 100644 index 0000000..e6aa737 --- /dev/null +++ b/static/res/applist_empty.svg @@ -0,0 +1 @@ + \ No newline at end of file From c48a1d8adcdea082264b77e1424549f22910c2cf Mon Sep 17 00:00:00 2001 From: Jorys Paulin Date: Sat, 5 May 2018 22:05:32 +0200 Subject: [PATCH 15/23] Fixed a duplication bug --- static/js/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/js/index.js b/static/js/index.js index b1019cf..a54e016 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -494,7 +494,7 @@ function showApps(host) { console.error('%c[index.js, showApps]', 'User\'s applist is empty') var img = new Image() img.src = 'static/res/applist_empty.svg' - document.getElementById('game-grid').appendChild(img) + $('#game-grid').html(img) snackbarLog('Your game list is empty') return; // We stop the function right here } From 0dfc4e6a61bc2c3dc50d5c3940bb503488d00b8a Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sat, 2 Jun 2018 10:35:16 +0200 Subject: [PATCH 16/23] Changed outline-color --- static/css/style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/css/style.css b/static/css/style.css index e717391..cf56ad4 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -192,6 +192,7 @@ main { } #host-grid .mdl-card:hover, #host-grid .mdl-card:focus, #host-grid .mdl-card:active, #game-grid .mdl-card:hover, #game-grid .mdl-card:focus, #game-grid .mdl-card:active { transform: scale(1.1); + outline-color: #00A3C6; } #host-grid .mdl-card__title { padding: 0; From 84e1cb80e19942629da5b11ab076e69b1c3e09f8 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sun, 3 Jun 2018 17:10:16 +0200 Subject: [PATCH 17/23] Removed Jquery from game list generation --- static/js/index.js | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index a54e016..4196b34 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -506,32 +506,24 @@ function showApps(host) { // double clicking the button will cause multiple box arts to appear. // to mitigate this we ensure we don't add a duplicate. // This isn't perfect: there's lots of RTTs before the logic prevents anything - var outerDiv = $("
", { - class: 'game-container mdl-card mdl-shadow--4dp', - id: 'game-' + app.id, - role: 'link', - tabindex: 0, - title: app.title, - 'aria-label': app.title - }); + var gameCard = document.createElement('div') + gameCard.id = 'game-' + app.id + gameCard.className = 'game-container mdl-card mdl-shadow--4dp' + gameCard.setAttribute('role', 'link') + gameCard.tabIndex = 0 + gameCard.title = app.title - $(outerDiv).append($("
", { - class: "game-title", - html: $("", { - html: app.title - }) - })); - $("#game-grid").append(outerDiv); + gameCard.innerHTML = `
${app.title}
` - $('#game-' + app.id).on('click', function() { - startGame(host, app.id); - }); - $('#game-' + app.id).keypress(function(e) { - if (e.keyCode == 13) { + gameCard.addEventListener('click', e => { + startGame(host, app.id) + }) + gameCard.addEventListener('keydown', e => { + if(e.key == "Enter") { startGame(host, app.id); } - }); - + }) + document.querySelector('#game-grid').appendChild(gameCard); // apply CSS stylization to indicate whether the app is active stylizeBoxArt(host, app.id); } @@ -543,7 +535,7 @@ function showApps(host) { img.src = 'static/res/placeholder_error.svg' }); img.onload = e => img.classList.add('fade-in'); - $(outerDiv).append(img); + $(gameCard).append(img); }); }, function(failedAppList) { $('#naclSpinner').hide(); From 7c48586394cc26b6b07d927ca2201e59823546f7 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 5 Jun 2018 16:49:47 +0200 Subject: [PATCH 18/23] Added keyboard support on games list --- static/css/style.css | 2 +- static/js/index.js | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index cf56ad4..e961f7d 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -190,7 +190,7 @@ main { transition: all .2s ease-in-out; will-change: transform; } -#host-grid .mdl-card:hover, #host-grid .mdl-card:focus, #host-grid .mdl-card:active, #game-grid .mdl-card:hover, #game-grid .mdl-card:focus, #game-grid .mdl-card:active { +#host-grid .mdl-card:hover, #host-grid .mdl-card:focus, #host-grid .mdl-card:active, #game-grid .mdl-card:focus, #game-grid .mdl-card:active { transform: scale(1.1); outline-color: #00A3C6; } diff --git a/static/js/index.js b/static/js/index.js index 4196b34..0f260ca 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -489,7 +489,7 @@ function showApps(host) { host.getAppList().then(function(appList) { $('#naclSpinner').hide(); $("#game-grid").show(); - + if(appList.length == 0) { console.error('%c[index.js, showApps]', 'User\'s applist is empty') var img = new Image() @@ -518,10 +518,25 @@ function showApps(host) { gameCard.addEventListener('click', e => { startGame(host, app.id) }) + gameCard.addEventListener('mouseover', e => { + gameCard.focus(); + }); gameCard.addEventListener('keydown', e => { if(e.key == "Enter") { startGame(host, app.id); } + if(e.key == "ArrowLeft") { + let prev = gameCard.previousSibling + if(prev !== null) + gameCard.previousSibling.focus() + // TODO: Add a sound when limit reached + } + if(e.key == "ArrowRight") { + let next = gameCard.nextSibling + if(next !== null) + gameCard.nextSibling.focus() + // TODO: Add a sound when limit reached + } }) document.querySelector('#game-grid').appendChild(gameCard); // apply CSS stylization to indicate whether the app is active From 9457304ad33a048954e2a681211bb32549dc8d2f Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 5 Jun 2018 18:36:38 +0200 Subject: [PATCH 19/23] Added native classList to backgroundPoll --- static/js/index.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 0f260ca..a6df867 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -119,27 +119,28 @@ function restoreUiAfterNaClLoad() { } function beginBackgroundPollingOfHost(host) { + var el = document.querySelector('#hostgrid-' + host.serverUid) if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + el.classList.remove('host-cell-inactive') // The host was already online. Just start polling in the background now. activePolls[host.serverUid] = window.setInterval(function() { // every 5 seconds, poll at the address we know it was live at host.pollServer(function() { if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + el.classList.remove('host-cell-inactive') } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + el.classList.add('host-cell-inactive') } }); }, 5000); } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + el.classList.add('host-cell-inactive') // The host was offline, so poll immediately. host.pollServer(function() { if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + el.classList.remove('host-cell-inactive') } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + el.classList.add('host-cell-inactive') } // Now start background polling @@ -147,9 +148,9 @@ function beginBackgroundPollingOfHost(host) { // every 5 seconds, poll at the address we know it was live at host.pollServer(function() { if (host.online) { - $("#hostgrid-" + host.serverUid).removeClass('host-cell-inactive'); + el.classList.remove('host-cell-inactive') } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); + el.classList.add('host-cell-inactive') } }); }, 5000); From 34b6004ad3e612892391ab4b1537274d78802193 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Fri, 8 Jun 2018 18:36:46 +0200 Subject: [PATCH 20/23] Removed unused gameList and gameSelection ids --- static/js/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index a6df867..a302173 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -479,14 +479,11 @@ function showApps(host) { } console.log('%c[index.js, showApps]', 'color: green;', 'Current host object:', host, host.toString()); //Logging both object (for console) and toString-ed object (for text logs) $('#quitCurrentApp').show(); - $("#gameList .game-container").remove(); // Show a spinner while the applist loads $('#naclSpinnerMessage').text('Loading apps...'); $('#naclSpinner').css('display', 'inline-block'); - $("div.game-container").remove(); - host.getAppList().then(function(appList) { $('#naclSpinner').hide(); $("#game-grid").show(); @@ -871,8 +868,6 @@ function updateDefaultBitrate() { function onWindowLoad() { console.log('%c[index.js]', 'color: green;', 'Moonlight\'s main window loaded'); - // don't show the game selection div - $('#gameSelection').css('display', 'none'); loadWindowState(); From 50d21da45323299b4ff40321f5602e9b60ea97f8 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sat, 9 Jun 2018 10:37:36 +0200 Subject: [PATCH 21/23] Replaced border to outline to avoid content jumping --- static/css/style.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/css/style.css b/static/css/style.css index e961f7d..387a508 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -299,10 +299,10 @@ main { border: none !important; } .current-game { - border: 2px solid #00A3C6; + outline: auto #8BC34A; } .host-cell-inactive { - border: 3px solid #8e0000; + outline: auto #F44336; } .host-cell:hover { cursor: pointer; From 5a30f4c983fbbfbd25c6921186e1eb769c1a35b6 Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Sat, 9 Jun 2018 10:45:27 +0200 Subject: [PATCH 22/23] Added "Running" title when hovering running game --- static/js/index.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/static/js/index.js b/static/js/index.js index a302173..8cef2e9 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -439,7 +439,13 @@ function removeClicked(host) { function stylizeBoxArt(freshApi, appIdToStylize) { // If the running game is the good one then style it var el = document.querySelector("#game-" + appIdToStylize); - return (freshApi.currentGame === appIdToStylize) ? el.classList.add('current-game') : el.classList.remove('current-game') + if(freshApi.currentGame === appIdToStylize) { + el.classList.add('current-game') + el.title += ' (Running)' + } else { + el.classList.remove('current-game') + el.title.replace(' (Running)', '') // TODO: Replace with localized string so make it e.title = game_title + } } function sortTitles(list, sortOrder) { From 744ff01e641f616be30769161fd1256299d8a6cf Mon Sep 17 00:00:00 2001 From: Jorys_Paulin Date: Tue, 12 Jun 2018 12:56:45 +0200 Subject: [PATCH 23/23] Revert "Removed unused gameList and gameSelection ids" This reverts commit 34b6004ad3e612892391ab4b1537274d78802193. --- static/js/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/static/js/index.js b/static/js/index.js index 8cef2e9..04c6f40 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -485,11 +485,14 @@ function showApps(host) { } console.log('%c[index.js, showApps]', 'color: green;', 'Current host object:', host, host.toString()); //Logging both object (for console) and toString-ed object (for text logs) $('#quitCurrentApp').show(); + $("#gameList .game-container").remove(); // Show a spinner while the applist loads $('#naclSpinnerMessage').text('Loading apps...'); $('#naclSpinner').css('display', 'inline-block'); + $("div.game-container").remove(); + host.getAppList().then(function(appList) { $('#naclSpinner').hide(); $("#game-grid").show(); @@ -874,6 +877,8 @@ function updateDefaultBitrate() { function onWindowLoad() { console.log('%c[index.js]', 'color: green;', 'Moonlight\'s main window loaded'); + // don't show the game selection div + $('#gameSelection').css('display', 'none'); loadWindowState();