From 2fd482bb7bf21c5fb12a91f9a3ce1ffca45cd6c1 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Thu, 6 Feb 2025 21:23:56 +0700 Subject: [PATCH] Use a better method to show the Better xCloud button ASAP --- dist/better-xcloud.pretty.user.js | 75 ++++++++-------- dist/better-xcloud.user.js | 21 ++--- src/index.ts | 19 +--- src/modules/patcher/patcher.ts | 29 +++++- src/modules/ui/header.ts | 90 ++++++++----------- src/utils/bx-event-bus.ts | 7 +- src/utils/bx-event.ts | 1 + src/utils/bx-exposed.ts | 1 + src/utils/history.ts | 3 - .../stream-settings-storage.ts | 1 - src/utils/xcloud-interceptor.ts | 5 +- 11 files changed, 121 insertions(+), 131 deletions(-) diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index 94f240b..fca6223 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 6.4.0 +// @version 6.4.1-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -192,7 +192,7 @@ class UserAgent { }); } } -var SCRIPT_VERSION = "6.4.0", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.4.1-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { supportedRegion: !0, @@ -5674,8 +5674,8 @@ ${subsVar} = subs; exposeReactCreateComponent(str) { let index = str.indexOf(".prototype.isReactComponent={}"); if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1; - let newCode = "window.BX_EXPOSED.reactCreateElement="; - return str = PatcherUtils.insertAt(str, index - 1, newCode), str; + if (str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactCreateElement="), index = PatcherUtils.indexOf(str, ".useEffect=", index), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactUseEffect="), str; }, gameCardCustomIcons(str) { let initialIndex = str.indexOf("const{supportedInputIcons:"); @@ -5705,6 +5705,16 @@ ${subsVar} = subs; let index = str.indexOf("}?w=${"); if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1; return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str; + }, + injectHeaderUseEffect(str) { + let index = str.indexOf('"EdgewaterHeader-module__spaceBetween'); + if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1; + let newCode = ` +window.BX_EXPOSED.reactUseEffect(() => { + window.BxEventBus.Script.emit('header.rendered', {}); +}); +`; + return str = PatcherUtils.insertAt(str, index, newCode), str; } }, PATCH_ORDERS = PatcherUtils.filterPatches([ ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [ @@ -5712,6 +5722,7 @@ ${subsVar} = subs; "disableAbsoluteMouse" ] : [], "exposeReactCreateComponent", + "injectHeaderUseEffect", "gameCardCustomIcons", ...getGlobalPref("ui.imageQuality") < 90 ? [ "setImageQuality" @@ -8325,6 +8336,7 @@ var BxExposed = { }, localCoOpManager: LocalCoOpManager.getInstance(), reactCreateElement: function(...args) {}, + reactUseEffect: function(...args) {}, createReactLocalCoOpIcon: (attrs) => { let reactCE = window.BX_EXPOSED.reactCreateElement; return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" }))); @@ -8363,8 +8375,6 @@ class HeaderSection { $btnRemotePlay; $btnSettings; $buttonsWrapper; - observer; - timeoutId; constructor() { BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({ classes: ["bx-header-remote-play-button", "bx-gone"], @@ -8372,38 +8382,32 @@ class HeaderSection { title: t("remote-play"), style: 8 | 64 | 2048, onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() - }), this.$btnSettings = createButton({ - classes: ["bx-header-settings-button"], - label: "???", + }); + let $btnSettings = this.$btnSettings = createButton({ + classes: ["bx-header-settings-button", "bx-gone"], + label: t("better-xcloud"), style: 16 | 32 | 64 | 256, onClick: (e) => SettingsDialog.getInstance().show() - }), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings); - } - injectSettingsButton($parent) { - if (!$parent) return; - let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings; - if (isElementVisible(this.$buttonsWrapper)) return; - if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); - $parent.appendChild(this.$buttonsWrapper); + }); + this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings), BxEventBus.Script.on("xcloud.server", ({ status }) => { + if (status === "ready") { + STATES.isSignedIn = !0, $btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"); + let PREF_LATEST_VERSION = getGlobalPref("version.latest"); + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); + } else if (status === "unavailable") { + if (STATES.supportedRegion = !1, document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); + } + $btnSettings.classList.remove("bx-gone"); + }); } checkHeader = () => { let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); - $target && this.injectSettingsButton($target); + if ($target?.appendChild(this.$buttonsWrapper), !STATES.isSignedIn) BxEventBus.Script.emit("xcloud.server", { status: "signed-out" }); }; - watchHeader() { - let $root = document.querySelector("#PageContent header") || document.querySelector("#root"); - if (!$root) return; - this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => { - this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000); - }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader(); - } showRemotePlayButton() { this.$btnRemotePlay?.classList.remove("bx-gone"); } - static watchHeader() { - HeaderSection.getInstance().watchHeader(); - } } class RemotePlayDialog extends NavigationDialog { static instance; @@ -9065,7 +9069,7 @@ class XcloudInterceptor { ip && request.headers.set("X-Forwarded-For", ip); } let response = await NATIVE_FETCH(request, init); - if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response; + if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server", { status: "unavailable" }), response; let obj = await response.clone().json(); RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region; @@ -9077,13 +9081,12 @@ class XcloudInterceptor { else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName); region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); } - BxEventBus.Script.emit("xcloud.server.ready", {}); let preferredRegion = getPreferredServerRegion(); if (preferredRegion && preferredRegion in STATES.serverRegions) { let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; } - return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; + return STATES.gsToken = obj.gsToken, BxEventBus.Script.emit("xcloud.server", { status: "ready" }), response.json = () => Promise.resolve(obj), response; } static async handlePlay(request, init) { BxEventBus.Stream.emit("state.loading", {}); @@ -9389,7 +9392,7 @@ function patchHistoryMethod(type) { } function onHistoryChanged(e) { if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; - window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {}); + window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), BxEventBus.Stream.emit("state.stopped", {}); } function setCodecPreferences(sdp, preferredCodec) { let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; @@ -10459,7 +10462,6 @@ window.addEventListener("load", (e) => { document.addEventListener("readystatechange", (e) => { if (document.readyState !== "interactive") return; if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); - else window.setTimeout(HeaderSection.watchHeader, 2000); if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) { let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); $parent && ($parent.style.display = "none"); @@ -10471,11 +10473,8 @@ window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.once("xcloud.server.unavailable", () => { - if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); -}); -BxEventBus.Script.on("xcloud.server.ready", () => { - STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); +BxEventBus.Script.on("header.rendered", () => { + HeaderSection.getInstance().checkHeader(); }); BxEventBus.Stream.on("state.loading", () => { if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index bb8b2cd..14cac11 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 6.4.0 +// @version 6.4.1-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -25,7 +25,7 @@ var ALL_PREFS = {global: ["audio.mic.onPlaying","audio.volume.booster.enabled"," var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0"; if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);if (match) CHROMIUM_VERSION = match[1];} class UserAgent {static STORAGE_KEY = "BetterXcloud.UserAgent";static #config;static #isMobile = null;static #isSafari = null;static #isSafariMobile = null;static #USER_AGENTS = {"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1","smarttv-generic": `${window.navigator.userAgent} Smart-TV`,"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"};static init() {if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";if (!UserAgent.#config.custom) UserAgent.#config.custom = "";UserAgent.spoof();}static updateStorage(profile, custom) {let config = UserAgent.#config;if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));}static getDefault() {return window.navigator.orgUserAgent || window.navigator.userAgent;}static get(profile) {let defaultUserAgent = window.navigator.userAgent;switch (profile) {case "default":return defaultUserAgent;case "custom":return UserAgent.#config.custom || defaultUserAgent;default:return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;}}static isSafari() {if (this.#isSafari !== null) return this.#isSafari;let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");return this.#isSafari = result, result;}static isSafariMobile() {if (this.#isSafariMobile !== null) return this.#isSafariMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");return this.#isSafariMobile = result, result;}static isMobile() {if (this.#isMobile !== null) return this.#isMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);return this.#isMobile = result, result;}static spoof() {let profile = UserAgent.#config.profile;if (profile === "default") return;let newUserAgent = UserAgent.get(profile);if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {value: newUserAgent});}} -var SCRIPT_VERSION = "6.4.0", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "6.4.1-beta", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {supportedRegion: !0,serverRegions: {},selectedRegion: {},gsToken: "",isSignedIn: !1,isPlaying: !1,browser: {capabilities: {touch: browserHasTouchSupport,batteryApi: "getBattery" in window.navigator,deviceVibration: !!window.navigator.vibrate,mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),emulatedNativeMkb: !!AppInterface}},userAgent: {isTv,capabilities: {touch: userAgentHasTouchSupport,mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)}},currentStream: {},remotePlay: {},pointerServerPort: 9269}; function deepClone(obj) {if (!obj) return {};if ("structuredClone" in window) return structuredClone(obj);return JSON.parse(JSON.stringify(obj));} @@ -249,7 +249,9 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {const settings = let subs = ${subsVar}; subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); ${subsVar} = subs; -`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;let newCode = "window.BX_EXPOSED.reactCreateElement=";return str = PatcherUtils.insertAt(str, index - 1, newCode), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);if (arrowIndex < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!paramVar || !supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {param: paramVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","gameCardCustomIcons",...getGlobalPref("ui.imageQuality") < 90 ? ["setImageQuality"] : [],"modifyPreloadedState","optimizeGameSlugGenerator","detectBrowserRouterReady","patchRequestInfoCrash","disableStreamGate","broadcastPollingMode","patchGamepadPolling","exposeStreamSession","exposeDialogRoutes","homePageBeforeLoad","productDetailPageBeforeLoad","streamPageBeforeLoad","guideAchievementsDefaultLocked","enableTvRoutes","supportLocalCoOp","overrideStorageGetSettings",getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",getGlobalPref("ui.layout") !== "default" && "websiteLayout",getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",...STATES.userAgent.capabilities.touch ? ["disableTouchContextMenu"] : [],...getGlobalPref("block.tracking") ? ["disableAiTrack","disableTelemetry","blockWebRtcStatsCollector","disableIndexDbLogging","disableTelemetryProvider"] : [],...getGlobalPref("xhome.enabled") ? ["remotePlayKeepAlive","remotePlayDirectConnectUrl","remotePlayDisableAchievementToast","remotePlayRecentlyUsedTitleIds","remotePlayWebTitle",STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"] : [],...BX_FLAGS.EnableXcloudLogging ? ["enableConsoleLogging","enableXcloudLogger"] : [] +`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;if (str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactCreateElement="), index = PatcherUtils.indexOf(str, ".useEffect=", index), index < 0) return !1;return str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactUseEffect="), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);if (arrowIndex < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!paramVar || !supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {param: paramVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;},injectHeaderUseEffect(str) {let index = str.indexOf('"EdgewaterHeader-module__spaceBetween');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1;let newCode = ` +window.BX_EXPOSED.reactUseEffect(() => {window.BxEventBus.Script.emit('header.rendered', {});}); +`;return str = PatcherUtils.insertAt(str, index, newCode), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectHeaderUseEffect","gameCardCustomIcons",...getGlobalPref("ui.imageQuality") < 90 ? ["setImageQuality"] : [],"modifyPreloadedState","optimizeGameSlugGenerator","detectBrowserRouterReady","patchRequestInfoCrash","disableStreamGate","broadcastPollingMode","patchGamepadPolling","exposeStreamSession","exposeDialogRoutes","homePageBeforeLoad","productDetailPageBeforeLoad","streamPageBeforeLoad","guideAchievementsDefaultLocked","enableTvRoutes","supportLocalCoOp","overrideStorageGetSettings",getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",getGlobalPref("ui.layout") !== "default" && "websiteLayout",getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",...STATES.userAgent.capabilities.touch ? ["disableTouchContextMenu"] : [],...getGlobalPref("block.tracking") ? ["disableAiTrack","disableTelemetry","blockWebRtcStatsCollector","disableIndexDbLogging","disableTelemetryProvider"] : [],...getGlobalPref("xhome.enabled") ? ["remotePlayKeepAlive","remotePlayDirectConnectUrl","remotePlayDisableAchievementToast","remotePlayRecentlyUsedTitleIds","remotePlayWebTitle",STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"] : [],...BX_FLAGS.EnableXcloudLogging ? ["enableConsoleLogging","enableXcloudLogger"] : [] ]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([hideSections.includes("news") && "ignoreNewsSection",(getGlobalPref("block.features").includes("friends") || hideSections.includes("friends")) && "ignorePlayWithFriendsSection",hideSections.includes("all-games") && "ignoreAllGamesSection",hideSections.includes("genres") && "ignoreGenresSection",!getGlobalPref("block.features").includes("byog") && hideSections.includes("byog") && "ignoreByogSection",STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",...getGlobalPref("ui.imageQuality") < 90 ? ["setBackgroundImageQuality"] : [],...blockSomeNotifications() ? ["changeNotificationsSubscription"] : [] ]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["exposeInputChannel","patchXcloudTitleInfo","disableGamepadDisconnectedScreen","patchStreamHud","playVibration","alwaysShowStreamHud",getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",...STATES.userAgent.capabilities.touch ? [getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"] : [],BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging","patchPollGamepads",getGlobalPref("stream.video.combineAudio") && "streamCombineSources",...getGlobalPref("xhome.enabled") ? ["patchRemotePlayMkb","remotePlayConnectMode"] : [],...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["patchMouseAndKeyboardEnabled","disableNativeRequestPointerLock"] : [] ]), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["detectProductDetailPage" @@ -284,18 +286,18 @@ if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers = if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); class LocalCoOpManager {static instance;static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager);supportedIds;constructor() {BxEventBus.Script.once("list.localCoOp.updated", (e) => {this.supportedIds = e.ids;}), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds);}isSupported(productId) {return this.supportedIds.has(productId);}} -var BxExposed = {getTitleInfo: () => STATES.currentStream.titleInfo,modifyPreloadedState: (state) => {let LOG_TAG3 = "PreloadState";try {state.appContext.requestInfo.userAgent = window.navigator.userAgent;} catch (e) {BxLogger.error(LOG_TAG3, e);}try {for (let exp in FeatureGates)state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (STATES.userAgent.capabilities.touch) {let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470";if (siglId in sigls) {let allGames = sigls[siglId].data.products;customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);} else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId);}} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);} catch (e) {BxLogger.error(LOG_TAG3, e);}try {state.uhf.headerMode = "Off", state.uhf.footerMode = "Off";} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";} catch (e) {BxLogger.error(LOG_TAG3, e);}return state;},modifyTitleInfo: function(titleInfo) {titleInfo = deepClone(titleInfo);let supportedInputTypes = titleInfo.details.supportedInputTypes;if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB");if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB");if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) {let touchControllerAvailability = getGlobalPref("touchController.mode");if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) {let gamepads = window.navigator.getGamepads(), gamepadFound = !1;for (let gamepad of gamepads)if (gamepad && gamepad.connected) {gamepadFound = !0;break;}gamepadFound && (touchControllerAvailability = "off");}if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = [];if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch");}return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo;},setupGainNode: ($media, audioStream) => {if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0, $media.pause();});else $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0;});try {let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();source.connect(gainNode).connect(audioCtx.destination);} catch (e) {BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;}},handleControllerShortcut: ControllerShortcut.handle,resetControllerShortcut: ControllerShortcut.reset,overrideSettings: {Tv_settings: {hasCompletedOnboarding: !0}},disableGamepadPolling: !1,backButtonPressed: () => {let navigationDialogManager = NavigationDialogManager.getInstance();if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;let dict = {bubbles: !0,cancelable: !0,key: "XF86Back",code: "XF86Back",keyCode: 4,which: 4};return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;},GameSlugRegexes: [/[;,/?:@&=+_`~$%#^*()!^β„’\xae\xa9]/g,/ {2,}/g,/ /g],toggleLocalCoOp(enable) {},beforePageLoad: (page) => {BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);},localCoOpManager: LocalCoOpManager.getInstance(),reactCreateElement: function(...args) {},createReactLocalCoOpIcon: (attrs) => {let reactCE = window.BX_EXPOSED.reactCreateElement;return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" })));}}; +var BxExposed = {getTitleInfo: () => STATES.currentStream.titleInfo,modifyPreloadedState: (state) => {let LOG_TAG3 = "PreloadState";try {state.appContext.requestInfo.userAgent = window.navigator.userAgent;} catch (e) {BxLogger.error(LOG_TAG3, e);}try {for (let exp in FeatureGates)state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (STATES.userAgent.capabilities.touch) {let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470";if (siglId in sigls) {let allGames = sigls[siglId].data.products;customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);} else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId);}} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);} catch (e) {BxLogger.error(LOG_TAG3, e);}try {state.uhf.headerMode = "Off", state.uhf.footerMode = "Off";} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";} catch (e) {BxLogger.error(LOG_TAG3, e);}return state;},modifyTitleInfo: function(titleInfo) {titleInfo = deepClone(titleInfo);let supportedInputTypes = titleInfo.details.supportedInputTypes;if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB");if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB");if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) {let touchControllerAvailability = getGlobalPref("touchController.mode");if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) {let gamepads = window.navigator.getGamepads(), gamepadFound = !1;for (let gamepad of gamepads)if (gamepad && gamepad.connected) {gamepadFound = !0;break;}gamepadFound && (touchControllerAvailability = "off");}if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = [];if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch");}return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo;},setupGainNode: ($media, audioStream) => {if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0, $media.pause();});else $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0;});try {let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();source.connect(gainNode).connect(audioCtx.destination);} catch (e) {BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;}},handleControllerShortcut: ControllerShortcut.handle,resetControllerShortcut: ControllerShortcut.reset,overrideSettings: {Tv_settings: {hasCompletedOnboarding: !0}},disableGamepadPolling: !1,backButtonPressed: () => {let navigationDialogManager = NavigationDialogManager.getInstance();if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;let dict = {bubbles: !0,cancelable: !0,key: "XF86Back",code: "XF86Back",keyCode: 4,which: 4};return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;},GameSlugRegexes: [/[;,/?:@&=+_`~$%#^*()!^β„’\xae\xa9]/g,/ {2,}/g,/ /g],toggleLocalCoOp(enable) {},beforePageLoad: (page) => {BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);},localCoOpManager: LocalCoOpManager.getInstance(),reactCreateElement: function(...args) {},reactUseEffect: function(...args) {},createReactLocalCoOpIcon: (attrs) => {let reactCE = window.BX_EXPOSED.reactCreateElement;return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" })));}}; function localRedirect(path) {let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");if (!$pageContent) return;let $anchor = CE("a", {href: url,class: "bx-hidden bx-offscreen"}, "");$anchor.addEventListener("click", (e) => {window.setTimeout(() => {$pageContent.removeChild($anchor);}, 1000);}), $pageContent.appendChild($anchor), $anchor.click();} window.localRedirect = localRedirect; function getPreferredServerRegion(shortName = !1) {let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions;if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;else return preferredRegion;for (let regionName in serverRegions) {let region = serverRegions[regionName];if (!region.isDefault) continue;if (shortName && region.shortName) return region.shortName;else return regionName;}return null;} -class HeaderSection {static instance;static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);LOG_TAG = "HeaderSection";$btnRemotePlay;$btnSettings;$buttonsWrapper;observer;timeoutId;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({classes: ["bx-header-remote-play-button", "bx-gone"],icon: BxIcon.REMOTE_PLAY,title: t("remote-play"),style: 8 | 64 | 2048,onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()}), this.$btnSettings = createButton({classes: ["bx-header-settings-button"],label: "???",style: 16 | 32 | 64 | 256,onClick: (e) => SettingsDialog.getInstance().show()}), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);}injectSettingsButton($parent) {if (!$parent) return;let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings;if (isElementVisible(this.$buttonsWrapper)) return;if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");$parent.appendChild(this.$buttonsWrapper);}checkHeader = () => {let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");$target && this.injectSettingsButton($target);};watchHeader() {let $root = document.querySelector("#PageContent header") || document.querySelector("#root");if (!$root) return;this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);}), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();}showRemotePlayButton() {this.$btnRemotePlay?.classList.remove("bx-gone");}static watchHeader() {HeaderSection.getInstance().watchHeader();}} +class HeaderSection {static instance;static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);LOG_TAG = "HeaderSection";$btnRemotePlay;$btnSettings;$buttonsWrapper;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({classes: ["bx-header-remote-play-button", "bx-gone"],icon: BxIcon.REMOTE_PLAY,title: t("remote-play"),style: 8 | 64 | 2048,onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()});let $btnSettings = this.$btnSettings = createButton({classes: ["bx-header-settings-button", "bx-gone"],label: t("better-xcloud"),style: 16 | 32 | 64 | 256,onClick: (e) => SettingsDialog.getInstance().show()});this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings), BxEventBus.Script.on("xcloud.server", ({ status }) => {if (status === "ready") {STATES.isSignedIn = !0, $btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud");let PREF_LATEST_VERSION = getGlobalPref("version.latest");if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");} else if (status === "unavailable") {if (STATES.supportedRegion = !1, document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();}$btnSettings.classList.remove("bx-gone");});}checkHeader = () => {let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");if ($target?.appendChild(this.$buttonsWrapper), !STATES.isSignedIn) BxEventBus.Script.emit("xcloud.server", { status: "signed-out" });};showRemotePlayButton() {this.$btnRemotePlay?.classList.remove("bx-gone");}} class RemotePlayDialog extends NavigationDialog {static instance;static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog);LOG_TAG = "RemotePlayNavigationDialog";STATE_LABELS = {On: t("powered-on"),Off: t("powered-off"),ConnectedStandby: t("standby"),Unknown: t("unknown")};$container;constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();}setupDialog() {let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p"));$resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => {let value = e.target.value;$settingNote.textContent = value === "1080p" ? "βœ… " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui");}), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", {manualTrigger: !0});let $qualitySettings = CE("div", {class: "bx-remote-play-settings"}, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions));$fragment.appendChild($qualitySettings);let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles();for (let con of consoles) {let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({classes: ["bx-remote-play-connect-button"],label: t("console-connect"),style: 1 | 64,onClick: (e) => manager.play(con.serverId)}));$fragment.appendChild($child);}$fragment.appendChild(CE("div", {class: "bx-remote-play-buttons",_nearby: {orientation: "horizontal"}}, createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: "https://better-xcloud.github.io/remote-play",label: t("help")}), createButton({style: 8 | 64,label: t("close"),onClick: (e) => this.hide()}))), this.$container = $fragment;}getDialog() {return this;}getContent() {return this.$container;}focusIfNeeded() {let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button");$btnConnect && $btnConnect.focus();}} class RemotePlayManager {static instance;static getInstance() {if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager;else RemotePlayManager.instance = null;return RemotePlayManager.instance;}LOG_TAG = "RemotePlayManager";isInitialized = !1;XCLOUD_TOKEN;XHOME_TOKEN;consoles;regions = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}initialize() {if (this.isInitialized) return;this.isInitialized = !0, this.requestXhomeToken(() => {this.getConsolesList(() => {BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);});});}getXcloudToken() {return this.XCLOUD_TOKEN;}setXcloudToken(token) {this.XCLOUD_TOKEN = token;}getXhomeToken() {return this.XHOME_TOKEN;}getConsoles() {return this.consoles;}requestXhomeToken(callback) {if (this.XHOME_TOKEN) {callback();return;}let GSSV_TOKEN;try {GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token;} catch (e) {for (let i = 0;i < localStorage.length; i++) {let key = localStorage.key(i);if (!key.startsWith("Auth.User.")) continue;let json = JSON.parse(localStorage.getItem(key));for (let token of json.tokens) {if (!token.relyingParty.includes("gssv.xboxlive.com")) continue;GSSV_TOKEN = token.tokenData.token;break;}break;}}let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify({offeringId: "xhome",token: GSSV_TOKEN}),headers: {"Content-Type": "application/json; charset=utf-8"}});fetch(request).then((resp) => resp.json()).then((json) => {this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback();});}async getConsolesList(callback) {if (this.consoles) {callback();return;}let options = {method: "GET",headers: {Authorization: `Bearer ${this.XHOME_TOKEN}`}};for (let region of this.regions)try {let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();if (json.results.length === 0) continue;this.consoles = json.results, STATES.remotePlay.server = region.baseUri;break;} catch (e) {}if (!STATES.remotePlay.server) this.consoles = [];callback();}play(serverId, resolution) {if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui");STATES.remotePlay.config = {serverId}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play");}togglePopup(force = null) {if (!this.isReady()) {Toast.show(t("getting-consoles-list"));return;}if (this.consoles.length === 0) {Toast.show(t("no-consoles-found"), "", { instant: !0 });return;}RemotePlayDialog.getInstance().show();}static detect() {if (!getGlobalPref("xhome.enabled")) return;if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");else window.BX_REMOTE_PLAY_CONFIG = null;}isReady() {return this.consoles !== null;}} class XhomeInterceptor {static consoleAddrs = {};static async handleLogin(request) {try {let obj = await request.clone().json();obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify(obj),headers: {"Content-Type": "application/json"}});} catch (e) {alert(e), console.log(e);}return NATIVE_FETCH(request);}static async handleConfiguration(request) {BxEventBus.Stream.emit("state.starting", {});let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [["ipAddress", "port"],["ipV4Address", "ipV4Port"],["ipV6Address", "ipV6Port"]];XhomeInterceptor.consoleAddrs = {};for (let pair of pairs) {let [keyAddr, keyPort] = pair;if (keyAddr && keyPort && serverDetails[keyAddr]) {let port = serverDetails[keyPort], ports = new Set;port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);}}return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleInputConfigs(request, opts) {let response = await NATIVE_FETCH(request);if (getGlobalPref("touchController.mode") !== "all") return response;let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];TouchController.setXboxTitleId(xboxTitleId);let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;if (!hasTouchSupport) {let supportedInputTypes = inputConfigs.supportedInputTypes;hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");}if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {data: null});else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleTitles(request) {let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;let index = request.url.indexOf(".xboxlive.com");return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {method: clone.method,body: await clone.text(),headers}), NATIVE_FETCH(request);}static async handlePlay(request) {BxEventBus.Stream.emit("state.loading", {});let body = await request.clone().json(), newRequest = new Request(request, {body: JSON.stringify(body)});return NATIVE_FETCH(newRequest);}static async handle(request) {TouchController.disable();let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));let opts = {method: clone.method,headers};if (clone.method === "POST") opts.body = await clone.text();let url = request.url;if (!url.includes("/servers/home")) {let parsed = new URL(url);url = STATES.remotePlay.server + parsed.pathname;}if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request);else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request);else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts);else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request);else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);return await NATIVE_FETCH(request);}} class LoadingScreen {static $bgStyle;static $waitTimeBox;static waitTimeInterval = null;static orgWebTitle;static secondsToString(seconds) {let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");return mDisplay + sDisplay;}static setup() {let titleInfo = STATES.currentStream.titleInfo;if (!titleInfo) return;if (!LoadingScreen.$bgStyle) {let $bgStyle = CE("style");document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;}if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();}static hideRocket() {let $bgStyle = LoadingScreen.$bgStyle;$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";}static setBackground(imageUrl) {let $bgStyle = LoadingScreen.$bgStyle;imageUrl = imageUrl + "?w=1920";let imageQuality = getGlobalPref("ui.imageQuality");if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;$bgStyle.textContent += '#game-stream{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 .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;let bg = new Image;bg.onload = (e) => {$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';}, bg.src = imageUrl;}static setupWaitTime(waitTime) {if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();let secondsLeft = waitTime, $countDown, $estimated;LoadingScreen.orgWebTitle = document.title;let endDate = new Date, 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 $waitTimeBox = LoadingScreen.$waitTimeBox;if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}, 1000);}static hide() {if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {let $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}';}setTimeout(LoadingScreen.reset, 2000);}static reset() {LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}} class GuideMenu {static instance;static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);$renderedButtons;closeGuideMenu() {if (window.BX_EXPOSED.dialogRoutes) {window.BX_EXPOSED.dialogRoutes.closeAll();return;}let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");$btnClose && $btnClose.click();}renderButtons() {if (this.$renderedButtons) return this.$renderedButtons;let buttons = {scriptSettings: createButton({label: t("better-xcloud"),icon: BxIcon.BETTER_XCLOUD,style: 128 | 64 | 1,onClick: () => {BxEventBus.Script.once("dialog.dismissed", () => {setTimeout(() => SettingsDialog.getInstance().show(), 50);}), this.closeGuideMenu();}}),closeApp: AppInterface && createButton({icon: BxIcon.POWER,label: t("close-app"),title: t("close-app"),style: 128 | 64 | 4,onClick: (e) => {AppInterface.closeApp();},attributes: {"data-state": "normal"}}),reloadPage: createButton({icon: BxIcon.REFRESH,label: t("reload-page"),title: t("reload-page"),style: 128 | 64,onClick: () => {if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();else window.location.reload();}}),backToHome: createButton({icon: BxIcon.HOME,label: t("back-to-home"),title: t("back-to-home"),style: 128 | 64,onClick: () => {this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));},attributes: {"data-state": "playing"}})}, buttonsLayout = [buttons.scriptSettings,[buttons.backToHome,buttons.reloadPage,buttons.closeApp]], $div = CE("div", {class: "bx-guide-home-buttons"});if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";for (let $button of buttonsLayout) {if (!$button) continue;if ($button instanceof HTMLElement) $div.appendChild($button);else if (Array.isArray($button)) {let $wrapper = CE("div", {});for (let $child of $button)$child && $wrapper.appendChild($child);$div.appendChild($wrapper);}}return this.$renderedButtons = $div, $div;}injectHome($root, isPlaying = !1) {{let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]");if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress);}let $target = null;if (isPlaying) {$target = $root.querySelector("a[class*=QuitGameButton]");let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");$btnXcloudHome && ($btnXcloudHome.style.display = "none");} else {let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");if ($dividers) $target = $dividers[$dividers.length - 1];}if (!$target) return !1;let $buttons = this.renderButtons();$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);}onShown = async (e) => {if (e.where === "home") {let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");$root && this.injectHome($root, STATES.isPlaying);}};addEventListeners() {window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);}observe($addedElm) {let className = $addedElm.className;if (!className) className = $addedElm.firstElementChild?.className ?? "";if (!className || className.startsWith("bx-")) return;if (className.includes("AchievementsButton-module__progressBarContainer")) {TrueAchievements.getInstance().injectAchievementsProgress($addedElm);return;}if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;{let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]");if ($achievDetailPage) {TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage);return;}}let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");if ($selectedTab) {let $elm = $selectedTab, index;for (index = 0;$elm = $elm?.previousElementSibling; index++);if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });}}} class StreamBadges {static instance;static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);LOG_TAG = "StreamBadges";serverInfo = {};badges = {playtime: {name: t("playtime"),icon: BxIcon.PLAYTIME,color: "#ff004d"},battery: {name: t("battery"),icon: BxIcon.BATTERY,color: "#00b543"},download: {name: t("download"),icon: BxIcon.DOWNLOAD,color: "#29adff"},upload: {name: t("upload"),icon: BxIcon.UPLOAD,color: "#ff77a8"},server: {name: t("server"),icon: BxIcon.SERVER,color: "#ff6c24"},video: {name: t("video"),icon: BxIcon.DISPLAY,color: "#742f29"},audio: {name: t("audio"),icon: BxIcon.AUDIO,color: "#5f574f"}};$container;intervalId;REFRESH_INTERVAL = 3000;constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}setRegion(region) {this.serverInfo.server = {region};}renderBadge(name, value) {let badgeInfo = this.badges[name], $badge;if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");return this.badges[name].$element = $badge, $badge;}updateBadges = async (forceUpdate = !1) => {if (!this.$container || !forceUpdate && !this.$container.isConnected) {this.stop();return;}let statsCollector = StreamStatsCollector.getInstance();await statsCollector.collect();let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {download: dl.toString(),upload: ul.toString(),playtime: play.toString(),battery: batt.toString()}, name;for (name in badges) {let value = badges[name];if (value === null) continue;let $elm = this.badges[name].$element;if (!$elm) continue;if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");}};async start() {await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);}stop() {this.intervalId && clearInterval(this.intervalId), this.intervalId = null;}destroy() {this.serverInfo = {}, delete this.$container;}async render() {if (this.$container) return this.start(), this.$container;await this.getServerStats();let batteryLevel = "";if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";let BADGES = [["playtime", "1m"],["battery", batteryLevel],["download", humanFileSize(0)],["upload", humanFileSize(0)],this.badges.server.$element ?? ["server", "?"],this.serverInfo.video ? this.badges.video.$element : ["video", "?"],this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]], $container = CE("div", { class: "bx-badges" });for (let item2 of BADGES) {if (!item2) continue;let $badge;if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2);else $badge = item2;$container.appendChild($badge);}return this.$container = $container, await this.start(), $container;}async getServerStats() {let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;if (stats.forEach((stat) => {if (stat.type === "codec") {let mimeType = stat.mimeType.split("/")[0];if (mimeType === "video") allVideoCodecs[stat.id] = stat;else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;else if (stat.kind === "audio") audioCodecId = stat.codecId;} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;}), videoCodecId) {let videoStat = allVideoCodecs[videoCodecId], video = {width: videoWidth,height: videoHeight,codec: videoStat.mimeType.substring(6)};if (video.codec === "H264") {let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);match && (video.profile = match[1]);}let text = videoHeight + "p";if (text && (text += "/"), text += video.codec, video.profile) {let profile = video.profile, quality = profile;if (profile.startsWith("4d")) quality = t("visual-quality-high");else if (profile.startsWith("42e")) quality = t("visual-quality-normal");else if (profile.startsWith("420")) quality = t("visual-quality-low");text += ` (${quality})`;}this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;}if (audioCodecId) {let audioStat = allAudioCodecs[audioCodecId], audio = {codec: audioStat.mimeType.substring(6),bitrate: audioStat.clockRate}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;}if (candidatePairId) {BxLogger.info("candidate", candidatePairId, allCandidatePairs);let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;if (server && server.region) text += server.region;text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);}}static setupEvents() {}} -class XcloudInterceptor {static SERVER_EXTRA_INFO = {EastUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],EastUS2: ["πŸ‡ΊπŸ‡Έ", "america-north"],NorthCentralUs: ["πŸ‡ΊπŸ‡Έ", "america-north"],SouthCentralUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],WestUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],WestUS2: ["πŸ‡ΊπŸ‡Έ", "america-north"],MexicoCentral: ["πŸ‡²πŸ‡½", "america-north"],BrazilSouth: ["πŸ‡§πŸ‡·", "america-south"],JapanEast: ["πŸ‡―πŸ‡΅", "asia"],KoreaCentral: ["πŸ‡°πŸ‡·", "asia"],AustraliaEast: ["πŸ‡¦πŸ‡Ί", "australia"],AustraliaSouthEast: ["πŸ‡¦πŸ‡Ί", "australia"],SwedenCentral: ["πŸ‡ΈπŸ‡ͺ", "europe"],UKSouth: ["πŸ‡¬πŸ‡§", "europe"],WestEurope: ["πŸ‡ͺπŸ‡Ί", "europe"]};static async handleLogin(request, init) {let bypassServer = getGlobalPref("server.bypassRestriction");if (bypassServer !== "off") {let ip = BypassServerIps[bypassServer];ip && request.headers.set("X-Forwarded-For", ip);}let response = await NATIVE_FETCH(request, init);if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;let obj = await response.clone().json();RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;for (region of obj.offeringSettings.regions) {let { name: regionName, name: shortName } = region;if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);let match = serverRegex.exec(region.baseUri);if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);}BxEventBus.Script.emit("xcloud.server.ready", {});let preferredRegion = getPreferredServerRegion();if (preferredRegion && preferredRegion in STATES.serverRegions) {let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;}return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;}static async handlePlay(request, init) {BxEventBus.Stream.emit("state.loading", {});let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName];if (region && parsedUrl.origin === region.baseUri) {badgeRegion = regionName;break;}}StreamBadges.getInstance().setRegion(badgeRegion);let clone = request.clone(), body = await clone.json(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;}if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;let newRequest = new Request(request, {body: JSON.stringify(body),headers});return NATIVE_FETCH(newRequest);}static async handleWaitTime(request, init) {let response = await NATIVE_FETCH(request, init);if (getGlobalPref("loadingScreen.waitTime.show")) {let json = await response.clone().json();if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);}return response;}static async handleConfiguration(request, init) {if (request.method !== "GET") return NATIVE_FETCH(request, init);if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();else TouchController.enable();let response = await NATIVE_FETCH(request, init), text = await response.clone().text();if (!text.length) return response;BxEventBus.Stream.emit("state.starting", {});let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;let overrideMkb = null;if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {enableMouseInput: overrideMkb,enableKeyboardInput: overrideMkb});if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handle(request, init) {let url = typeof request === "string" ? request : request.url;if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);return NATIVE_FETCH(request, init);}} +class XcloudInterceptor {static SERVER_EXTRA_INFO = {EastUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],EastUS2: ["πŸ‡ΊπŸ‡Έ", "america-north"],NorthCentralUs: ["πŸ‡ΊπŸ‡Έ", "america-north"],SouthCentralUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],WestUS: ["πŸ‡ΊπŸ‡Έ", "america-north"],WestUS2: ["πŸ‡ΊπŸ‡Έ", "america-north"],MexicoCentral: ["πŸ‡²πŸ‡½", "america-north"],BrazilSouth: ["πŸ‡§πŸ‡·", "america-south"],JapanEast: ["πŸ‡―πŸ‡΅", "asia"],KoreaCentral: ["πŸ‡°πŸ‡·", "asia"],AustraliaEast: ["πŸ‡¦πŸ‡Ί", "australia"],AustraliaSouthEast: ["πŸ‡¦πŸ‡Ί", "australia"],SwedenCentral: ["πŸ‡ΈπŸ‡ͺ", "europe"],UKSouth: ["πŸ‡¬πŸ‡§", "europe"],WestEurope: ["πŸ‡ͺπŸ‡Ί", "europe"]};static async handleLogin(request, init) {let bypassServer = getGlobalPref("server.bypassRestriction");if (bypassServer !== "off") {let ip = BypassServerIps[bypassServer];ip && request.headers.set("X-Forwarded-For", ip);}let response = await NATIVE_FETCH(request, init);if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server", { status: "unavailable" }), response;let obj = await response.clone().json();RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;for (region of obj.offeringSettings.regions) {let { name: regionName, name: shortName } = region;if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);let match = serverRegex.exec(region.baseUri);if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);}let preferredRegion = getPreferredServerRegion();if (preferredRegion && preferredRegion in STATES.serverRegions) {let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;}return STATES.gsToken = obj.gsToken, BxEventBus.Script.emit("xcloud.server", { status: "ready" }), response.json = () => Promise.resolve(obj), response;}static async handlePlay(request, init) {BxEventBus.Stream.emit("state.loading", {});let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName];if (region && parsedUrl.origin === region.baseUri) {badgeRegion = regionName;break;}}StreamBadges.getInstance().setRegion(badgeRegion);let clone = request.clone(), body = await clone.json(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;}if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;let newRequest = new Request(request, {body: JSON.stringify(body),headers});return NATIVE_FETCH(newRequest);}static async handleWaitTime(request, init) {let response = await NATIVE_FETCH(request, init);if (getGlobalPref("loadingScreen.waitTime.show")) {let json = await response.clone().json();if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);}return response;}static async handleConfiguration(request, init) {if (request.method !== "GET") return NATIVE_FETCH(request, init);if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();else TouchController.enable();let response = await NATIVE_FETCH(request, init), text = await response.clone().text();if (!text.length) return response;BxEventBus.Stream.emit("state.starting", {});let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;let overrideMkb = null;if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {enableMouseInput: overrideMkb,enableKeyboardInput: overrideMkb});if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handle(request, init) {let url = typeof request === "string" ? request : request.url;if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);return NATIVE_FETCH(request, init);}} function clearApplicationInsightsBuffers() {window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");} function clearDbLogs(dbName, table) {let request = window.indexedDB.open(dbName);request.onsuccess = (e) => {let db = e.target.result;try {let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);} catch (ex) {}};} function clearAllLogs() {clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");} @@ -308,7 +310,7 @@ function addCss() {let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica, function preloadFonts() {let $link = CE("link", {rel: "preload",href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",as: "font",type: "font/otf",crossorigin: ""});document.querySelector("head")?.appendChild($link);} class MouseCursorHider {static instance;static getInstance() {if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider;else MouseCursorHider.instance = null;return MouseCursorHider.instance;}timeoutId;isCursorVisible = !0;show() {document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0;}hide() {document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1;}onMouseMove = (e) => {!this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000);};start() {this.show(), document.addEventListener("mousemove", this.onMouseMove);}stop() {this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show();}} function patchHistoryMethod(type) {let orig = window.history[type];return function(...args) {return BxEvent.dispatch(window, BxEvent.POPSTATE, {arguments: args}), orig.apply(this, arguments);};} -function onHistoryChanged(e) {if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});} +function onHistoryChanged(e) {if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), BxEventBus.Stream.emit("state.stopped", {});} function setCodecPreferences(sdp, preferredCodec) {let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];for (let match of matches) {let id = match[1];if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);}if (!preferredCodecIds.length) return sdp;let lines = sdp.split(`\r `);for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {let line = lines[lineIndex];if (!line.startsWith("m=video")) continue;let tmp = line.trim().split(" "), ids = tmp.slice(3);ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");break;}return lines.join(`\r `);} @@ -398,14 +400,13 @@ if (window.location.pathname.includes("/auth/msa")) {let nativePushState = windo BxLogger.info("readyState", document.readyState); if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") {window.stop();let css = "";css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}';let isSafari = UserAgent.isSafari(), $secondaryAction;if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading"));else $secondaryAction = CE("a", {href: "https://better-xcloud.github.io/troubleshooting",target: "_blank"}, "πŸ€“ " + t("how-to-fix"));let $fragment = document.createDocumentFragment();throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", {class: "bx-reload-overlay"}, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari");} window.addEventListener("load", (e) => {window.setTimeout(() => {if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0);}, 3000);}); -document.addEventListener("readystatechange", (e) => {if (document.readyState !== "interactive") return;if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize();else window.setTimeout(HeaderSection.watchHeader, 2000);if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");$parent && ($parent.style.display = "none");}preloadFonts();}); +document.addEventListener("readystatechange", (e) => {if (document.readyState !== "interactive") return;if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize();if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");$parent && ($parent.style.display = "none");}preloadFonts();}); window.BX_EXPOSED = BxExposed; window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.once("xcloud.server.unavailable", () => {if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();}); -BxEventBus.Script.on("xcloud.server.ready", () => {STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);}); +BxEventBus.Script.on("header.rendered", () => {HeaderSection.getInstance().checkHeader();}); BxEventBus.Stream.on("state.loading", () => {if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);else STATES.currentStream.titleSlug = "remote-play";}); getGlobalPref("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.ready", LoadingScreen.setup); BxEventBus.Stream.on("state.starting", () => {LoadingScreen.hide();{let cursorHider = MouseCursorHider.getInstance();if (cursorHider) cursorHider.start(), cursorHider.hide();}}); diff --git a/src/index.ts b/src/index.ts index bb6b6b2..bb4ec3b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ import { GameTile } from "./modules/ui/game-tile"; import { ProductDetailsPage } from "./modules/ui/product-details"; import { NavigationDialogManager } from "./modules/ui/dialog/navigation-dialog"; import { GlobalPref, StreamPref } from "./enums/pref-keys"; -import { SettingsDialog } from "./modules/ui/dialog/settings-dialog"; import { StreamUiHandler } from "./modules/stream/stream-ui"; import { UserAgent } from "./utils/user-agent"; import { XboxApi } from "./utils/xbox-api"; @@ -174,7 +173,7 @@ document.addEventListener('readystatechange', e => { } } else { // Show Settings button in the header when not signed in - window.setTimeout(HeaderSection.watchHeader, 2000); + // window.setTimeout(HeaderSection.watchHeader, 2000); } // Hide "Play with Friends" skeleton section @@ -198,20 +197,8 @@ window.addEventListener('popstate', onHistoryChanged); window.history.pushState = patchHistoryMethod('pushState'); window.history.replaceState = patchHistoryMethod('replaceState'); -BxEventBus.Script.once('xcloud.server.unavailable', () => { - STATES.supportedRegion = false; - window.setTimeout(HeaderSection.watchHeader, 2000); - - // Open Settings dialog on Unsupported page - const $unsupportedPage = document.querySelector('div[class^=UnsupportedMarketPage-module__container]'); - if ($unsupportedPage) { - SettingsDialog.getInstance().show(); - } -}); - -BxEventBus.Script.on('xcloud.server.ready', () => { - STATES.isSignedIn = true; - window.setTimeout(HeaderSection.watchHeader, 2000); +BxEventBus.Script.on('header.rendered', () => { + HeaderSection.getInstance().checkHeader(); }); BxEventBus.Stream.on('state.loading', () => { diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index 2d3e57a..0424d53 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1029,9 +1029,14 @@ ${subsVar} = subs; return false; } - const newCode = 'window.BX_EXPOSED.reactCreateElement='; - str = PatcherUtils.insertAt(str, index - 1, newCode); + str = PatcherUtils.insertAt(str, index - 1, 'window.BX_EXPOSED.reactCreateElement='); + index = PatcherUtils.indexOf(str, '.useEffect=', index); + if (index < 0) { + return false; + } + + str = PatcherUtils.insertAt(str, index - 1, 'window.BX_EXPOSED.reactUseEffect='); return str; }, @@ -1130,7 +1135,24 @@ ${subsVar} = subs; str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref(GlobalPref.UI_IMAGE_QUALITY)}`); return str; - } + }, + + injectHeaderUseEffect(str: string) { + let index = str.indexOf('"EdgewaterHeader-module__spaceBetween'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 300)); + + if (index < 0) { + return false; + } + + const newCode = ` +window.BX_EXPOSED.reactUseEffect(() => { + window.BxEventBus.Script.emit('header.rendered', {}); +}); +`; + str = PatcherUtils.insertAt(str, index, newCode); + return str; + }, }; let PATCH_ORDERS = PatcherUtils.filterPatches([ @@ -1140,6 +1162,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ ] : []), 'exposeReactCreateComponent', + 'injectHeaderUseEffect', 'gameCardCustomIcons', // 'gameCardPassTitle', diff --git a/src/modules/ui/header.ts b/src/modules/ui/header.ts index 6110448..349e4b3 100755 --- a/src/modules/ui/header.ts +++ b/src/modules/ui/header.ts @@ -1,7 +1,7 @@ import { isFullVersion } from "@macros/build" with { type: "macro" }; -import { SCRIPT_VERSION } from "@utils/global"; -import { createButton, ButtonStyle, CE, isElementVisible } from "@utils/html"; +import { SCRIPT_VERSION, STATES } from "@utils/global"; +import { createButton, ButtonStyle, CE } from "@utils/html"; import { BxIcon } from "@utils/bx-icon"; import { getPreferredServerRegion } from "@utils/region"; import { RemotePlayManager } from "@/modules/remote-play-manager"; @@ -10,6 +10,7 @@ import { SettingsDialog } from "./dialog/settings-dialog"; import { GlobalPref } from "@/enums/pref-keys"; import { getGlobalPref } from "@/utils/pref-utils"; import { BxLogger } from "@/utils/bx-logger"; +import { BxEventBus } from "@/utils/bx-event-bus"; export class HeaderSection { private static instance: HeaderSection; @@ -20,9 +21,6 @@ export class HeaderSection { private $btnSettings: HTMLElement; private $buttonsWrapper: HTMLElement; - private observer?: MutationObserver; - private timeoutId?: number | null; - constructor() { BxLogger.info(this.LOG_TAG, 'constructor()'); @@ -38,9 +36,9 @@ export class HeaderSection { this.$btnRemotePlay = null; } - this.$btnSettings = createButton({ - classes: ['bx-header-settings-button'], - label: '???', + let $btnSettings = this.$btnSettings = createButton({ + classes: ['bx-header-settings-button', 'bx-gone'], + label: t('better-xcloud'), style: ButtonStyle.FROSTED | ButtonStyle.DROP_SHADOW | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT, onClick: e => SettingsDialog.getInstance().show(), }); @@ -49,65 +47,47 @@ export class HeaderSection { getGlobalPref(GlobalPref.REMOTE_PLAY_ENABLED) ? this.$btnRemotePlay : null, this.$btnSettings, ); + + BxEventBus.Script.on('xcloud.server', ({status}) => { + if (status === 'ready') { + STATES.isSignedIn = true; + + // Show server name + $btnSettings.querySelector('span')!.textContent = getPreferredServerRegion(true) || t('better-xcloud'); + const PREF_LATEST_VERSION = getGlobalPref(GlobalPref.VERSION_LATEST); + // Show new update status + if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) { + $btnSettings.setAttribute('data-update-available', 'true'); + } + } else if (status === 'unavailable') { + STATES.supportedRegion = false; + + // Open Settings dialog on Unsupported page + const $unsupportedPage = document.querySelector('div[class^=UnsupportedMarketPage-module__container]'); + if ($unsupportedPage) { + SettingsDialog.getInstance().show(); + } + } + + $btnSettings.classList.remove('bx-gone'); + }); } - private injectSettingsButton($parent?: HTMLElement) { - if (!$parent) { - return; - } - - const PREF_LATEST_VERSION = getGlobalPref(GlobalPref.VERSION_LATEST); - - // Setup Settings button - const $btnSettings = this.$btnSettings; - if (isElementVisible(this.$buttonsWrapper)) { - return; - } - - $btnSettings.querySelector('span')!.textContent = getPreferredServerRegion(true) || t('better-xcloud'); - - // Show new update status - if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) { - $btnSettings.setAttribute('data-update-available', 'true'); - } - - // Add the Settings button to the web page - $parent.appendChild(this.$buttonsWrapper); - } - - private checkHeader = () => { + checkHeader = () => { let $target = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]'); if (!$target) { $target = document.querySelector('div[class^=UnsupportedMarketPage-module__buttons]'); } - $target && this.injectSettingsButton($target as HTMLElement); - } + // Add the Settings button to the web page + $target?.appendChild(this.$buttonsWrapper); - private watchHeader() { - const $root = document.querySelector('#PageContent header') || document.querySelector('#root'); - if (!$root) { - return; + if (!STATES.isSignedIn) { + BxEventBus.Script.emit('xcloud.server', { status: 'signed-out' }); } - - this.timeoutId && clearTimeout(this.timeoutId); - this.timeoutId = null; - - this.observer && this.observer.disconnect(); - this.observer = new MutationObserver(mutationList => { - this.timeoutId && clearTimeout(this.timeoutId); - this.timeoutId = window.setTimeout(this.checkHeader, 2000); - }); - this.observer.observe($root, { subtree: true, childList: true }); - - this.checkHeader(); } showRemotePlayButton() { this.$btnRemotePlay?.classList.remove('bx-gone'); } - - static watchHeader() { - HeaderSection.getInstance().watchHeader(); - } } diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index 7f6d598..4b0b2f1 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -8,8 +8,9 @@ import type { SpeakerState } from "@/modules/shortcuts/sound-shortcut"; type EventCallback = (payload: T) => void; type ScriptEvents = { - 'xcloud.server.ready': {}; - 'xcloud.server.unavailable': {}; + 'xcloud.server': { + status: 'ready' | 'unavailable' | 'signed-out', + }; 'dialog.shown': {}; 'dialog.dismissed': {}; @@ -34,6 +35,8 @@ type ScriptEvents = { }; 'webgpu.ready': {}, + + 'header.rendered': {}, }; type StreamEvents = { diff --git a/src/utils/bx-event.ts b/src/utils/bx-event.ts index 789eaa1..69550f7 100755 --- a/src/utils/bx-event.ts +++ b/src/utils/bx-event.ts @@ -14,6 +14,7 @@ export namespace BxEvent { export const TOUCH_LAYOUT_MANAGER_READY = 'bx-touch-layout-manager-ready'; // Inside app + // TODO: Use EventBus export const REMOTE_PLAY_READY = 'bx-remote-play-ready'; export const REMOTE_PLAY_FAILED = 'bx-remote-play-failed'; diff --git a/src/utils/bx-exposed.ts b/src/utils/bx-exposed.ts index 87cc187..126a189 100755 --- a/src/utils/bx-exposed.ts +++ b/src/utils/bx-exposed.ts @@ -242,6 +242,7 @@ export const BxExposed = { localCoOpManager: isFullVersion() ? LocalCoOpManager.getInstance() : null, reactCreateElement: function(...args: any[]) {}, + reactUseEffect: function(...args: any[]) {}, createReactLocalCoOpIcon: isFullVersion() ? (attrs: any): any => { const reactCE = window.BX_EXPOSED.reactCreateElement; diff --git a/src/utils/history.ts b/src/utils/history.ts index 06de4b6..f212bc3 100755 --- a/src/utils/history.ts +++ b/src/utils/history.ts @@ -3,7 +3,6 @@ import { isFullVersion } from "@macros/build" with { type: "macro" }; import { BxEvent } from "@utils/bx-event"; import { LoadingScreen } from "@modules/loading-screen"; import { RemotePlayManager } from "@/modules/remote-play-manager"; -import { HeaderSection } from "@/modules/ui/header"; import { BxEventBus } from "./bx-event-bus"; import { NavigationDialogManager } from "@/modules/ui/dialog/navigation-dialog"; @@ -35,7 +34,5 @@ export function onHistoryChanged(e: PopStateEvent) { NavigationDialogManager.getInstance().hide(); LoadingScreen.reset(); - window.setTimeout(HeaderSection.watchHeader, 2000); - BxEventBus.Stream.emit('state.stopped', {}); } diff --git a/src/utils/settings-storages/stream-settings-storage.ts b/src/utils/settings-storages/stream-settings-storage.ts index 61c08e3..8d809b2 100644 --- a/src/utils/settings-storages/stream-settings-storage.ts +++ b/src/utils/settings-storages/stream-settings-storage.ts @@ -14,7 +14,6 @@ import { ControllerCustomizationDefaultPresetId } from "../local-db/controller-c import { ControllerShortcutDefaultId } from "../local-db/controller-shortcuts-table"; import { BxEventBus } from "../bx-event-bus"; import { WebGPUPlayer } from "@/modules/player/webgpu/webgpu-player"; -import { BX_FLAGS } from "../bx-flags"; export class StreamSettingsStorage extends BaseSettingsStorage { diff --git a/src/utils/xcloud-interceptor.ts b/src/utils/xcloud-interceptor.ts index a8d1fef..607be1b 100755 --- a/src/utils/xcloud-interceptor.ts +++ b/src/utils/xcloud-interceptor.ts @@ -52,7 +52,7 @@ export class XcloudInterceptor { const response = await NATIVE_FETCH(request, init); if (response.status !== 200) { // Unsupported region - BxEventBus.Script.emit('xcloud.server.unavailable', {}); + BxEventBus.Script.emit('xcloud.server', { status: 'unavailable' }); return response; } @@ -92,8 +92,6 @@ export class XcloudInterceptor { STATES.serverRegions[region.name] = Object.assign({}, region); } - BxEventBus.Script.emit('xcloud.server.ready', {}); - const preferredRegion = getPreferredServerRegion(); if (preferredRegion && preferredRegion in STATES.serverRegions) { const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); @@ -104,6 +102,7 @@ export class XcloudInterceptor { } STATES.gsToken = obj.gsToken; + BxEventBus.Script.emit('xcloud.server', { status: 'ready' }); response.json = () => Promise.resolve(obj); return response;