diff --git a/index.html b/index.html index 498060b..ba03ad6 100644 --- a/index.html +++ b/index.html @@ -1,184 +1,180 @@ - - - - Moonlight - - - - + + + + Moonlight + + + + -
+
-
-
Your PCs
-
-
-

Add Host

-
-
-
-
-
Your Games
-
-
- -
-
-
-
-
-
+
+
+
+

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

-
- - -
+
+ + +
- - + +
-
-
+
+
- -
+ + + diff --git a/static/css/style.css b/static/css/style.css index f2a1a82..387a508 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; @@ -177,7 +178,7 @@ main { } #game-grid .mdl-card { position: relative; - background: transparent; + 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; @@ -187,9 +188,11 @@ 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 { +#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; } #host-grid .mdl-card__title { padding: 0; @@ -203,6 +206,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; @@ -290,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; 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 cf988d0..04c6f40 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -1,158 +1,161 @@ -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) { - $("#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'); - } else { - $("#hostgrid-" + host.serverUid).addClass('host-cell-inactive'); - } + var el = document.querySelector('#hostgrid-' + host.serverUid) + if (host.online) { + 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) { + el.classList.remove('host-cell-inactive') + } else { + el.classList.add('host-cell-inactive') + } + }); + }, 5000); + } else { + el.classList.add('host-cell-inactive') + // The host was offline, so poll immediately. + host.pollServer(function() { + if (host.online) { + el.classList.remove('host-cell-inactive') + } else { + el.classList.add('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) { + el.classList.remove('host-cell-inactive') + } else { + el.classList.add('host-cell-inactive') + } }); - } + }, 5000); + }); + } } function stopBackgroundPollingOfHost(host) { @@ -162,254 +165,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 +437,15 @@ 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); + 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) { @@ -438,344 +455,371 @@ 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; + 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'); + + $("div.game-container").remove(); + + 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() + img.src = 'static/res/applist_empty.svg' + $('#game-grid').html(img) + snackbarLog('Your game list is empty') + return; // We stop the function right here } - 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 game grid is populated, empty it + const sortedAppList = sortTitles(appList, 'ASC'); - // Show a spinner while the applist loads - $('#naclSpinnerMessage').text('Loading apps...'); - $('#naclSpinner').css('display', 'inline-block'); + 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 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 - host.getAppList().then(function (appList) { - // if game grid is populated, empty it - $("div.game-container").remove(); + gameCard.innerHTML = `
${app.title}
` - $('#naclSpinner').hide(); - $("#game-grid").show(); - - const sortedAppList = sortTitles(appList, 'ASC'); - - sortedAppList.forEach(function (app) { - 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); - } - - }, 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); - } - }); + gameCard.addEventListener('click', e => { + startGame(host, app.id) + }) + gameCard.addEventListener('mouseover', e => { + gameCard.focus(); }); - }, 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()); + 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 + stylizeBoxArt(host, app.id); + } + var img = new Image(); + host.getBoxArt(app.id).then(function(resolvedPromise) { + img.src = resolvedPromise; + }, 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()); + img.src = 'static/res/placeholder_error.svg' + }); + img.onload = e => img.classList.add('fade-in'); + $(gameCard).append(img); }); + }, function(failedAppList) { + $('#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()); + }); - showAppsMode(); + 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(); } @@ -784,111 +828,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())); + }, }; 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 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 diff --git a/static/res/no_app_image.png b/static/res/no_app_image.png deleted file mode 100644 index 4cbad3e..0000000 Binary files a/static/res/no_app_image.png and /dev/null differ diff --git a/static/res/placeholder_error.svg b/static/res/placeholder_error.svg new file mode 100644 index 0000000..c3332b6 --- /dev/null +++ b/static/res/placeholder_error.svg @@ -0,0 +1 @@ + \ No newline at end of file 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