diff --git a/index.html b/index.html
index c56ea1e..ff72d25 100644
--- a/index.html
+++ b/index.html
@@ -34,15 +34,20 @@
-
+
+
diff --git a/static/css/style.css b/static/css/style.css
index da8f73c..0d48836 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -1,36 +1,3 @@
-@font-face {
- font-family: 'Material Icons';
- font-style: normal;
- font-weight: 400;
- src: local('Material Icons'),
- local('MaterialIcons-Regular'),
- url('../fonts/MaterialIcons-Regular.woff') format('woff'),
-}
-.material-icons {
- font-family: 'Material Icons';
- font-weight: normal;
- font-style: normal;
- font-size: 24px; /* Preferred icon size */
- display: inline-block;
- line-height: 1;
- text-transform: none;
- letter-spacing: normal;
- word-wrap: normal;
- white-space: nowrap;
- direction: ltr;
-
- /* Support for all WebKit browsers. */
- -webkit-font-smoothing: antialiased;
- /* Support for Safari and Chrome. */
- text-rendering: optimizeLegibility;
-
- /* Support for Firefox. */
- -moz-osx-font-smoothing: grayscale;
-
- /* Support for IE. */
- font-feature-settings: 'liga';
-}
-
.mdl-layout__header-row {
color: #fff;
}
@@ -46,21 +13,14 @@
color:#fff;
padding:25px;
}
-#addHostIcon {
- width:64px;
- height:64px;
- margin:auto;
- display:block;
-}
#backIcon {
height: 32px;
width: 32px;
margin-left: -20px;
padding-right: 20px;
}
-#addHostCell {
- padding-top:3px;
- padding-bottom:3px;
+#backIcon:hover {
+ cursor: pointer;
}
.mdl-dialog {
border: none;
@@ -173,6 +133,12 @@ main {
margin: 0;
width: 100%;
height: 100%;
+ -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+ -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+ box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+}
+.box-art:hover {
+ cursor: pointer;
}
.current-game {
border: 3px solid green;
@@ -184,3 +150,27 @@ main {
color: #000;
font-weight: bold;
}
+.host-cell {
+ display: inline-block;
+ width: 150px !important;
+ height: 150px !important;
+ background-color: #000;
+ -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+ -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+ box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.34);
+ -webkit-border-radius: 10px;
+ -moz-border-radius: 10px;
+ border-radius: 10px;
+}
+.host-cell:hover {
+ cursor: pointer;
+ color: #000 !important;
+}
+.host-cell:hover img {
+ -webkit-filter: invert(100%);
+ filter: invert(100%);
+}
+.host-cell img {
+ width: 80px;;
+ margin-bottom: 10px;
+}
diff --git a/static/js/index.js b/static/js/index.js
index 0c9e117..ca06485 100644
--- a/static/js/index.js
+++ b/static/js/index.js
@@ -1,4 +1,7 @@
-var host = "";
+// CURRENT ISSUE: host is not being saved. or it may have not been saved, and my state is screwed up.
+// if (given host not in hosts) hosts.append(given host);
+
+var _host = "";
var hosts = [];
var pairingCert;
var myUniqueid;
@@ -13,6 +16,7 @@ function attachListeners() {
$('#selectFramerate').on('change', saveFramerate);
$('#bitrateSlider').on('input', updateBitrateField); // input occurs every notch you slide
$('#bitrateSlider').on('change', saveBitrate); // change occurs once the mouse lets go.
+ $("#remoteAudioEnabledSwitch").on('click', saveRemoteAudio);
$('#hostChosen').on('click', hostChosen);
$('#addHostCell').on('click', addHost);
$('#cancelAddHost').on('click', cancelAddHost);
@@ -149,14 +153,19 @@ function pairTo(host, onSuccess, onFailure) {
function hostChosen(sourceEvent) {
if(sourceEvent && sourceEvent.srcElement) {
- console.log('parsing host from grid element.');
- host = sourceEvent.srcElement.innerText;
+ if (sourceEvent.srcElement.innerText == "") {
+ console.log('user clicked image. we gotta hack to parse out the host.');
+ host = sourceEvent.currentTarget.childNodes[1].textContent;
+ } else {
+ console.log('parsing host from grid element.');
+ host = sourceEvent.srcElement.innerText;
+ }
}
api = new NvHTTP(host, myUniqueid);
api.refreshServerInfo().then(function (ret) {
if(!api.paired) {
- pairTo(host, function(){ showApps(); }, function(){});
+ pairTo(host, function(){ showApps(); saveHosts(); }, function(){});
} else {
showApps();
}
@@ -178,15 +187,15 @@ function cancelAddHost() {
}
function addHostToGrid(host) {
- if(hosts.indexOf(host) < 0) { // we don't have this host in our list. add it, and save it.
- var cell = document.createElement('div');
- cell.className += 'mdl-cell mdl-cell--3-col';
- cell.id = 'hostgrid-' + host;
- cell.innerHTML = host;
- $('#host-grid').append(cell);
- cell.onclick = hostChosen;
+ var cell = document.createElement('div');
+ cell.className += 'mdl-cell mdl-cell--3-col host-cell mdl-button mdl-js-button mdl-js-ripple-effect';
+ cell.id = 'hostgrid-' + host;
+ cell.innerHTML = host;
+ $(cell).prepend($("
![]()
", {src: "static/res/ic_desktop_windows_white_24px.svg"}));
+ $('#host-grid').append(cell);
+ cell.onclick = hostChosen;
+ if(hosts.indexOf(host) < 0) {
hosts.push(host);
- saveHosts();
}
}
@@ -196,6 +205,7 @@ function continueAddHost() {
pairTo(inputHost,
function() {
addHostToGrid(inputHost);
+ saveHosts();
document.querySelector('#addHostDialog').close();
},
function() {
@@ -218,6 +228,25 @@ function pairingPopupCanceled() {
document.querySelector('#pairingDialog').close();
}
+// puts the CSS style for current app on the app that's currently running
+// and puts the CSS style for non-current app apps that aren't running
+// this requires a hot-off-the-host `api`, and the appId we're going to stylize
+// 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');
+ }
+}
+
// show the app list
function showApps() {
if(!api || !api.paired) { // safety checking. shouldn't happen.
@@ -231,21 +260,13 @@ function showApps() {
api.getAppList().then(function (appList) {
appList.forEach(function (app) {
api.getBoxArt(app.id).then(function (resolvedPromise) {
+ // put the box art into the image holder
var imageBlob = new Blob([resolvedPromise], {type: "image/png"});
$("#game-grid").append($("
", {html:$("
![]()
", {src: URL.createObjectURL(imageBlob), id: 'game-'+app.id, name: app.title }), class: 'box-art mdl-cell mdl-cell--3-col'}).append($("
", {html: app.title, class:"game-title"})));
$('#game-'+app.id).on('click', startGame);
- if (api.currentGame === app.id){ // stylize the currently running game
- // destylize it, if it has the not-current-game style
- if ($('#game-'+ app.id).hasClass("not-current-game")) $('#game-'+ app.id).removeClass("not-current-game");
- // add the current-game style
- $('#game-'+ app.id).addClass("current-game");
- } else {
- // destylize it, if it has the current-game style
- if ($('#game-'+ app.id).hasClass("current-game")) $('#game-'+ app.id).removeClass("current-game");
- // add the not-current-game style
- $('#game-'+ app.id).addClass('not-current-game');
- }
+ // apply CSS stylization to indicate whether the app is active
+ stylizeBoxArt(api, app.id);
}, function (failedPromise) {
console.log('Error! Failed to retrieve box art for app ID: ' + app.id + '. Returned value was: ' + failedPromise)
@@ -349,6 +370,8 @@ function startGame(sourceEvent) {
});
}
+ remote_audio_enabled = $("#remoteAudioEnabledSwitch").parent().hasClass('is-checked') ? 1 : 0;
+
api.launchApp(appID,
streamWidth + "x" + streamHeight + "x" + frameRate,
1, // Allow GFE to optimize game settings
@@ -477,6 +500,14 @@ function saveBitrate() {
storeData('bitrate', $('#bitrateSlider').val(), null);
}
+function saveRemoteAudio() {
+ console.log('saving remote audio state');
+ // problem: when off, and the app is just starting, a tick to the switch doesn't always toggle it
+ // second problem: this callback is called immediately after clicking, so the HTML class `is-checked` isn't toggled yet
+ // to solve the second problem, we invert the boolean. This has worked in all cases I've tried, except for the first case
+ storeData('remoteAudio', !$("#remoteAudioEnabledSwitch").parent().hasClass('is-checked'), null);
+}
+
function updateDefaultBitrate() {
var res = $('#selectResolution').val();
var frameRate = $('#selectFramerate').val();
@@ -514,6 +545,16 @@ function onWindowLoad(){
chrome.storage.sync.get('resolution', function(previousValue) {
$('#selectResolution').val(previousValue.resolution != null ? previousValue.resolution : '1280:720');
});
+ chrome.storage.sync.get('remoteAudio', function(previousValue) {
+ if(previousValue.remoteAudio == null) {
+ document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.off();
+ return;
+ } else if(previousValue.remoteAudio == false) {
+ document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.off();
+ } else {
+ document.querySelector('#remoteAudioEnabledSwitchContainer').MaterialSwitch.on();
+ }
+ });
// load stored framerate prefs
chrome.storage.sync.get('frameRate', function(previousValue) {
$('#selectFramerate').val(previousValue.frameRate != null ? previousValue.frameRate : '60');
@@ -522,12 +563,7 @@ function onWindowLoad(){
chrome.storage.sync.get('hosts', function(previousValue) {
hosts = previousValue.hosts != null ? previousValue.hosts : [];
for(var i = 0; i < hosts.length; i++) { // programmatically add each new host.
- var cell = document.createElement('div');
- cell.className += 'mdl-cell mdl-cell--3-col';
- cell.id = 'hostgrid-' + hosts[i];
- cell.innerHTML = hosts[i];
- $('#host-grid').append(cell);
- cell.onclick = hostChosen;
+ addHostToGrid(hosts[i]);
}
});
diff --git a/static/js/messages.js b/static/js/messages.js
index cc223c8..8279a0d 100644
--- a/static/js/messages.js
+++ b/static/js/messages.js
@@ -22,10 +22,17 @@ function handleMessage(msg) {
console.log(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.
- api.refreshServerInfo().then(function (ret) {
+
+ 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();
chrome.app.window.current().restore();
});
+
} else if(msg.data === 'Connection Established') {
$('#loadingSpinner').css('display', 'none');
} else if(msg.data.indexOf('ProgressMsg: ') === 0) {
diff --git a/static/js/utils.js b/static/js/utils.js
index 27e430a..441ef5e 100644
--- a/static/js/utils.js
+++ b/static/js/utils.js
@@ -41,6 +41,12 @@ function NvHTTP(address, clientUid) {
this._baseUrlHttps = 'https://' + address + ':47984';
this._baseUrlHttp = 'http://' + address + ':47989';
this._memCachedBoxArtArray = {};
+
+ 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;
_self = this;
};
@@ -78,7 +84,7 @@ NvHTTP.prototype = {
_parseServerInfo: function(xmlStr) {
$xml = _self._parseXML(xmlStr);
$root = $xml.find('root');
-
+
if($root.attr("status_code") != 200) {
return false;
}
@@ -86,7 +92,24 @@ NvHTTP.prototype = {
_self.paired = $root.find("PairStatus").text().trim() == 1;
_self.currentGame = parseInt($root.find("currentgame").text().trim(), 10);
_self.serverMajorVersion = parseInt($root.find("appversion").text().trim().substring(0, 1), 10);
-
+ _self.serverUid = $root.find('uniqueid').text().trim();
+ _self.GfeVersion = $root.find('GfeVersion').text().trim();
+ _self.gputype = $root.find('gputype').text().trim();
+ _self.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(!_self.supportedDisplayModes[yres + ':' + xres]) {
+ _self.supportedDisplayModes[yres + ':' + xres] = [];
+ }
+ if(!_self.supportedDisplayModes[yres + ':' + xres].includes(fps)) {
+ _self.supportedDisplayModes[yres + ':' + xres].push(fps);
+ }
+
+ });
+
// 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.
diff --git a/static/res/ic_desktop_windows_white_24px.svg b/static/res/ic_desktop_windows_white_24px.svg
new file mode 100644
index 0000000..685582f
--- /dev/null
+++ b/static/res/ic_desktop_windows_white_24px.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file