Merge pull request #454 from Jorys-Paulin/develop

Changes and improvements
This commit is contained in:
Cameron Gutman 2018-06-12 19:33:29 -07:00 committed by GitHub
commit 47cfbf3d9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1363 additions and 1302 deletions

View File

@ -1,184 +1,180 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="-1"> <meta http-equiv="Expires" content="-1">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Moonlight</title> <title>Moonlight</title>
<link rel="stylesheet" href="static/css/roboto.css"> <link rel="stylesheet" href="static/css/roboto.css">
<link rel="stylesheet" href="static/css/material.min.css"> <link rel="stylesheet" href="static/css/material.min.css">
<link rel="stylesheet" href="static/css/style.css"> <link rel="stylesheet" href="static/css/style.css">
<link rel="stylesheet" href="static/css/material-icons.css"> <link rel="stylesheet" href="static/css/material-icons.css">
</head> </head>
<body data-name="moonlight-chrome" data-tools="pnacl" data-configs="Debug Release" data-path="{tc}/{config}"> <body data-name="moonlight-chrome" data-tools="pnacl" data-configs="Debug Release" data-path="{tc}/{config}">
<div class="mdl-layout mdl-js-layout"> <div class="mdl-layout mdl-js-layout">
<header id="main-navigation" class="mdl-layout__header mdl-layout__header--transparent"> <header id="main-navigation" class="mdl-layout__header mdl-layout__header--transparent">
<div class="mdl-layout__header-row"> <div class="mdl-layout__header-row">
<button id="backIcon" class="mdl-button mdl-js-button mdl-button--icon" role="link" aria-label="Host selection"><i class="material-icons">keyboard_arrow_left</i></button> <button id="backIcon" class="mdl-button mdl-js-button mdl-button--icon" role="link" aria-label="Host selection"><i class="material-icons">keyboard_arrow_left</i></button>
<!-- Title --> <!-- Title -->
<span class="mdl-layout-title">MOON<span>LIGHT</span></span> <span class="mdl-layout-title">MOON<span>LIGHT</span></span>
<!-- Add spacer, to align navigation to the right --> <!-- Add spacer, to align navigation to the right -->
<div class="mdl-layout-spacer"></div> <div class="mdl-layout-spacer"></div>
<!-- Navigation --> <!-- Navigation -->
<nav class="mdl-navigation"> <nav class="mdl-navigation">
<div class="nav-menu-parent"> <div class="nav-menu-parent">
<div id="resolutionMenu"> <div id="resolutionMenu">
<button id="selectResolution" data-value="1280:720" class="mdl-button mdl-js-button"> <button id="selectResolution" data-value="1280:720" class="mdl-button mdl-js-button">
720p 720p
</button> </button>
</div> </div>
<ul class="resolutionMenu mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" <ul class="resolutionMenu mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="selectResolution">
for="selectResolution"> <li class="mdl-menu__item" data-value="1280:720">720p</li>
<li class="mdl-menu__item" data-value="1280:720">720p</li> <li class="mdl-menu__item" data-value="1920:1080">1080p</li>
<li class="mdl-menu__item" data-value="1920:1080">1080p</li> <li class="mdl-menu__item" data-value="3840:2160">4K</li>
<li class="mdl-menu__item" data-value="3840:2160">4K</li> </ul>
</ul>
<div id="resolutionTooltip" class="mdl-tooltip" for="resolutionMenu"> <div id="resolutionTooltip" class="mdl-tooltip" for="resolutionMenu">
Resolution Resolution
</div> </div>
</div> </div>
<div class="nav-menu-parent"> <div class="nav-menu-parent">
<div id="framerateMenu"> <div id="framerateMenu">
<button id="selectFramerate" data-value="60" class="mdl-button mdl-js-button"> <button id="selectFramerate" data-value="60" class="mdl-button mdl-js-button">
60 FPS 60 FPS
</button> </button>
</div> </div>
<ul class="framerateMenu mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" <ul class="framerateMenu mdl-menu mdl-menu--bottom-right mdl-js-menu mdl-js-ripple-effect" for="selectFramerate">
for="selectFramerate"> <li class="mdl-menu__item" data-value="30">30 FPS</li>
<li class="mdl-menu__item" data-value="30">30 FPS</li> <li class="mdl-menu__item" data-value="60">60 FPS</li>
<li class="mdl-menu__item" data-value="60">60 FPS</li> </ul>
</ul>
<div id="framerateTooltip" class="mdl-tooltip" for="framerateMenu"> <div id="framerateTooltip" class="mdl-tooltip" for="framerateMenu">
Framerate Framerate
</div> </div>
</div> </div>
<div class="nav-menu-parent"> <div class="nav-menu-parent">
<div id="bandwidthMenu"> <div id="bandwidthMenu">
<button id="bitrateField" class="mdl-button">10 Mbps</button> <button id="bitrateField" class="mdl-button">10 Mbps</button>
</div> </div>
<div class="bitrateMenu mdl-menu mdl-js-menu mdl-js-ripple-effect" for="bandwidthMenu"> <div class="bitrateMenu mdl-menu mdl-js-menu mdl-js-ripple-effect" for="bandwidthMenu">
<input id="bitrateSlider" class="mdl-slider mdl-js-slider" type="range" min="0" max="100" step="0.5" value="10"> <input id="bitrateSlider" class="mdl-slider mdl-js-slider" type="range" min="0" max="100" step="0.5" value="10">
</div> </div>
<div id="bandwidthTooltip" class="mdl-tooltip" for="bandwidthMenu"> <div id="bandwidthTooltip" class="mdl-tooltip" for="bandwidthMenu">
Bandwidth Bandwidth
</div> </div>
</div> </div>
<div class="nav-menu-parent"> <div class="nav-menu-parent">
<label id="externalAudioBtn" class="mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect" for="remoteAudioEnabledSwitch"> <label id="externalAudioBtn" class="mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect" for="remoteAudioEnabledSwitch">
<input type="checkbox" id="remoteAudioEnabledSwitch" class="mdl-icon-toggle__input"> <input type="checkbox" id="remoteAudioEnabledSwitch" class="mdl-icon-toggle__input">
<i class="mdl-icon-toggle__label material-icons">volume_up</i> <i class="mdl-icon-toggle__label material-icons">volume_up</i>
</label> </label>
<div id="externalAudioTooltip" class="mdl-tooltip" for="externalAudioBtn"> <div id="externalAudioTooltip" class="mdl-tooltip" for="externalAudioBtn">
Play audio on the host PC speakers Play audio on the host
</div> </div>
</div> </div>
<div class="nav-menu-parent"> <div class="nav-menu-parent">
<label id="optimizeGamesBtn" class="mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect" for="optimizeGamesSwitch"> <label id="optimizeGamesBtn" class="mdl-icon-toggle mdl-js-icon-toggle mdl-js-ripple-effect" for="optimizeGamesSwitch">
<input type="checkbox" id="optimizeGamesSwitch" class="mdl-icon-toggle__input"> <input type="checkbox" id="optimizeGamesSwitch" class="mdl-icon-toggle__input">
<i class="mdl-icon-toggle__label material-icons">timeline</i> <i class="mdl-icon-toggle__label material-icons">timeline</i>
</label> </label>
<div id="optimizeGamesTooltip" class="mdl-tooltip" for="optimizeGamesBtn"> <div id="optimizeGamesTooltip" class="mdl-tooltip" for="optimizeGamesBtn">
Allow GeForce Experience to optimize game settings for streaming Allow game optimisations
</div> </div>
</div> </div>
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="quitCurrentApp" aria-label="Quit current app"> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="quitCurrentApp" aria-label="Quit current app">
<i class="material-icons">remove_circle_outline</i> <i class="material-icons">remove_circle_outline</i>
Quit Current App Quit Current App
</button> </button>
</nav> </nav>
</div> </div>
</header> </header>
<main id="main-content" class="mdl-layout__content"> <main id="main-content" class="mdl-layout__content">
<div id="host-grid"> <div id="host-grid">
<div class="page-title">Your PCs</div> <div class="add-host-card mdl-card mdl-shadow--4dp" id="addHostCell">
<div class="add-host-card mdl-card mdl-shadow--4dp" id="addHostCell"> <div class="mdl-card__title mdl-card--expand" id="addHostIcon" role="link" tabindex="0" aria-label="Add Host">
<div class="mdl-card__title mdl-card--expand" id="addHostIcon" role="link" tabindex="0" aria-label="Add Host"> <h2 class="mdl-card__title-text">Add Host</h2>
<h2 class="mdl-card__title-text" >Add Host</h2> </div>
</div> </div>
</div> </div>
</div> <div id="game-grid"></div>
<div id="game-grid"> <div id="listener"></div>
<div class="page-title">Your Games</div> <!-- NaCl module placeholder. NaCl gets thrown into here -->
</div> <div id="loadingSpinner" class="mdl-progress mdl-js-progress mdl-progress__indeterminate">
<div id="listener"></div> <h5 id="loadingMessage"></h5>
<!-- NaCl module placeholder. NaCl gets thrown into here --> </div>
<div id="loadingSpinner" class="mdl-progress mdl-js-progress mdl-progress__indeterminate"> <div id="naclSpinner" class="mdl-progress mdl-js-progress mdl-progress__indeterminate">
<h5 id="loadingMessage"></h5> <h5 id="naclSpinnerMessage"></h5>
</div> </div>
<div id="naclSpinner" class="mdl-progress mdl-js-progress mdl-progress__indeterminate">
<h5 id="naclSpinnerMessage"></h5>
</div>
</main> </main>
</div> </div>
<script defer src="static/js/jquery-2.2.0.min.js"></script> <script defer src="static/js/jquery-2.2.0.min.js"></script>
<script defer src="static/js/material.min.js"></script> <script defer src="static/js/material.min.js"></script>
<script type="text/javascript" src="static/js/messages.js"></script> <script type="text/javascript" src="static/js/messages.js"></script>
<script type="text/javascript" src="static/js/common.js"></script> <script type="text/javascript" src="static/js/common.js"></script>
<script type="text/javascript" src="static/js/index.js"></script> <script type="text/javascript" src="static/js/index.js"></script>
<script type="text/javascript" src="static/js/utils.js"></script> <script type="text/javascript" src="static/js/utils.js"></script>
<script type="text/javascript" src="static/js/mdns-browser/dns.js"></script> <script type="text/javascript" src="static/js/mdns-browser/dns.js"></script>
<script type="text/javascript" src="static/js/mdns-browser/main.js"></script> <script type="text/javascript" src="static/js/mdns-browser/main.js"></script>
<dialog id="pairingDialog" class="mdl-dialog"> <dialog id="pairingDialog" class="mdl-dialog">
<h3 class="mdl-dialog__title">Pairing</h3> <h3 class="mdl-dialog__title">Pairing</h3>
<div class="mdl-dialog__content"> <div class="mdl-dialog__content">
<p id="pairingDialogText"> <p id="pairingDialogText">
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
</p> </p>
</div> </div>
<div class="mdl-dialog__actions"> <div class="mdl-dialog__actions">
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelPairingDialog">Cancel</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelPairingDialog">Cancel</button>
</div> </div>
</dialog> </dialog>
<dialog id="quitAppDialog" class="mdl-dialog"> <dialog id="quitAppDialog" class="mdl-dialog">
<h3 class="mdl-dialog__title">Quit Running App?</h3> <h3 class="mdl-dialog__title">Quit Running App?</h3>
<div class="mdl-dialog__content"> <div class="mdl-dialog__content">
<p id="quitAppDialogText"> <p id="quitAppDialogText">
Y is already running. Would you like to quit Y? Y is already running. Would you like to quit Y?
</p> </p>
</div> </div>
<div class="mdl-dialog__actions"> <div class="mdl-dialog__actions">
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelQuitApp">No</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelQuitApp">No</button>
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueQuitApp">Yes</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueQuitApp">Yes</button>
</div> </div>
</dialog> </dialog>
<dialog id="deleteHostDialog" class="mdl-dialog"> <dialog id="deleteHostDialog" class="mdl-dialog">
<h3 class="mdl-dialog__title">Delete PC</h3> <h3 class="mdl-dialog__title">Delete PC</h3>
<div class="mdl-dialog__content"> <div class="mdl-dialog__content">
<p id="deleteHostDialogText"> <p id="deleteHostDialogText">
Are you sure you want to delete this host? Are you sure you want to delete this host?
</p> </p>
</div> </div>
<div class="mdl-dialog__actions"> <div class="mdl-dialog__actions">
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelDeleteHost">No</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelDeleteHost">No</button>
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueDeleteHost">Yes</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueDeleteHost">Yes</button>
</div> </div>
</dialog> </dialog>
<dialog id="addHostDialog" class="mdl-dialog"> <dialog id="addHostDialog" class="mdl-dialog">
<h3 class="mdl-dialog__title">Add Host Manually</h3> <h3 class="mdl-dialog__title">Add Host Manually</h3>
<div class="mdl-dialog__content"> <div class="mdl-dialog__content">
<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"> <div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label">
<input class="mdl-textfield__input" type="text" id="dialogInputHost"/> <input class="mdl-textfield__input" type="text" id="dialogInputHost" />
<label class="mdl-textfield__label" for="dialogInputHost">IP Address or Hostname of Geforce PC</label> <label class="mdl-textfield__label" for="dialogInputHost">IP Address or Hostname of Geforce PC</label>
</div> </div>
</div> </div>
<div class="mdl-dialog__actions"> <div class="mdl-dialog__actions">
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelAddHost">Cancel</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="cancelAddHost">Cancel</button>
<button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueAddHost">Continue</button> <button type="button" class="mdl-button mdl-js-button mdl-button--raised mdl-button--colored mdl-js-ripple-effect" id="continueAddHost">Continue</button>
</div> </div>
</dialog> </dialog>
<div id="snackbar" class="mdl-snackbar mdl-js-snackbar"> <div id="snackbar" class="mdl-snackbar mdl-js-snackbar">
<div class="mdl-snackbar__text"></div> <div class="mdl-snackbar__text"></div>
<button id="snackButton" class="mdl-snackbar__action" type="button"></button> <!-- this button exists to suppress the snackbar warning. we're really using a toast. --> <button id="snackButton" class="mdl-snackbar__action" type="button"></button>
</div> <!-- this button exists to suppress the snackbar warning. we're really using a toast. -->
</div>
</body> </body>
</html> </html>

View File

@ -72,6 +72,7 @@ main {
.nav-menu-parent { .nav-menu-parent {
position: relative; position: relative;
margin: 0 4px;
} }
.mdl-menu__outline { .mdl-menu__outline {
background-color: #333846; background-color: #333846;
@ -177,7 +178,7 @@ main {
} }
#game-grid .mdl-card { #game-grid .mdl-card {
position: relative; 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 { #host-grid .mdl-card, #game-grid .mdl-card {
text-align: center; text-align: center;
@ -187,9 +188,11 @@ main {
margin: 15px; margin: 15px;
cursor: pointer; cursor: pointer;
transition: all .2s ease-in-out; 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); transform: scale(1.1);
outline-color: #00A3C6;
} }
#host-grid .mdl-card__title { #host-grid .mdl-card__title {
padding: 0; padding: 0;
@ -203,6 +206,12 @@ main {
#game-grid .mdl-card img { #game-grid .mdl-card img {
height: 100%; height: 100%;
width: 100%; width: 100%;
opacity: 0;
transition: opacity .3s;
z-index: -1;
}
#game-grid .mdl-card img.fade-in {
opacity: 1;
} }
#game-grid .game-title { #game-grid .game-title {
position: absolute; position: absolute;
@ -290,10 +299,10 @@ main {
border: none !important; border: none !important;
} }
.current-game { .current-game {
border: 2px solid #00A3C6; outline: auto #8BC34A;
} }
.host-cell-inactive { .host-cell-inactive {
border: 3px solid #8e0000; outline: auto #F44336;
} }
.host-cell:hover { .host-cell:hover {
cursor: pointer; cursor: pointer;

View File

@ -1,33 +1,35 @@
function createWindow(state) { function createWindow(state) {
chrome.app.window.create('index.html', { chrome.app.window.create('index.html', {
state: state, state: state,
bounds: { bounds: {
width: 960, width: 960,
height: 540 height: 540
} }
}, function(window) { }, function(window) {
// workaround: // workaround:
// state = 'normal' in some cases not work (e.g. starting app from 'chrome://extensions' always open window in fullscreen mode) // 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' // it requires manually restoring window state to 'normal'
if (state == 'normal') { if (state == 'normal') {
setTimeout(function() { window.restore(); }, 1000); setTimeout(function() {
} window.restore();
}); }, 1000);
}
});
} }
chrome.app.runtime.onLaunched.addListener(function() { chrome.app.runtime.onLaunched.addListener(function() {
console.log('Chrome app runtime launched.'); console.log('Chrome app runtime launched.');
var windowState = 'normal'; var windowState = 'normal';
if (chrome.storage) { if (chrome.storage) {
// load stored window state // load stored window state
chrome.storage.sync.get('windowState', function(item) { chrome.storage.sync.get('windowState', function(item) {
windowState = (item && item.windowState) windowState = (item && item.windowState) ?
? item.windowState item.windowState :
: windowState; windowState;
createWindow(windowState); createWindow(windowState);
}); });
} else { } else {
createWindow(windowState); createWindow(windowState);
} }
}); });

View File

@ -35,7 +35,7 @@ var common = (function() {
} else if (tool == 'pnacl') { } else if (tool == 'pnacl') {
mimetype = 'application/x-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; return mimetype;
} }
@ -72,7 +72,7 @@ var common = (function() {
*/ */
function createNaClModule(name, tool, path, width, height, attrs) { function createNaClModule(name, tool, path, width, height, attrs) {
console.log('%c[createNaClModule, common.js]', 'color: gray;', "name: " + name + ", tool: " + tool + ", path: " + path + ", width: " + width + 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'); var moduleEl = document.createElement('embed');
moduleEl.setAttribute('name', 'nacl_module'); moduleEl.setAttribute('name', 'nacl_module');
moduleEl.setAttribute('id', 'nacl_module'); moduleEl.setAttribute('id', 'nacl_module');
@ -115,7 +115,7 @@ var common = (function() {
moduleEl.readyState = 4; moduleEl.readyState = 4;
moduleEl.dispatchEvent(new CustomEvent('load')); moduleEl.dispatchEvent(new CustomEvent('load'));
moduleEl.dispatchEvent(new CustomEvent('loadend')); 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. * This event listener is registered in attachDefaultListeners above.
*/ */
function handleCrash(event) { 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) { if (common.naclModule.exitStatus == -1) {
updateStatus('CRASHED'); updateStatus('CRASHED');
} else { } else {
@ -373,7 +373,7 @@ document.addEventListener('DOMContentLoaded', function() {
for (var key_ix = 0; key_ix < pairs.length; key_ix++) { for (var key_ix = 0; key_ix < pairs.length; key_ix++) {
var keyValue = pairs[key_ix].split('='); var keyValue = pairs[key_ix].split('=');
searchVars[unescape(keyValue[0])] = 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 ? 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. // 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 // 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; isRelease = path.toLowerCase().indexOf('release') != -1;
loadFunction(body.dataset.name, tc, path, body.dataset.width, loadFunction(body.dataset.name, tc, path, body.dataset.width,
body.dataset.height, attrs); body.dataset.height, attrs);
} }
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -9,66 +9,69 @@ var callbacks_ids = 1;
* @return {void} The NaCl module calls back trought the handleMessage method * @return {void} The NaCl module calls back trought the handleMessage method
*/ */
var sendMessage = function(method, params) { var sendMessage = function(method, params) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
var id = callbacks_ids++; var id = callbacks_ids++;
callbacks[id] = {'resolve': resolve, 'reject': reject}; callbacks[id] = {
'resolve': resolve,
'reject': reject
};
common.naclModule.postMessage({ common.naclModule.postMessage({
'callbackId': id, 'callbackId': id,
'method': method, 'method': method,
'params': params 'params': params
});
}); });
});
} }
/** /**
* handleMessage - Handles messages from the NaCl module * 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} * @return {void}
*/ */
function handleMessage(msg) { function handleMessage(msg) {
if (msg.data.callbackId && callbacks[msg.data.callbackId]) { // if it's a callback, treat it as such 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); callbacks[msg.data.callbackId][msg.data.type](msg.data.ret);
delete callbacks[msg.data.callbackId] delete callbacks[msg.data.callbackId]
} else { // else, it's just info, or an event } else { // else, it's just info, or an event
console.log('%c[messages.js, handleMessage]', 'color:gray;', 'Message data: ', msg.data) 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 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. $('#loadingSpinner').css('display', 'none'); // This is a fallback for RTSP handshake failing, which immediately terminates the stream.
$('body').css('backgroundColor', '#282C38'); $('body').css('backgroundColor', '#282C38');
// Release our keep awake request // Release our keep awake request
chrome.power.releaseKeepAwake(); chrome.power.releaseKeepAwake();
api.refreshServerInfo().then(function (ret) { // refresh the serverinfo to acknowledge the currently running app api.refreshServerInfo().then(function(ret) { // refresh the serverinfo to acknowledge the currently running app
api.getAppList().then(function (appList) { api.getAppList().then(function(appList) {
appList.forEach(function (app) { appList.forEach(function(app) {
stylizeBoxArt(api, app.id); // and reapply stylization to indicate what's currently running stylizeBoxArt(api, app.id); // and reapply stylization to indicate what's currently running
}); });
}); });
showApps(api); showApps(api);
isInGame = false; isInGame = false;
// restore main window from 'fullscreen' to 'normal' mode (if required) // restore main window from 'fullscreen' to 'normal' mode (if required)
(windowState == 'normal') && chrome.app.window.current().restore(); (windowState == 'normal') && chrome.app.window.current().restore();
}); });
} else if(msg.data === 'Connection Established') { } else if (msg.data === 'Connection Established') {
$('#loadingSpinner').css('display', 'none'); $('#loadingSpinner').css('display', 'none');
$('body').css('backgroundColor', 'black'); $('body').css('backgroundColor', 'black');
// Keep the display awake while streaming // Keep the display awake while streaming
chrome.power.requestKeepAwake("display"); chrome.power.requestKeepAwake("display");
} else if(msg.data.indexOf('ProgressMsg: ') === 0) { } else if (msg.data.indexOf('ProgressMsg: ') === 0) {
$('#loadingMessage').text(msg.data.replace('ProgressMsg: ', '')); $('#loadingMessage').text(msg.data.replace('ProgressMsg: ', ''));
} else if(msg.data.indexOf('TransientMsg: ') === 0) { } else if (msg.data.indexOf('TransientMsg: ') === 0) {
snackbarLog(msg.data.replace('TransientMsg: ', '')); snackbarLog(msg.data.replace('TransientMsg: ', ''));
} else if(msg.data.indexOf('DialogMsg: ') === 0) { } else if (msg.data.indexOf('DialogMsg: ') === 0) {
// FIXME: Really use a dialog // FIXME: Really use a dialog
snackbarLogLong(msg.data.replace('DialogMsg: ', '')); snackbarLogLong(msg.data.replace('DialogMsg: ', ''));
} else if(msg.data === 'displayVideo') { } else if (msg.data === 'displayVideo') {
$("#listener").addClass("fullscreen"); $("#listener").addClass("fullscreen");
}
} }
}
} }

View File

@ -1,468 +1,471 @@
function guuid() { function guuid() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); var r = Math.random() * 16 | 0,
return v.toString(16); v = c == 'x' ? r : (r & 0x3 | 0x8);
}); return v.toString(16);
});
} }
function uniqueid() { function uniqueid() {
return 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { return 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) {
var r = Math.random()*16|0; var r = Math.random() * 16 | 0;
return r.toString(16); return r.toString(16);
}); });
} }
function generateRemoteInputKey() { function generateRemoteInputKey() {
return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) { return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/[x]/g, function(c) {
var r = Math.random()*16|0; var r = Math.random() * 16 | 0;
return r.toString(16); return r.toString(16);
}); });
} }
function generateRemoteInputKeyId() { function generateRemoteInputKeyId() {
return ((Math.random()-0.5) * 0x7FFFFFFF)|0; return ((Math.random() - 0.5) * 0x7FFFFFFF) | 0;
} }
function getConnectedGamepadMask() { function getConnectedGamepadMask() {
var count = 0; var count = 0;
var mask = 0; var mask = 0;
var gamepads = navigator.getGamepads ? navigator.getGamepads() : []; var gamepads = navigator.getGamepads ? navigator.getGamepads() : [];
for (var i = 0; i < gamepads.length; i++) { for (var i = 0; i < gamepads.length; i++) {
var gamepad = gamepads[i]; var gamepad = gamepads[i];
if (gamepad) { if (gamepad) {
// See logic in gamepad.cpp // See logic in gamepad.cpp
// These must stay in sync! // These must stay in sync!
if (!gamepad.connected) { if (!gamepad.connected) {
// Not connected // Not connected
continue; continue;
} }
if (gamepad.timestamp == 0) { if (gamepad.timestamp == 0) {
// On some platforms, Chrome returns "connected" pads that // On some platforms, Chrome returns "connected" pads that
// really aren't, so timestamp stays at zero. To work around this, // 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 // we'll only count gamepads that have a non-zero timestamp in our
// controller index. // controller index.
continue; continue;
} }
mask |= 1 << count++; mask |= 1 << count++;
}
} }
}
console.log('%c[utils.js, getConnectedGamepadMask]', 'color:gray;', 'Detected '+count+' gamepads'); console.log('%c[utils.js, getConnectedGamepadMask]', 'color:gray;', 'Detected ' + count + ' gamepads');
return mask; return mask;
} }
String.prototype.toHex = function() { String.prototype.toHex = function() {
var hex = ''; var hex = '';
for(var i = 0; i < this.length; i++) { for (var i = 0; i < this.length; i++) {
hex += '' + this.charCodeAt(i).toString(16); hex += '' + this.charCodeAt(i).toString(16);
} }
return hex; return hex;
} }
function NvHTTP(address, clientUid, userEnteredAddress = '') { function NvHTTP(address, clientUid, userEnteredAddress = '') {
console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this); console.log('%c[utils.js, NvHTTP Object]', 'color: gray;', this);
this.address = address; this.address = address;
this.paired = false; this.paired = false;
this.currentGame = 0; this.currentGame = 0;
this.serverMajorVersion = 0; this.serverMajorVersion = 0;
this.appVersion = ''; this.appVersion = '';
this.clientUid = clientUid; this.clientUid = clientUid;
this._pollCount = 0; this._pollCount = 0;
this._consecutivePollFailures = 0; this._consecutivePollFailures = 0;
this.online = false; this.online = false;
this.userEnteredAddress = userEnteredAddress; // if the user entered an address, we keep it on hand to try when polling this.userEnteredAddress = userEnteredAddress; // if the user entered an address, we keep it on hand to try when polling
this.serverUid = ''; this.serverUid = '';
this.GfeVersion = ''; this.GfeVersion = '';
this.supportedDisplayModes = {}; // key: y-resolution:x-resolution, value: array of supported framerates (only ever seen 30 or 60, here) this.supportedDisplayModes = {}; // key: y-resolution:x-resolution, value: array of supported framerates (only ever seen 30 or 60, here)
this.gputype = ''; this.gputype = '';
this.numofapps = 0; this.numofapps = 0;
this.hostname = address; this.hostname = address;
this.externalIP = ''; this.externalIP = '';
this._pollCompletionCallbacks = []; this._pollCompletionCallbacks = [];
_self = this; _self = this;
}; };
function _arrayBufferToBase64( buffer ) { function _arrayBufferToBase64(buffer) {
var binary = ''; var binary = '';
var bytes = new Uint8Array( buffer ); var bytes = new Uint8Array(buffer);
var len = bytes.byteLength; var len = bytes.byteLength;
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] ); binary += String.fromCharCode(bytes[i]);
} }
return window.btoa( binary ); return window.btoa(binary);
} }
function _base64ToArrayBuffer(base64) { function _base64ToArrayBuffer(base64) {
var binary_string = window.atob(base64); var binary_string = window.atob(base64);
var len = binary_string.length; var len = binary_string.length;
var bytes = new Uint8Array( len ); var bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) { for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i); bytes[i] = binary_string.charCodeAt(i);
} }
return bytes.buffer; return bytes.buffer;
} }
NvHTTP.prototype = { NvHTTP.prototype = {
refreshServerInfo: function () { refreshServerInfo: function() {
// try HTTPS first // try HTTPS first
return sendMessage('openUrl', [ this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { return sendMessage('openUrl', [this._baseUrlHttps + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) {
if (!this._parseServerInfo(ret)) { // if that fails if (!this._parseServerInfo(ret)) { // if that fails
// try HTTP as a failover. Useful to clients who aren't paired yet // 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) { return sendMessage('openUrl', [this._baseUrlHttp + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) {
this._parseServerInfo(retHttp); this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this)); }.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 // 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) { refreshServerInfoAtAddress: function(givenAddress) {
// try HTTPS first // try HTTPS first
return sendMessage('openUrl', [ 'https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) { return sendMessage('openUrl', ['https://' + givenAddress + ':47984' + '/serverinfo?' + this._buildUidStr(), false]).then(function(ret) {
if (!this._parseServerInfo(ret)) { // if that fails 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'); 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 // 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 sendMessage('openUrl', ['http://' + givenAddress + ':47989' + '/serverinfo?' + this._buildUidStr(), false]).then(function(retHttp) {
return this._parseServerInfo(retHttp); return this._parseServerInfo(retHttp);
}.bind(this));
}
}.bind(this)); }.bind(this));
}, }
}.bind(this));
},
// called every few seconds to poll the server for updated info // called every few seconds to poll the server for updated info
pollServer: function(onComplete) { pollServer: function(onComplete) {
// Pend this callback on completion // Pend this callback on completion
this._pollCompletionCallbacks.push(onComplete); this._pollCompletionCallbacks.push(onComplete);
// Check if a poll was already in progress // Check if a poll was already in progress
if (this._pollCompletionCallbacks.length > 1) { if (this._pollCompletionCallbacks.length > 1) {
// Don't start another. The one in progress will // Don't start another. The one in progress will
// alert our caller too. // 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; return;
} }
this.selectServerAddress(function(successfulAddress) { // otherwise, put it in our cache, then return it
// Successfully determined server address. Update base URL. sendMessage('openUrl', [
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', [
this._baseUrlHttps + this._baseUrlHttps +
'/launch?' + this._buildUidStr() + '/appasset?' + this._buildUidStr() +
'&appid=' + appId + '&appid=' + appId +
'&mode=' + mode + '&AssetType=2&AssetIdx=0',
'&additionalStates=1&sops=' + sops + true
'&rikey=' + rikey + ]).then(function(boxArtBuffer) {
'&rikeyid=' + rikeyid + var reader = new FileReader();
'&localAudioPlayMode=' + localAudio + reader.onloadend = function() {
'&surroundAudioInfo=' + surroundAudioInfo + var obj = {};
'&remoteControllersBitmap=' + gamepadMask + obj['boxart-' + appId] = this.result;
'&gcmap=' + gamepadMask, chrome.storage.local.set(obj, function(onSuccess) {});
false console.log('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Returning network-fetched box art');
]).then(function (ret) { resolve(this.result);
return true; }
}); reader.readAsDataURL(new Blob([boxArtBuffer], {
}, type: "image/png"
}));
resumeApp: function (rikey, rikeyid, surroundAudioInfo) { }.bind(this), function(error) {
return sendMessage('openUrl', [ console.error('%c[utils.js, utils.js, getBoxArt]', 'color: gray;', 'Box-art request failed!', error);
this._baseUrlHttps + reject(error);
'/resume?' + this._buildUidStr() + return;
'&rikey=' + rikey + }.bind(this));
'&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)); }.bind(this));
}, }.bind(this));
_buildUidStr: function () { } else { // shouldn't run because we always have chrome.storage, but I'm not going to antagonize other browsers
return 'uniqueid=' + this.clientUid + '&uuid=' + guuid(); 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) { launchApp: function(appId, mode, sops, rikey, rikeyid, localAudio, surroundAudioInfo, gamepadMask) {
return $($.parseXML(xmlData.toString())); 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()));
},
}; };

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 256" width="512" height="256"><defs><clipPath id="_clipPath_yIRtlut3od2o3W0V0tyVSEn4O7rQwUEb"><rect width="512" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_yIRtlut3od2o3W0V0tyVSEn4O7rQwUEb)"><g id="Group"><defs><filter id="WcPFsRr92txvAquqtQLJDA42eOuH20Tm" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#WcPFsRr92txvAquqtQLJDA42eOuH20Tm)"><path d="M 184 28 L 328 28 C 329.656 28 331 29.344 331 31 L 331 225 C 331 226.656 329.656 228 328 228 L 184 228 C 182.344 228 181 226.656 181 225 L 181 31 C 181 29.344 182.344 28 184 28 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_add_circle_48px"><path d=" M 232 104 L 280 104 L 280 152 L 232 152 L 232 104 Z " fill="none"/><path d=" M 256 108 C 244.95 108 236 116.95 236 128 C 236 139.05 244.95 148 256 148 C 267.05 148 276 139.05 276 128 C 276 116.95 267.05 108 256 108 Z M 266 130 L 258 130 L 258 138 L 254 138 L 254 130 L 246 130 L 246 126 L 254 126 L 254 118 L 258 118 L 258 126 L 266 126 L 266 130 Z " fill="rgb(0,163,198)"/></g></g><g id="Group"><defs><filter id="6XR36XqPb13ScFas2P9OjS9OpNZIfllA" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#6XR36XqPb13ScFas2P9OjS9OpNZIfllA)"><path d="M 54.75 53 L 162.75 53 C 163.992 53 165 54.008 165 55.25 L 165 200.75 C 165 201.992 163.992 203 162.75 203 L 54.75 203 C 53.508 203 52.5 201.992 52.5 200.75 L 52.5 55.25 C 52.5 54.008 53.508 53 54.75 53 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_gamepad_48px"><path d=" M 96.75 116 L 120.75 116 L 120.75 140 L 96.75 140 L 96.75 116 Z " fill="none"/><path d=" M 111.75 123.5 L 111.75 118 L 105.75 118 L 105.75 123.5 L 108.75 126.5 L 111.75 123.5 Z M 104.25 125 L 98.75 125 L 98.75 131 L 104.25 131 L 107.25 128 L 104.25 125 Z M 105.75 132.5 L 105.75 138 L 111.75 138 L 111.75 132.5 L 108.75 129.5 L 105.75 132.5 Z M 113.25 125 L 110.25 128 L 113.25 131 L 118.75 131 L 118.75 125 L 113.25 125 Z " fill="rgb(39,39,39)"/></g></g><g id="Group"><defs><filter id="UdSh6fub9y3Hcxceeait0RXX4mwjJMX0" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#UdSh6fub9y3Hcxceeait0RXX4mwjJMX0)"><path d="M 349.25 53 L 457.25 53 C 458.492 53 459.5 54.008 459.5 55.25 L 459.5 200.75 C 459.5 201.992 458.492 203 457.25 203 L 349.25 203 C 348.008 203 347 201.992 347 200.75 L 347 55.25 C 347 54.008 348.008 53 349.25 53 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_gamepad_48px"><path d=" M 391.25 116 L 415.25 116 L 415.25 140 L 391.25 140 L 391.25 116 Z " fill="none"/><path d=" M 406.25 123.5 L 406.25 118 L 400.25 118 L 400.25 123.5 L 403.25 126.5 L 406.25 123.5 Z M 398.75 125 L 393.25 125 L 393.25 131 L 398.75 131 L 401.75 128 L 398.75 125 Z M 400.25 132.5 L 400.25 138 L 406.25 138 L 406.25 132.5 L 403.25 129.5 L 400.25 132.5 Z M 407.75 125 L 404.75 128 L 407.75 131 L 413.25 131 L 413.25 125 L 407.75 125 Z " fill="rgb(39,39,39)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 512 256" width="512" height="256"><defs><clipPath id="_clipPath_q3Ua5WPG9X3dA70MRZ1ug2ARElMiysSJ"><rect width="512" height="256"/></clipPath></defs><g clip-path="url(#_clipPath_q3Ua5WPG9X3dA70MRZ1ug2ARElMiysSJ)"><g id="Group"><defs><filter id="cHVudzbrBQ2CJG0jNPFm7Epib205WyaO" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#cHVudzbrBQ2CJG0jNPFm7Epib205WyaO)"><path d="M 184 28 L 328 28 C 329.656 28 331 29.344 331 31 L 331 225 C 331 226.656 329.656 228 328 228 L 184 228 C 182.344 228 181 226.656 181 225 L 181 31 C 181 29.344 182.344 28 184 28 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_warning_48px"><path d=" M 232 104 L 280 104 L 280 152 L 232 152 L 232 104 Z " fill="none"/><path d=" M 234 146 L 278 146 L 256 108 L 234 146 Z M 258 140 L 254 140 L 254 136 L 258 136 L 258 140 Z M 258 132 L 254 132 L 254 124 L 258 124 L 258 132 Z " fill="rgb(244,67,54)"/></g></g><g id="Group"><defs><filter id="mus5hysAZhLwu6bXMOLrMCeg8QRvpyK5" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#mus5hysAZhLwu6bXMOLrMCeg8QRvpyK5)"><path d="M 54.75 53 L 162.75 53 C 163.992 53 165 54.008 165 55.25 L 165 200.75 C 165 201.992 163.992 203 162.75 203 L 54.75 203 C 53.508 203 52.5 201.992 52.5 200.75 L 52.5 55.25 C 52.5 54.008 53.508 53 54.75 53 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_gamepad_48px"><path d=" M 96.75 116 L 120.75 116 L 120.75 140 L 96.75 140 L 96.75 116 Z " fill="none"/><path d=" M 111.75 123.5 L 111.75 118 L 105.75 118 L 105.75 123.5 L 108.75 126.5 L 111.75 123.5 Z M 104.25 125 L 98.75 125 L 98.75 131 L 104.25 131 L 107.25 128 L 104.25 125 Z M 105.75 132.5 L 105.75 138 L 111.75 138 L 111.75 132.5 L 108.75 129.5 L 105.75 132.5 Z M 113.25 125 L 110.25 128 L 113.25 131 L 118.75 131 L 118.75 125 L 113.25 125 Z " fill="rgb(39,39,39)"/></g></g><g id="Group"><defs><filter id="secyACecQxmUzvUd03msvhsmzkojlvlZ" x="-200%" y="-200%" width="400%" height="400%"><feOffset xmlns="http://www.w3.org/2000/svg" in="SourceAlpha" result="offOut" dx="0" dy="0"/><feGaussianBlur xmlns="http://www.w3.org/2000/svg" in="offOut" result="blurOut" stdDeviation="2.5"/><feComponentTransfer xmlns="http://www.w3.org/2000/svg" in="blurOut" result="opacOut"><feFuncA xmlns="http://www.w3.org/2000/svg" type="table" tableValues="0 0.5"/></feComponentTransfer><feBlend xmlns="http://www.w3.org/2000/svg" in="SourceGraphic" in2="opacOut" mode="normal"/></filter></defs><g filter="url(#secyACecQxmUzvUd03msvhsmzkojlvlZ)"><path d="M 349.25 53 L 457.25 53 C 458.492 53 459.5 54.008 459.5 55.25 L 459.5 200.75 C 459.5 201.992 458.492 203 457.25 203 L 349.25 203 C 348.008 203 347 201.992 347 200.75 L 347 55.25 C 347 54.008 348.008 53 349.25 53 Z" style="stroke:none;fill:#1D1D1D;stroke-miterlimit:10;"/></g><g id="ic_gamepad_48px"><path d=" M 391.25 116 L 415.25 116 L 415.25 140 L 391.25 140 L 391.25 116 Z " fill="none"/><path d=" M 406.25 123.5 L 406.25 118 L 400.25 118 L 400.25 123.5 L 403.25 126.5 L 406.25 123.5 Z M 398.75 125 L 393.25 125 L 393.25 131 L 398.75 131 L 401.75 128 L 398.75 125 Z M 400.25 132.5 L 400.25 138 L 406.25 138 L 406.25 132.5 L 403.25 129.5 L 400.25 132.5 Z M 407.75 125 L 404.75 128 L 407.75 131 L 413.25 131 L 413.25 125 L 407.75 125 Z " fill="rgb(39,39,39)"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 300 400" width="300" height="400"><defs><clipPath id="_clipPath_7VrBmLEdSmb7vTXt6k0koa8xYu1zCaFz"><rect width="300" height="400"/></clipPath></defs><g clip-path="url(#_clipPath_7VrBmLEdSmb7vTXt6k0koa8xYu1zCaFz)"><rect width="300" height="400" style="fill:rgb(29,29,29)"/><g id="ic_warning_black_48px"><path d=" M 102 152 L 198 152 L 198 248 L 102 248 L 102 152 Z " fill="none"/><path d=" M 106 236 L 194 236 L 150 160 L 106 236 Z M 154 224 L 146 224 L 146 216 L 154 216 L 154 224 Z M 154 208 L 146 208 L 146 192 L 154 192 L 154 208 Z " fill="rgb(39,39,39)"/></g></g></svg>

After

Width:  |  Height:  |  Size: 762 B

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!-- Generator: Gravit.io --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="isolation:isolate" viewBox="0 0 300 400" width="300" height="400"><defs><clipPath id="_clipPath_BVjMtbR4dEXaBuo9qBU4zJA4ZPJHGfxW"><rect width="300" height="400"/></clipPath></defs><g clip-path="url(#_clipPath_BVjMtbR4dEXaBuo9qBU4zJA4ZPJHGfxW)"><clipPath id="_clipPath_UBQAdzuop4HX8nHiyTz6R71FUYdEnoXn"><rect x="0" y="0" width="300" height="400" transform="matrix(1,0,0,1,0,0)" fill="rgb(255,255,255)"/></clipPath><g clip-path="url(#_clipPath_UBQAdzuop4HX8nHiyTz6R71FUYdEnoXn)"><g id="Group"><rect x="0" y="0" width="300" height="400" transform="matrix(1,0,0,1,0,0)" fill="rgb(29,29,29)"/><g id="ic_gamepad_48px"><path d=" M 101 151 L 199 151 L 199 249 L 101 249 L 101 151 Z " fill="none"/><path d=" M 162.25 181.625 L 162.25 159.167 L 137.75 159.167 L 137.75 181.625 L 150 193.875 L 162.25 181.625 Z M 131.625 187.75 L 109.167 187.75 L 109.167 212.25 L 131.625 212.25 L 143.875 200 L 131.625 187.75 Z M 137.75 218.375 L 137.75 240.833 L 162.25 240.833 L 162.25 218.375 L 150 206.125 L 137.75 218.375 Z M 168.375 187.75 L 156.125 200 L 168.375 212.25 L 190.833 212.25 L 190.833 187.75 L 168.375 187.75 Z " fill="rgb(39,39,39)"/></g></g></g></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB