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(); }