From 771111d1f85f9cdb1e4fc0966af7770bedbdfdf3 Mon Sep 17 00:00:00 2001
From: redphx <96280+redphx@users.noreply.github.com>
Date: Tue, 15 Aug 2023 17:42:21 +0700
Subject: [PATCH] Loading screen (#104)
* Show game's art in the loading screen
* Fade in/out game's art
* Enable UI_GAME_ART_LOADING_SCREEN by default
* Check PREF_UI_GAME_ART_LOADING_SCREEN
* Fix touch controller setting
* Show estimated time
* Show countdown time on web title
* Rename PREF_UI_GAME_ART_LOADING_SCREEN to UI_LOADING_SCREEN_GAME_ART
* Add setting to show/hide the wait time
* Add time zone offset to end time
* Store APP_CONTEXT
* Add setting to hide the rocket animation
---
better-xcloud.user.js | 347 ++++++++++++++++++++++++++++++++++++++++--
1 file changed, 335 insertions(+), 12 deletions(-)
diff --git a/better-xcloud.user.js b/better-xcloud.user.js
index e73f455..6ca55d2 100644
--- a/better-xcloud.user.js
+++ b/better-xcloud.user.js
@@ -110,9 +110,9 @@ var STREAM_WEBRTC;
var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID;
+var APP_CONTEXT;
const HAS_TOUCH_SUPPORT = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
-const TOUCH_SUPPORTED_GAME_IDS = new Set();
// Credit: https://phosphoricons.com
const ICON_VIDEO_SETTINGS = '';
@@ -120,6 +120,236 @@ const ICON_STREAM_STATS = ' {
+ $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');
+
+ document.querySelector('#game-stream rect[width="800"]').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 {
static get #EVENT_SHOW_CONTROLLER() {
return new MessageEvent('message', {
@@ -844,6 +1074,7 @@ class PreloadedState {
},
set: (state) => {
this._state = state;
+ APP_CONTEXT = structuredClone(state.appContext);
// Get a list of touch-supported games
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
@@ -853,11 +1084,7 @@ class PreloadedState {
} catch (e) {}
for (let id in titles) {
- const details = titles[id].data.details;
- // Has move than one input type -> must have touch support
- if (details.supportedInputTypes.length > 1) {
- TOUCH_SUPPORTED_GAME_IDS.add(details.productId);
- }
+ TitlesInfo.saveFromTitleInfo(titles[id].data);
}
}
}
@@ -893,6 +1120,10 @@ class Preferences {
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
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_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; }
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
@@ -1019,6 +1250,20 @@ class Preferences {
[Preferences.REDUCE_ANIMATIONS]: {
'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]: {
'default': false,
},
@@ -1727,6 +1972,37 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
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 */
#headerArea, #uhfSkipToMain, .uhf-footer {
display: none;
@@ -1963,6 +2239,8 @@ function interceptHttpRequests() {
const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE);
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_AUDIO_MIC_ON_PLAYING = PREFS.get(Preferences.AUDIO_MIC_ON_PLAYING);
@@ -2009,6 +2287,9 @@ function interceptHttpRequests() {
// Get region
if (url.endsWith('/sessions/cloud/play')) {
+ // Setup loading screen
+ PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.setup();
+
// Start hiding cursor
if (PREFS.get(Preferences.STREAM_HIDE_IDLE_CURSOR)) {
MouseCursorHider.start();
@@ -2047,8 +2328,28 @@ function interceptHttpRequests() {
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);
+ 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') {
+ LoadingScreen.hide();
+
+ const promise = orgFetch(...arg);
+ if (!PREF_OVERRIDE_CONFIGURATION) {
+ return promise;
+ }
// Touch controller for all games
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
@@ -2057,8 +2358,9 @@ function interceptHttpRequests() {
// Get game ID from window.location
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
// Check touch support
- if (match && !TOUCH_SUPPORTED_GAME_IDS.has(match[1])) {
- TouchController.enable();
+ if (match) {
+ const titleId = match[1];
+ !TitlesInfo.hasTouchSupport(titleId) && TouchController.enable();
}
// If both settings are invalid -> return promise
@@ -2100,14 +2402,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'))) {
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(json => {
for (let game of json.results) {
- if (game.details.supportedInputTypes.length > 1) {
- TOUCH_SUPPORTED_GAME_IDS.add(game.details.productId);
- }
+ TitlesInfo.saveFromTitleInfo(game);
}
return response;
@@ -2226,6 +2540,11 @@ function injectSettingsButton($parent) {
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: 'Standard 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': {
[Preferences.STREAM_SIMPLIFY_MENU]: 'Simplify Stream\'s menu',
[Preferences.SKIP_SPLASH_VIDEO]: 'Skip Xbox splash video',
@@ -2619,6 +2938,8 @@ function patchVideoApi() {
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
+ LoadingScreen.reset();
+
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
this.volume = 0;
this.style.display = 'none';
@@ -2911,6 +3232,8 @@ function onHistoryChanged() {
MouseCursorHider.stop();
TouchController.reset();
+
+ LoadingScreen.reset();
}