", {
+ 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