mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 10:51:44 +02:00
Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
d9a14f9d83 | |||
18dd006aad | |||
f74de11e10 | |||
2a71e17d2d | |||
2a85dd574e | |||
9692286f1e | |||
26498efa7c | |||
d1c724ff2c | |||
eec41c58b6 | |||
38cc78e0da | |||
3cf029818e | |||
5104cf33b4 | |||
771111d1f8 | |||
79e0661977 | |||
2dc3097737 | |||
0833afc0a2 | |||
ca8b3cfbd8 | |||
bd852c788d |
32
README.md
32
README.md
@ -25,8 +25,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/65ee4193-c31d-46fb-b580-196614246ee6">
|
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/0eedde97-74c7-44df-bc89-2ebf8edb6e2c">
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956">
|
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956">
|
||||||
<br>
|
<br>
|
||||||
@ -51,7 +50,9 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
|||||||
|
|
||||||
### Server
|
### Server
|
||||||
- **Set the region of streaming server**
|
- **Set the region of streaming server**
|
||||||
> Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes.
|
> Connect to another server instead of the default one.
|
||||||
|
> It's not using VPN.
|
||||||
|
> ["Can I get banned for using this?"](#faq)
|
||||||
- **Preferred game's language**
|
- **Preferred game's language**
|
||||||
> If the game doesn't support this language, it will use the same language as xCloud's website.
|
> If the game doesn't support this language, it will use the same language as xCloud's website.
|
||||||
- **Prefer IPv6 server**
|
- **Prefer IPv6 server**
|
||||||
@ -91,6 +92,21 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
|||||||
>
|
>
|
||||||
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
|
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
|
||||||
|
|
||||||
|
### Loading screen
|
||||||
|
- Show game art
|
||||||
|
> Replace the black background with game art if it's available.
|
||||||
|
- Show the estimated wait time
|
||||||
|
> The time is estimated by the server.
|
||||||
|
> It's not 100% correct: you might get in the game sooner or later.
|
||||||
|
> Don't be mad when the estimated time is inaccurate.
|
||||||
|
> Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info.
|
||||||
|
- Show/hide the rocket animation
|
||||||
|
> Always show/Hide when queuing/Always hide.
|
||||||
|
> Hide this animation might save some battery life while queuing.
|
||||||
|
|
||||||
|
<img height="300" alt="Loading screen" src="https://github.com/redphx/better-xcloud/assets/96280/46074b14-1abb-466d-a859-d46ad4dac2fd">
|
||||||
|
|
||||||
|
|
||||||
### UI
|
### UI
|
||||||
- **Simplify Stream's menu**
|
- **Simplify Stream's menu**
|
||||||
> Hide the labels of the menu buttons.
|
> Hide the labels of the menu buttons.
|
||||||
@ -113,14 +129,16 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
|||||||
> Similar to (but not as good as) the "Clarity Boost" of xCloud on Edge browser. [Demo video](https://youtu.be/ZhW2choAHUs).
|
> Similar to (but not as good as) the "Clarity Boost" of xCloud on Edge browser. [Demo video](https://youtu.be/ZhW2choAHUs).
|
||||||
> Also known as poor man's "Clarity Boost".
|
> Also known as poor man's "Clarity Boost".
|
||||||
> Affects the stream's performance, uses more battery, and may causes frames to drop (especially on lower-end devices).
|
> Affects the stream's performance, uses more battery, and may causes frames to drop (especially on lower-end devices).
|
||||||
|
> Works with Chrome/Chromium browsers.
|
||||||
> Doesn't work with Safari.
|
> Doesn't work with Safari.
|
||||||
>
|
>
|
||||||
> 
|
> 
|
||||||
|
> *(click to enlarge)*
|
||||||
|
|
||||||
- **Stretch video to full sctreen**
|
- **Stretch video to full sctreen**
|
||||||
> Useful when you don't have a 16:9 screen
|
> Useful when you don't have a 16:9 screen
|
||||||
- **Adjust video filters**
|
- **Adjust video filters**
|
||||||
> Brightness/Contrast/Saturation.
|
> Brightness/Contrast/Saturation.
|
||||||
> ⚠️ These features don't work when xCloud's "Clarity Boost" feature is ON ([#64](https://github.com/redphx/better-xcloud/issues/64)).
|
> ⚠️ These features don't work when xCloud's "Clarity Boost" feature is ON ([#64](https://github.com/redphx/better-xcloud/issues/64)).
|
||||||
- **Display stream's statuses**
|
- **Display stream's statuses**
|
||||||
> Region/Server/Codecs/Resolution...
|
> Region/Server/Codecs/Resolution...
|
||||||
@ -185,7 +203,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/0d4abb6b-49ab-4c9a-a52d-df7e396d2145">
|
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/142625ea-20ab-4392-a111-0c5bc08bae09">
|
||||||
|
|
||||||
- While playing > `...` > `Stream Stats`.
|
- While playing > `...` > `Stream Stats`.
|
||||||
- Double-click on the stats bar to show the Settings dialog.
|
- Double-click on the stats bar to show the Settings dialog.
|
||||||
@ -195,8 +213,8 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
|||||||
|
|
||||||
| Abbr. | Full name | Explain |
|
| Abbr. | Full name | Explain |
|
||||||
|------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
|
|------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|
| PING | Ping | The number of seconds it takes for data to be sent from your device to the server and back over (the correct term is "Round Trip Time") |
|
||||||
| FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (may not be the same as the FPS of the game) |
|
| FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (may not be the same as the FPS of the game) |
|
||||||
| RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (similar to ping, lower is better) |
|
|
||||||
| DT | Decode Time | The average time it took to decode one frame in the last second (bugged in Kiwi Browser [#26](https://github.com/redphx/better-xcloud/issues/26)) |
|
| DT | Decode Time | The average time it took to decode one frame in the last second (bugged in Kiwi Browser [#26](https://github.com/redphx/better-xcloud/issues/26)) |
|
||||||
| BR | Bitrate | The amount of data the server sent to your device in the last second |
|
| BR | Bitrate | The amount of data the server sent to your device in the last second |
|
||||||
| PL | Packets Lost | The total number of packets lost |
|
| PL | Packets Lost | The total number of packets lost |
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.12
|
// @version 1.14.1
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.12
|
// @version 1.14.1
|
||||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||||
// @author redphx
|
// @author redphx
|
||||||
// @license MIT
|
// @license MIT
|
||||||
@ -13,7 +13,7 @@
|
|||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const SCRIPT_VERSION = '1.12';
|
const SCRIPT_VERSION = '1.14.1';
|
||||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||||
|
|
||||||
console.log(`[Better xCloud] readyState: ${document.readyState}`);
|
console.log(`[Better xCloud] readyState: ${document.readyState}`);
|
||||||
@ -110,9 +110,9 @@ var STREAM_WEBRTC;
|
|||||||
var $STREAM_VIDEO;
|
var $STREAM_VIDEO;
|
||||||
var $SCREENSHOT_CANVAS;
|
var $SCREENSHOT_CANVAS;
|
||||||
var GAME_TITLE_ID;
|
var GAME_TITLE_ID;
|
||||||
|
var APP_CONTEXT;
|
||||||
|
|
||||||
const HAS_TOUCH_SUPPORT = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
const HAS_TOUCH_SUPPORT = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
||||||
const TOUCH_SUPPORTED_GAME_IDS = new Set();
|
|
||||||
|
|
||||||
// Credit: https://phosphoricons.com
|
// Credit: https://phosphoricons.com
|
||||||
const ICON_VIDEO_SETTINGS = '<path d="M16 9.144A6.89 6.89 0 0 0 9.144 16 6.89 6.89 0 0 0 16 22.856 6.89 6.89 0 0 0 22.856 16 6.9 6.9 0 0 0 16 9.144zm0 11.427c-2.507 0-4.571-2.064-4.571-4.571s2.064-4.571 4.571-4.571 4.571 2.064 4.571 4.571-2.064 4.571-4.571 4.571zm15.704-7.541c-.065-.326-.267-.607-.556-.771l-4.26-2.428-.017-4.802c-.001-.335-.15-.652-.405-.868-1.546-1.307-3.325-2.309-5.245-2.953-.306-.103-.641-.073-.923.085L16 3.694l-4.302-2.407c-.282-.158-.618-.189-.924-.086a16.02 16.02 0 0 0-5.239 2.964 1.14 1.14 0 0 0-.403.867L5.109 9.84.848 12.268a1.14 1.14 0 0 0-.555.771 15.22 15.22 0 0 0 0 5.936c.064.326.267.607.555.771l4.261 2.428.017 4.802c.001.335.149.652.403.868 1.546 1.307 3.326 2.309 5.245 2.953.306.103.641.073.923-.085L16 28.306l4.302 2.407a1.13 1.13 0 0 0 .558.143 1.18 1.18 0 0 0 .367-.059c1.917-.648 3.695-1.652 5.239-2.962.255-.216.402-.532.405-.866l.021-4.807 4.261-2.428a1.14 1.14 0 0 0 .555-.771 15.21 15.21 0 0 0-.003-5.931zm-2.143 4.987l-4.082 2.321a1.15 1.15 0 0 0-.429.429l-.258.438a1.13 1.13 0 0 0-.174.601l-.022 4.606a13.71 13.71 0 0 1-3.623 2.043l-4.117-2.295a1.15 1.15 0 0 0-.559-.143h-.546c-.205-.005-.407.045-.586.143l-4.119 2.3a13.74 13.74 0 0 1-3.634-2.033l-.016-4.599a1.14 1.14 0 0 0-.174-.603l-.257-.437c-.102-.182-.249-.333-.429-.437l-4.085-2.328a12.92 12.92 0 0 1 0-4.036l4.074-2.325a1.15 1.15 0 0 0 .429-.429l.258-.438a1.14 1.14 0 0 0 .175-.601l.021-4.606a13.7 13.7 0 0 1 3.625-2.043l4.11 2.295a1.14 1.14 0 0 0 .585.143h.52c.205.005.407-.045.586-.143l4.119-2.3a13.74 13.74 0 0 1 3.634 2.033l.016 4.599a1.14 1.14 0 0 0 .174.603l.257.437c.102.182.249.333.429.438l4.085 2.327a12.88 12.88 0 0 1 .007 4.041h.007z" fill-rule="nonzero"/>';
|
const ICON_VIDEO_SETTINGS = '<path d="M16 9.144A6.89 6.89 0 0 0 9.144 16 6.89 6.89 0 0 0 16 22.856 6.89 6.89 0 0 0 22.856 16 6.9 6.9 0 0 0 16 9.144zm0 11.427c-2.507 0-4.571-2.064-4.571-4.571s2.064-4.571 4.571-4.571 4.571 2.064 4.571 4.571-2.064 4.571-4.571 4.571zm15.704-7.541c-.065-.326-.267-.607-.556-.771l-4.26-2.428-.017-4.802c-.001-.335-.15-.652-.405-.868-1.546-1.307-3.325-2.309-5.245-2.953-.306-.103-.641-.073-.923.085L16 3.694l-4.302-2.407c-.282-.158-.618-.189-.924-.086a16.02 16.02 0 0 0-5.239 2.964 1.14 1.14 0 0 0-.403.867L5.109 9.84.848 12.268a1.14 1.14 0 0 0-.555.771 15.22 15.22 0 0 0 0 5.936c.064.326.267.607.555.771l4.261 2.428.017 4.802c.001.335.149.652.403.868 1.546 1.307 3.326 2.309 5.245 2.953.306.103.641.073.923-.085L16 28.306l4.302 2.407a1.13 1.13 0 0 0 .558.143 1.18 1.18 0 0 0 .367-.059c1.917-.648 3.695-1.652 5.239-2.962.255-.216.402-.532.405-.866l.021-4.807 4.261-2.428a1.14 1.14 0 0 0 .555-.771 15.21 15.21 0 0 0-.003-5.931zm-2.143 4.987l-4.082 2.321a1.15 1.15 0 0 0-.429.429l-.258.438a1.13 1.13 0 0 0-.174.601l-.022 4.606a13.71 13.71 0 0 1-3.623 2.043l-4.117-2.295a1.15 1.15 0 0 0-.559-.143h-.546c-.205-.005-.407.045-.586.143l-4.119 2.3a13.74 13.74 0 0 1-3.634-2.033l-.016-4.599a1.14 1.14 0 0 0-.174-.603l-.257-.437c-.102-.182-.249-.333-.429-.437l-4.085-2.328a12.92 12.92 0 0 1 0-4.036l4.074-2.325a1.15 1.15 0 0 0 .429-.429l.258-.438a1.14 1.14 0 0 0 .175-.601l.021-4.606a13.7 13.7 0 0 1 3.625-2.043l4.11 2.295a1.14 1.14 0 0 0 .585.143h.52c.205.005.407-.045.586-.143l4.119-2.3a13.74 13.74 0 0 1 3.634 2.033l.016 4.599a1.14 1.14 0 0 0 .174.603l.257.437c.102.182.249.333.429.438l4.085 2.327a12.88 12.88 0 0 1 .007 4.041h.007z" fill-rule="nonzero"/>';
|
||||||
@ -120,6 +120,237 @@ const ICON_STREAM_STATS = '<path d="M27.295 9.31C24.303 6.313 20.234 4.631 16 4.
|
|||||||
const ICON_SCREENSHOT_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMjguMzA4IDUuMDM4aC00LjI2NWwtMi4wOTctMy4xNDVhMS4yMyAxLjIzIDAgMCAwLTEuMDIzLS41NDhoLTkuODQ2YTEuMjMgMS4yMyAwIDAgMC0xLjAyMy41NDhMNy45NTYgNS4wMzhIMy42OTJBMy43MSAzLjcxIDAgMCAwIDAgOC43MzF2MTcuMjMxYTMuNzEgMy43MSAwIDAgMCAzLjY5MiAzLjY5MmgyNC42MTVBMy43MSAzLjcxIDAgMCAwIDMyIDI1Ljk2MlY4LjczMWEzLjcxIDMuNzEgMCAwIDAtMy42OTItMy42OTJ6bS02Ljc2OSAxMS42OTJjMCAzLjAzOS0yLjUgNS41MzgtNS41MzggNS41MzhzLTUuNTM4LTIuNS01LjUzOC01LjUzOCAyLjUtNS41MzggNS41MzgtNS41MzggNS41MzggMi41IDUuNTM4IDUuNTM4eiIvPjwvc3ZnPgo=';
|
const ICON_SCREENSHOT_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMjguMzA4IDUuMDM4aC00LjI2NWwtMi4wOTctMy4xNDVhMS4yMyAxLjIzIDAgMCAwLTEuMDIzLS41NDhoLTkuODQ2YTEuMjMgMS4yMyAwIDAgMC0xLjAyMy41NDhMNy45NTYgNS4wMzhIMy42OTJBMy43MSAzLjcxIDAgMCAwIDAgOC43MzF2MTcuMjMxYTMuNzEgMy43MSAwIDAgMCAzLjY5MiAzLjY5MmgyNC42MTVBMy43MSAzLjcxIDAgMCAwIDMyIDI1Ljk2MlY4LjczMWEzLjcxIDMuNzEgMCAwIDAtMy42OTItMy42OTJ6bS02Ljc2OSAxMS42OTJjMCAzLjAzOS0yLjUgNS41MzgtNS41MzggNS41MzhzLTUuNTM4LTIuNS01LjUzOC01LjUzOCAyLjUtNS41MzggNS41MzgtNS41MzggNS41MzggMi41IDUuNTM4IDUuNTM4eiIvPjwvc3ZnPgo=';
|
||||||
|
|
||||||
|
|
||||||
|
class TitlesInfo {
|
||||||
|
static #INFO = {};
|
||||||
|
|
||||||
|
static get(titleId) {
|
||||||
|
return TitlesInfo.#INFO[titleId];
|
||||||
|
}
|
||||||
|
|
||||||
|
static update(titleId, info) {
|
||||||
|
TitlesInfo.#INFO[titleId] = TitlesInfo.#INFO[titleId] || {};
|
||||||
|
Object.assign(TitlesInfo.#INFO[titleId], info);
|
||||||
|
}
|
||||||
|
|
||||||
|
static saveFromTitleInfo(titleInfo) {
|
||||||
|
const details = titleInfo.details;
|
||||||
|
TitlesInfo.#INFO[details.productId] = {
|
||||||
|
titleId: titleInfo.titleId,
|
||||||
|
// Has more than one input type -> must have touch support
|
||||||
|
hasTouchSupport: (details.supportedInputTypes.length > 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static saveFromCatalogInfo(catalogInfo) {
|
||||||
|
const titleId = catalogInfo.StoreId;
|
||||||
|
|
||||||
|
TitlesInfo.update(titleId, {
|
||||||
|
imageHero: catalogInfo.Image_Hero ? catalogInfo.Image_Hero.URL : '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static hasTouchSupport(titleId) {
|
||||||
|
const gameInfo = TitlesInfo.#INFO[titleId] || {};
|
||||||
|
return !!gameInfo.hasTouchSupport;
|
||||||
|
}
|
||||||
|
|
||||||
|
static requestCatalogInfo(titleId, callback) {
|
||||||
|
const url = `https://catalog.gamepass.com/v3/products?market=${APP_CONTEXT.marketInfo.market}&language=${APP_CONTEXT.marketInfo.locale}&hydration=RemoteHighSapphire0`;
|
||||||
|
const appVersion = document.querySelector('meta[name=gamepass-app-version]').content;
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Ms-Cv': APP_CONTEXT.telemetryInfo.initialCv,
|
||||||
|
'Calling-App-Name': 'Xbox Cloud Gaming Web',
|
||||||
|
'Calling-App-Version': appVersion,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
Products: [titleId],
|
||||||
|
}),
|
||||||
|
}).then(resp => {
|
||||||
|
callback && callback(TitlesInfo.get(titleId));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LoadingScreen {
|
||||||
|
static #$bgStyle;
|
||||||
|
static #$waitTimeBox;
|
||||||
|
|
||||||
|
static #waitTimeInterval;
|
||||||
|
static #orgWebTitle;
|
||||||
|
|
||||||
|
static #secondsToString(seconds) {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
|
||||||
|
const mDisplay = m > 0 ? `${m}m`: '';
|
||||||
|
const sDisplay = `${s}s`.padStart(s >=0 ? 3 : 4, '0');
|
||||||
|
return mDisplay + sDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setup() {
|
||||||
|
// Get titleId from location
|
||||||
|
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!LoadingScreen.#$bgStyle) {
|
||||||
|
const $bgStyle = createElement('style');
|
||||||
|
document.documentElement.appendChild($bgStyle);
|
||||||
|
LoadingScreen.#$bgStyle = $bgStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleId = match[1];
|
||||||
|
const titleInfo = TitlesInfo.get(titleId);
|
||||||
|
if (titleInfo && titleInfo.imageHero) {
|
||||||
|
LoadingScreen.#setBackground(titleInfo.imageHero);
|
||||||
|
} else {
|
||||||
|
TitlesInfo.requestCatalogInfo(titleId, info => {
|
||||||
|
info && info.imageHero && LoadingScreen.#setBackground(info.imageHero);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PREFS.get(Preferences.UI_LOADING_SCREEN_ROCKET) === 'hide') {
|
||||||
|
LoadingScreen.#hideRocket();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static #hideRocket() {
|
||||||
|
let $bgStyle = LoadingScreen.#$bgStyle;
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
#game-stream div[class*=RocketAnimation-module__container] > svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
$bgStyle.textContent += css;
|
||||||
|
}
|
||||||
|
|
||||||
|
static #setBackground(imageUrl) {
|
||||||
|
// Setup style tag
|
||||||
|
let $bgStyle = LoadingScreen.#$bgStyle;
|
||||||
|
|
||||||
|
// Limit max width to reduce image size
|
||||||
|
imageUrl = imageUrl + '?w=1920';
|
||||||
|
|
||||||
|
const css = `
|
||||||
|
#game-stream {
|
||||||
|
background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
background-position: center center !important;
|
||||||
|
background-repeat: no-repeat !important;
|
||||||
|
background-size: cover !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#game-stream rect[width="800"] {
|
||||||
|
transition: opacity 0.3s ease-in-out !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
$bgStyle.textContent += css;
|
||||||
|
|
||||||
|
const bg = new Image();
|
||||||
|
bg.onload = e => {
|
||||||
|
$bgStyle.textContent += `
|
||||||
|
#game-stream rect[width="800"] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
bg.src = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
static setupWaitTime(waitTime) {
|
||||||
|
const CE = createElement;
|
||||||
|
|
||||||
|
// Hide rocket when queing
|
||||||
|
if (PREFS.get(Preferences.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') {
|
||||||
|
LoadingScreen.#hideRocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
let secondsLeft = waitTime;
|
||||||
|
let $countDown;
|
||||||
|
let $estimated;
|
||||||
|
|
||||||
|
LoadingScreen.#orgWebTitle = document.title;
|
||||||
|
|
||||||
|
const endDate = new Date();
|
||||||
|
const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
|
||||||
|
endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
|
||||||
|
|
||||||
|
let endDateStr = endDate.toISOString().slice(0, 19);
|
||||||
|
endDateStr = endDateStr.substring(0, 10) + ' ' + endDateStr.substring(11, 19);
|
||||||
|
endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`;
|
||||||
|
|
||||||
|
let estimatedWaitTime = LoadingScreen.#secondsToString(waitTime);
|
||||||
|
|
||||||
|
let $waitTimeBox = LoadingScreen.#$waitTimeBox;
|
||||||
|
if (!$waitTimeBox) {
|
||||||
|
$waitTimeBox = CE('div', {'class': 'better-xcloud-wait-time-box'},
|
||||||
|
CE('label', {}, 'Estimated finish time'),
|
||||||
|
$estimated = CE('span', {'class': 'better-xcloud-wait-time-estimated'}),
|
||||||
|
CE('label', {}, 'Countdown'),
|
||||||
|
$countDown = CE('span', {'class': 'better-xcloud-wait-time-countdown'}),
|
||||||
|
);
|
||||||
|
|
||||||
|
document.documentElement.appendChild($waitTimeBox);
|
||||||
|
LoadingScreen.#$waitTimeBox = $waitTimeBox;
|
||||||
|
} else {
|
||||||
|
$waitTimeBox.classList.remove('better-xcloud-gone');
|
||||||
|
$estimated = $waitTimeBox.querySelector('.better-xcloud-wait-time-estimated');
|
||||||
|
$countDown = $waitTimeBox.querySelector('.better-xcloud-wait-time-countdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
$estimated.textContent = endDateStr;
|
||||||
|
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft);
|
||||||
|
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`;
|
||||||
|
|
||||||
|
LoadingScreen.#waitTimeInterval = setInterval(() => {
|
||||||
|
secondsLeft--;
|
||||||
|
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft);
|
||||||
|
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`;
|
||||||
|
|
||||||
|
if (secondsLeft <= 0) {
|
||||||
|
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval);
|
||||||
|
LoadingScreen.#waitTimeInterval = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static hide() {
|
||||||
|
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle);
|
||||||
|
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone');
|
||||||
|
|
||||||
|
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
|
||||||
|
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
|
||||||
|
LoadingScreen.#$bgStyle.textContent += `
|
||||||
|
#game-stream {
|
||||||
|
background: #000 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
LoadingScreen.#$bgStyle.textContent += `
|
||||||
|
#game-stream rect[width="800"] {
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static reset() {
|
||||||
|
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone');
|
||||||
|
LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = '');
|
||||||
|
|
||||||
|
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval);
|
||||||
|
LoadingScreen.#waitTimeInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TouchController {
|
class TouchController {
|
||||||
static get #EVENT_SHOW_CONTROLLER() {
|
static get #EVENT_SHOW_CONTROLLER() {
|
||||||
return new MessageEvent('message', {
|
return new MessageEvent('message', {
|
||||||
@ -504,12 +735,19 @@ class StreamBadges {
|
|||||||
|
|
||||||
|
|
||||||
class StreamStats {
|
class StreamStats {
|
||||||
|
static get PING() { return 'ping'; }
|
||||||
|
static get FPS() { return 'fps'; }
|
||||||
|
static get BITRATE() { return 'btr'; }
|
||||||
|
static get DECODE_TIME() { return 'dt'; }
|
||||||
|
static get PACKETS_LOST() { return 'pl'; }
|
||||||
|
static get FRAMES_LOST() { return 'fl'; }
|
||||||
|
|
||||||
static #interval;
|
static #interval;
|
||||||
static #updateInterval = 1000;
|
static #updateInterval = 1000;
|
||||||
|
|
||||||
static #$container;
|
static #$container;
|
||||||
static #$fps;
|
static #$fps;
|
||||||
static #$rtt;
|
static #$ping;
|
||||||
static #$dt;
|
static #$dt;
|
||||||
static #$pl;
|
static #$pl;
|
||||||
static #$fl;
|
static #$fl;
|
||||||
@ -642,23 +880,25 @@ class StreamStats {
|
|||||||
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
|
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
|
||||||
// Round Trip Time
|
// Round Trip Time
|
||||||
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
|
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
|
||||||
StreamStats.#$rtt.textContent = `${roundTripTime}ms`;
|
StreamStats.#$ping.textContent = roundTripTime;
|
||||||
|
|
||||||
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
||||||
grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : '';
|
grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : '';
|
||||||
}
|
}
|
||||||
StreamStats.#$rtt.setAttribute('data-grade', grade);
|
StreamStats.#$ping.setAttribute('data-grade', grade);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static #refreshStyles() {
|
static #refreshStyles() {
|
||||||
|
const PREF_ITEMS = PREFS.get(Preferences.STATS_ITEMS);
|
||||||
const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION);
|
const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION);
|
||||||
const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT);
|
const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT);
|
||||||
const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY);
|
const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY);
|
||||||
const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE);
|
const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE);
|
||||||
|
|
||||||
|
StreamStats.#$container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']');
|
||||||
StreamStats.#$container.setAttribute('data-position', PREF_POSITION);
|
StreamStats.#$container.setAttribute('data-position', PREF_POSITION);
|
||||||
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT);
|
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT);
|
||||||
StreamStats.#$container.style.opacity = PREF_OPACITY + '%';
|
StreamStats.#$container.style.opacity = PREF_OPACITY + '%';
|
||||||
@ -684,19 +924,22 @@ class StreamStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CE = createElement;
|
const CE = createElement;
|
||||||
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'},
|
const STATS = {
|
||||||
CE('label', {}, 'FPS'),
|
[StreamStats.PING]: (StreamStats.#$ping = CE('span', {}, '0')),
|
||||||
StreamStats.#$fps = CE('span', {}, 0),
|
[StreamStats.FPS]: (StreamStats.#$fps = CE('span', {}, '0')),
|
||||||
CE('label', {}, 'RTT'),
|
[StreamStats.BITRATE]: (StreamStats.#$br = CE('span', {}, '0 Mbps')),
|
||||||
StreamStats.#$rtt = CE('span', {}, '0ms'),
|
[StreamStats.DECODE_TIME]: (StreamStats.#$dt = CE('span', {}, '0ms')),
|
||||||
CE('label', {}, 'DT'),
|
[StreamStats.PACKETS_LOST]: (StreamStats.#$pl = CE('span', {}, '0 (0.00%)')),
|
||||||
StreamStats.#$dt = CE('span', {}, '0ms'),
|
[StreamStats.FRAMES_LOST]: (StreamStats.#$fl = CE('span', {}, '0 (0.00%)')),
|
||||||
CE('label', {}, 'BR'),
|
};
|
||||||
StreamStats.#$br = CE('span', {}, '0 Mbps'),
|
|
||||||
CE('label', {}, 'PL'),
|
const $barFragment = document.createDocumentFragment();
|
||||||
StreamStats.#$pl = CE('span', {}, '0 (0.00%)'),
|
for (let statKey in STATS) {
|
||||||
CE('label', {}, 'FL'),
|
const $div = CE('div', {'class': `better-xcloud-stat-${statKey}`}, CE('label', {}, statKey.toUpperCase()), STATS[statKey]);
|
||||||
StreamStats.#$fl = CE('span', {}, '0 (0.00%)'));
|
$barFragment.appendChild($div);
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, $barFragment);
|
||||||
|
|
||||||
let clickTimeout;
|
let clickTimeout;
|
||||||
StreamStats.#$container.addEventListener('mousedown', e => {
|
StreamStats.#$container.addEventListener('mousedown', e => {
|
||||||
@ -718,48 +961,59 @@ class StreamStats {
|
|||||||
const refreshFunc = e => {
|
const refreshFunc = e => {
|
||||||
StreamStats.#refreshStyles()
|
StreamStats.#refreshStyles()
|
||||||
};
|
};
|
||||||
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
|
|
||||||
|
|
||||||
let $close;
|
let $close;
|
||||||
const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING);
|
|
||||||
const $quickGlance = PREFS.toElement(Preferences.STATS_QUICK_GLANCE, e => {
|
|
||||||
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
|
const STATS_UI = {
|
||||||
});
|
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
|
||||||
const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc);
|
'label': 'Show stats when starting the game',
|
||||||
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
|
},
|
||||||
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
|
[Preferences.STATS_QUICK_GLANCE]: {
|
||||||
const $textSize = PREFS.toElement(Preferences.STATS_TEXT_SIZE, refreshFunc);
|
'label': 'Enable "Quick Glance" mode',
|
||||||
|
'onChange': e => {
|
||||||
|
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[Preferences.STATS_ITEMS]: {
|
||||||
|
'label': 'Stats',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_POSITION]: {
|
||||||
|
'label': 'Position',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_TEXT_SIZE]: {
|
||||||
|
'label': 'Text size',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_OPACITY]: {
|
||||||
|
'label': 'Opacity (50-100%)',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_TRANSPARENT]: {
|
||||||
|
'label': 'Transparent background',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
[Preferences.STATS_CONDITIONAL_FORMATTING]: {
|
||||||
|
'label': 'Conditional formatting text color',
|
||||||
|
'onChange': refreshFunc,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const $fragment = document.createDocumentFragment();
|
||||||
|
for (let settingKey in STATS_UI) {
|
||||||
|
const setting = STATS_UI[settingKey];
|
||||||
|
|
||||||
|
$fragment.appendChild(CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${settingKey}`}, setting.label),
|
||||||
|
PREFS.toElement(settingKey, setting.onChange)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
StreamStats.#$settings = CE('div', {'class': 'better-xcloud-stats-settings'},
|
StreamStats.#$settings = CE('div', {'class': 'better-xcloud-stats-settings'},
|
||||||
CE('b', {}, 'Stream Stats Settings'),
|
CE('b', {}, 'Stream Stats Settings'),
|
||||||
CE('div', {},
|
$fragment,
|
||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
|
|
||||||
$showStartup
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_QUICK_GLANCE}`}, 'Enable "Quick Glance" mode'),
|
|
||||||
$quickGlance
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {}, 'Position'),
|
|
||||||
$position
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {}, 'Text size'),
|
|
||||||
$textSize
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_OPACITY}`}, 'Opacity (50-100%)'),
|
|
||||||
$opacity
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_TRANSPARENT}`}, 'Transparent background'),
|
|
||||||
$transparent
|
|
||||||
),
|
|
||||||
CE('div', {},
|
|
||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_CONDITIONAL_FORMATTING}`}, 'Conditional formatting text color'),
|
|
||||||
$formatting
|
|
||||||
),
|
|
||||||
$close = CE('button', {}, 'Close'));
|
$close = CE('button', {}, 'Close'));
|
||||||
|
|
||||||
$close.addEventListener('click', e => StreamStats.hideSettingsUi());
|
$close.addEventListener('click', e => StreamStats.hideSettingsUi());
|
||||||
@ -844,6 +1098,7 @@ class PreloadedState {
|
|||||||
},
|
},
|
||||||
set: (state) => {
|
set: (state) => {
|
||||||
this._state = state;
|
this._state = state;
|
||||||
|
APP_CONTEXT = structuredClone(state.appContext);
|
||||||
|
|
||||||
// Get a list of touch-supported games
|
// Get a list of touch-supported games
|
||||||
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
|
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
|
||||||
@ -853,11 +1108,7 @@ class PreloadedState {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
for (let id in titles) {
|
for (let id in titles) {
|
||||||
const details = titles[id].data.details;
|
TitlesInfo.saveFromTitleInfo(titles[id].data);
|
||||||
// Has move than one input type -> must have touch support
|
|
||||||
if (details.supportedInputTypes.length > 1) {
|
|
||||||
TOUCH_SUPPORTED_GAME_IDS.add(details.productId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -893,6 +1144,10 @@ class Preferences {
|
|||||||
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
||||||
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
||||||
|
|
||||||
|
static get UI_LOADING_SCREEN_GAME_ART() { return 'ui_loading_screen_game_art'; }
|
||||||
|
static get UI_LOADING_SCREEN_WAIT_TIME() { return 'ui_loading_screen_wait_time'; }
|
||||||
|
static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; }
|
||||||
|
|
||||||
static get VIDEO_CLARITY() { return 'video_clarity'; }
|
static get VIDEO_CLARITY() { return 'video_clarity'; }
|
||||||
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; }
|
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; }
|
||||||
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
||||||
@ -901,6 +1156,7 @@ class Preferences {
|
|||||||
|
|
||||||
static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; }
|
static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; }
|
||||||
|
|
||||||
|
static get STATS_ITEMS() { return 'stats_items'; };
|
||||||
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
|
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
|
||||||
static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; }
|
static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; }
|
||||||
static get STATS_POSITION() { return 'stats_position'; }
|
static get STATS_POSITION() { return 'stats_position'; }
|
||||||
@ -1019,6 +1275,20 @@ class Preferences {
|
|||||||
[Preferences.REDUCE_ANIMATIONS]: {
|
[Preferences.REDUCE_ANIMATIONS]: {
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
|
[Preferences.UI_LOADING_SCREEN_GAME_ART]: {
|
||||||
|
'default': true,
|
||||||
|
},
|
||||||
|
[Preferences.UI_LOADING_SCREEN_WAIT_TIME]: {
|
||||||
|
'default': false,
|
||||||
|
},
|
||||||
|
[Preferences.UI_LOADING_SCREEN_ROCKET]: {
|
||||||
|
'default': 'show',
|
||||||
|
'options': {
|
||||||
|
'show': 'Always show',
|
||||||
|
'hide-queue': 'Hide when queuing',
|
||||||
|
'hide': 'Always hide',
|
||||||
|
},
|
||||||
|
},
|
||||||
[Preferences.BLOCK_SOCIAL_FEATURES]: {
|
[Preferences.BLOCK_SOCIAL_FEATURES]: {
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
@ -1041,7 +1311,7 @@ class Preferences {
|
|||||||
[Preferences.VIDEO_CLARITY]: {
|
[Preferences.VIDEO_CLARITY]: {
|
||||||
'default': 0,
|
'default': 0,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 3,
|
'max': 5,
|
||||||
},
|
},
|
||||||
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
|
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
|
||||||
'default': false,
|
'default': false,
|
||||||
@ -1064,6 +1334,18 @@ class Preferences {
|
|||||||
[Preferences.AUDIO_MIC_ON_PLAYING]: {
|
[Preferences.AUDIO_MIC_ON_PLAYING]: {
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[Preferences.STATS_ITEMS]: {
|
||||||
|
'default': [StreamStats.PING, StreamStats.FPS, StreamStats.BITRATE, StreamStats.DECODE_TIME, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST],
|
||||||
|
'multiple_options': {
|
||||||
|
[StreamStats.PING]: 'Ping',
|
||||||
|
[StreamStats.FPS]: 'FPS',
|
||||||
|
[StreamStats.BITRATE]: 'Bitrate',
|
||||||
|
[StreamStats.DECODE_TIME]: 'Decode time',
|
||||||
|
[StreamStats.PACKETS_LOST]: 'Packets lost',
|
||||||
|
[StreamStats.FRAMES_LOST]: 'Frames lost',
|
||||||
|
},
|
||||||
|
},
|
||||||
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
|
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
@ -1164,6 +1446,17 @@ class Preferences {
|
|||||||
|
|
||||||
if ('options' in config && !(value in config.options)) {
|
if ('options' in config && !(value in config.options)) {
|
||||||
value = config.default;
|
value = config.default;
|
||||||
|
} else if ('multiple_options' in config) {
|
||||||
|
if (value.length) {
|
||||||
|
const validOptions = Object.keys(config.multiple_options);
|
||||||
|
value.forEach((item, idx) => {
|
||||||
|
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value.length) {
|
||||||
|
value = config.default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1182,7 +1475,7 @@ class Preferences {
|
|||||||
|
|
||||||
let $control;
|
let $control;
|
||||||
if ('options' in setting) {
|
if ('options' in setting) {
|
||||||
$control = CE('select', {id: 'xcloud_setting_' + key});
|
$control = CE('select', {'id': 'xcloud_setting_' + key});
|
||||||
for (let value in setting.options) {
|
for (let value in setting.options) {
|
||||||
const label = setting.options[value];
|
const label = setting.options[value];
|
||||||
|
|
||||||
@ -1193,6 +1486,41 @@ class Preferences {
|
|||||||
$control.value = currentValue;
|
$control.value = currentValue;
|
||||||
$control.addEventListener('change', e => {
|
$control.addEventListener('change', e => {
|
||||||
PREFS.set(key, e.target.value);
|
PREFS.set(key, e.target.value);
|
||||||
|
onChange && onChange(e);
|
||||||
|
});
|
||||||
|
} else if ('multiple_options' in setting) {
|
||||||
|
$control = CE('select', {'id': 'xcloud_setting_' + key, 'multiple': true});
|
||||||
|
for (let value in setting.multiple_options) {
|
||||||
|
const label = setting.multiple_options[value];
|
||||||
|
|
||||||
|
const $option = CE('option', {value: value}, label);
|
||||||
|
$option.selected = currentValue.indexOf(value) > -1;
|
||||||
|
|
||||||
|
$option.addEventListener('mousedown', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.target.selected = !e.target.selected;
|
||||||
|
|
||||||
|
const $parent = e.target.parentElement;
|
||||||
|
$parent.focus();
|
||||||
|
$parent.dispatchEvent(new Event('change'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$control.appendChild($option);
|
||||||
|
}
|
||||||
|
|
||||||
|
$control.addEventListener('mousedown', e => {
|
||||||
|
const $this = this;
|
||||||
|
const orgScrollTop = $this.scrollTop;
|
||||||
|
setTimeout(() => ($this.scrollTop = orgScrollTop), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
$control.addEventListener('mousemove', e => e.preventDefault());
|
||||||
|
|
||||||
|
// $control.value = currentValue;
|
||||||
|
$control.addEventListener('change', e => {
|
||||||
|
const values = Array.from(e.target.selectedOptions).map(e => e.value);
|
||||||
|
PREFS.set(key, values);
|
||||||
|
|
||||||
onChange && onChange(e);
|
onChange && onChange(e);
|
||||||
});
|
});
|
||||||
} else if (typeof setting.default === 'number') {
|
} else if (typeof setting.default === 'number') {
|
||||||
@ -1500,6 +1828,32 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better-xcloud-stats-bar > div {
|
||||||
|
display: none;
|
||||||
|
margin-right: 8px;
|
||||||
|
border-right: 2px solid #fff;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[fps]"] > .better-xcloud-stat-fps,
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[ping]"] > .better-xcloud-stat-ping,
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[btr]"] > .better-xcloud-stat-btr,
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[dt]"] > .better-xcloud-stat-dt,
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[pl]"] > .better-xcloud-stat-pl,
|
||||||
|
.better-xcloud-stats-bar[data-stats*="[fl]"] > .better-xcloud-stat-fl {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[fps]"] > .better-xcloud-stat-fps,
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[ping]"] > .better-xcloud-stat-ping,
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[btr]"] > .better-xcloud-stat-btr,
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[dt]"] > .better-xcloud-stat-dt,
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[pl]"] > .better-xcloud-stat-pl,
|
||||||
|
.better-xcloud-stats-bar[data-stats$="[fl]"] > .better-xcloud-stat-fl {
|
||||||
|
margin-right: 0;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar[data-display=glancing]::before {
|
.better-xcloud-stats-bar[data-display=glancing]::before {
|
||||||
content: '👀 ';
|
content: '👀 ';
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@ -1507,20 +1861,23 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
|
|
||||||
.better-xcloud-stats-bar[data-position=top-left] {
|
.better-xcloud-stats-bar[data-position=top-left] {
|
||||||
left: 0;
|
left: 0;
|
||||||
|
border-radius: 0 0 4px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar[data-position=top-right] {
|
.better-xcloud-stats-bar[data-position=top-right] {
|
||||||
right: 0;
|
right: 0;
|
||||||
|
border-radius: 0 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar[data-position=top-center] {
|
.better-xcloud-stats-bar[data-position=top-center] {
|
||||||
transform: translate(-50%, 0);
|
transform: translate(-50%, 0);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
border-radius: 0 0 4px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar[data-transparent=true] {
|
.better-xcloud-stats-bar[data-transparent=true] {
|
||||||
background: none;
|
background: none;
|
||||||
filter: drop-shadow(1px 0 0 #000) drop-shadow(-1px 0 0 #000) drop-shadow(0 1px 0 #000) drop-shadow(0 -1px 0 #000);
|
filter: drop-shadow(1px 0 0 #000000f0) drop-shadow(-1px 0 0 #000000f0) drop-shadow(0 1px 0 #000000f0) drop-shadow(0 -1px 0 #000000f0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar label {
|
.better-xcloud-stats-bar label {
|
||||||
@ -1535,9 +1892,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
min-width: 60px;
|
min-width: 60px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
padding-right: 8px;
|
|
||||||
margin-right: 8px;
|
|
||||||
border-right: 2px solid #fff;
|
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1557,11 +1911,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
min-width: 30px;
|
min-width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar span:last-of-type {
|
|
||||||
border: 0;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.better-xcloud-stats-settings {
|
.better-xcloud-stats-settings {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -1727,6 +2076,37 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better-xcloud-wait-time-box {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: #000000cc;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 0 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-wait-time-box label {
|
||||||
|
display: block;
|
||||||
|
text-transform: uppercase;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-wait-time-estimated, .better-xcloud-wait-time-countdown {
|
||||||
|
display: block;
|
||||||
|
font-family: Consolas, "Courier New", Courier, monospace;
|
||||||
|
text-align: right;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-wait-time-estimated {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide UI elements */
|
/* Hide UI elements */
|
||||||
#headerArea, #uhfSkipToMain, .uhf-footer {
|
#headerArea, #uhfSkipToMain, .uhf-footer {
|
||||||
display: none;
|
display: none;
|
||||||
@ -1740,7 +2120,7 @@ div[class*=NotFocusedDialog] {
|
|||||||
height: 0px !important;
|
height: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#game-stream video {
|
#game-stream video:not([src]) {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -1963,6 +2343,8 @@ function interceptHttpRequests() {
|
|||||||
const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION);
|
const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION);
|
||||||
const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE);
|
const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE);
|
||||||
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
|
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
|
||||||
|
const PREF_UI_LOADING_SCREEN_GAME_ART = PREFS.get(Preferences.UI_LOADING_SCREEN_GAME_ART);
|
||||||
|
const PREF_UI_LOADING_SCREEN_WAIT_TIME = PREFS.get(Preferences.UI_LOADING_SCREEN_WAIT_TIME);
|
||||||
|
|
||||||
const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER);
|
const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER);
|
||||||
const PREF_AUDIO_MIC_ON_PLAYING = PREFS.get(Preferences.AUDIO_MIC_ON_PLAYING);
|
const PREF_AUDIO_MIC_ON_PLAYING = PREFS.get(Preferences.AUDIO_MIC_ON_PLAYING);
|
||||||
@ -2009,6 +2391,9 @@ function interceptHttpRequests() {
|
|||||||
|
|
||||||
// Get region
|
// Get region
|
||||||
if (url.endsWith('/sessions/cloud/play')) {
|
if (url.endsWith('/sessions/cloud/play')) {
|
||||||
|
// Setup loading screen
|
||||||
|
PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.setup();
|
||||||
|
|
||||||
// Start hiding cursor
|
// Start hiding cursor
|
||||||
if (PREFS.get(Preferences.STREAM_HIDE_IDLE_CURSOR)) {
|
if (PREFS.get(Preferences.STREAM_HIDE_IDLE_CURSOR)) {
|
||||||
MouseCursorHider.start();
|
MouseCursorHider.start();
|
||||||
@ -2047,8 +2432,28 @@ function interceptHttpRequests() {
|
|||||||
return orgFetch(...arg);
|
return orgFetch(...arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PREF_OVERRIDE_CONFIGURATION && url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
// Get wait time
|
||||||
|
if (PREF_UI_LOADING_SCREEN_WAIT_TIME && url.includes('xboxlive.com') && url.includes('/waittime/')) {
|
||||||
const promise = orgFetch(...arg);
|
const promise = orgFetch(...arg);
|
||||||
|
return promise.then(response => {
|
||||||
|
return response.clone().json().then(json => {
|
||||||
|
if (json.estimatedAllocationTimeInSeconds > 0) {
|
||||||
|
// Setup wait time overlay
|
||||||
|
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
||||||
|
PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.hide();
|
||||||
|
|
||||||
|
const promise = orgFetch(...arg);
|
||||||
|
if (!PREF_OVERRIDE_CONFIGURATION) {
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
// Touch controller for all games
|
// Touch controller for all games
|
||||||
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
|
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
|
||||||
@ -2057,8 +2462,9 @@ function interceptHttpRequests() {
|
|||||||
// Get game ID from window.location
|
// Get game ID from window.location
|
||||||
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
||||||
// Check touch support
|
// Check touch support
|
||||||
if (match && !TOUCH_SUPPORTED_GAME_IDS.has(match[1])) {
|
if (match) {
|
||||||
TouchController.enable();
|
const titleId = match[1];
|
||||||
|
!TitlesInfo.hasTouchSupport(titleId) && TouchController.enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
// If both settings are invalid -> return promise
|
// If both settings are invalid -> return promise
|
||||||
@ -2100,14 +2506,26 @@ function interceptHttpRequests() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// catalog.gamepass
|
||||||
|
if (url.startsWith('https://catalog.gamepass.com') && url.includes('/products')) {
|
||||||
|
const promise = orgFetch(...arg);
|
||||||
|
return promise.then(response => {
|
||||||
|
return response.clone().json().then(json => {
|
||||||
|
for (let productId in json.Products) {
|
||||||
|
TitlesInfo.saveFromCatalogInfo(json.Products[productId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && (url.endsWith('/titles') || url.endsWith('/mru'))) {
|
if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && (url.endsWith('/titles') || url.endsWith('/mru'))) {
|
||||||
const promise = orgFetch(...arg);
|
const promise = orgFetch(...arg);
|
||||||
return promise.then(response => {
|
return promise.then(response => {
|
||||||
return response.clone().json().then(json => {
|
return response.clone().json().then(json => {
|
||||||
for (let game of json.results) {
|
for (let game of json.results) {
|
||||||
if (game.details.supportedInputTypes.length > 1) {
|
TitlesInfo.saveFromTitleInfo(game);
|
||||||
TOUCH_SUPPORTED_GAME_IDS.add(game.details.productId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@ -2226,6 +2644,11 @@ function injectSettingsButton($parent) {
|
|||||||
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: 'Standard layout\'s button style',
|
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: 'Standard layout\'s button style',
|
||||||
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: 'Custom layout\'s button style',
|
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: 'Custom layout\'s button style',
|
||||||
},
|
},
|
||||||
|
'Loading screen': {
|
||||||
|
[Preferences.UI_LOADING_SCREEN_GAME_ART]: 'Show game art',
|
||||||
|
[Preferences.UI_LOADING_SCREEN_WAIT_TIME]: 'Show the estimated wait time',
|
||||||
|
[Preferences.UI_LOADING_SCREEN_ROCKET]: 'Rocket animation',
|
||||||
|
},
|
||||||
'UI': {
|
'UI': {
|
||||||
[Preferences.STREAM_SIMPLIFY_MENU]: 'Simplify Stream\'s menu',
|
[Preferences.STREAM_SIMPLIFY_MENU]: 'Simplify Stream\'s menu',
|
||||||
[Preferences.SKIP_SPLASH_VIDEO]: 'Skip Xbox splash video',
|
[Preferences.SKIP_SPLASH_VIDEO]: 'Skip Xbox splash video',
|
||||||
@ -2363,7 +2786,7 @@ function getVideoPlayerFilterStyle() {
|
|||||||
|
|
||||||
const clarity = PREFS.get(Preferences.VIDEO_CLARITY);
|
const clarity = PREFS.get(Preferences.VIDEO_CLARITY);
|
||||||
if (clarity != 0) {
|
if (clarity != 0) {
|
||||||
const level = 7 - (clarity - 1); // 5,6,7
|
const level = (7 - (clarity - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
|
||||||
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
||||||
document.getElementById('better-xcloud-filter-clarity-matrix').setAttributeNS(null, 'kernelMatrix', matrix);
|
document.getElementById('better-xcloud-filter-clarity-matrix').setAttributeNS(null, 'kernelMatrix', matrix);
|
||||||
|
|
||||||
@ -2522,6 +2945,12 @@ function injectStreamMenuButtons() {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const msVideoProcessing = $STREAM_VIDEO.msVideoProcessing;
|
||||||
|
if (msVideoProcessing && msVideoProcessing !== 'default') {
|
||||||
|
alert('This feature doesn\'t work when the Clarity Boost mode is ON');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Close HUD
|
// Close HUD
|
||||||
$btnCloseHud.click();
|
$btnCloseHud.click();
|
||||||
|
|
||||||
@ -2619,14 +3048,20 @@ function patchVideoApi() {
|
|||||||
|
|
||||||
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
||||||
HTMLMediaElement.prototype.play = function() {
|
HTMLMediaElement.prototype.play = function() {
|
||||||
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
|
LoadingScreen.reset();
|
||||||
this.volume = 0;
|
|
||||||
this.style.display = 'none';
|
|
||||||
this.dispatchEvent(new Event('ended'));
|
|
||||||
|
|
||||||
return {
|
if (this.className && this.className.startsWith('XboxSplashVideo')) {
|
||||||
catch: () => {},
|
if (PREF_SKIP_SPLASH_VIDEO) {
|
||||||
};
|
this.volume = 0;
|
||||||
|
this.style.display = 'none';
|
||||||
|
this.dispatchEvent(new Event('ended'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
catch: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.orgPlay.apply(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.addEventListener('playing', showFunc);
|
this.addEventListener('playing', showFunc);
|
||||||
@ -2911,6 +3346,10 @@ function onHistoryChanged() {
|
|||||||
|
|
||||||
MouseCursorHider.stop();
|
MouseCursorHider.stop();
|
||||||
TouchController.reset();
|
TouchController.reset();
|
||||||
|
|
||||||
|
LoadingScreen.reset();
|
||||||
|
|
||||||
|
setTimeout(checkHeader, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user