From 3c2549178b80620ab9b9d3acad102dcd7eafab59 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 7 Feb 2025 21:31:01 +0700 Subject: [PATCH] Patch createPortal --- dist/better-xcloud.pretty.user.js | 152 +++++++++--------- dist/better-xcloud.user.js | 15 +- src/index.ts | 8 +- src/modules/patcher/patcher-utils.ts | 7 + src/modules/patcher/patcher.ts | 25 +-- .../patcher/patches/src/create-portal.ts | 13 ++ .../src/{streamhud.ts => stream-hud.ts} | 0 src/utils/bx-event-bus.ts | 4 +- 8 files changed, 130 insertions(+), 94 deletions(-) create mode 100644 src/modules/patcher/patches/src/create-portal.ts rename src/modules/patcher/patches/src/{streamhud.ts => stream-hud.ts} (100%) diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index a4dad9e..0e1190c 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -5131,7 +5131,8 @@ var game_card_icons_default = `var supportedInputIcons=$supportedInputIcons$,{pr var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged;this.orgOnGamepadInput=this.onGamepadInput;var match,onGamepadChangedStr=this.onGamepadChanged.toString();if(onGamepadChangedStr.startsWith("function "))onGamepadChangedStr=onGamepadChangedStr.substring(9);onGamepadChangedStr=onGamepadChangedStr.replaceAll("0","arguments[1]");eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);var onGamepadInputStr=this.onGamepadInput.toString();if(onGamepadInputStr.startsWith("function "))onGamepadInputStr=onGamepadInputStr.substring(9);match=onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);if(match){let gamepadIndexVar=match[0];onGamepadInputStr=onGamepadInputStr.replace("this.gamepadStates.get(",`this.gamepadStates.get(${gamepadIndexVar},`),eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`),BxLogger.info("supportLocalCoOp","✅ Successfully patched local co-op support")}else BxLogger.error("supportLocalCoOp","❌ Unable to patch local co-op support");this.toggleLocalCoOp=(enable)=>{BxLogger.info("toggleLocalCoOp",enable?"Enabled":"Disabled"),this.onGamepadChanged=enable?this.patchedOnGamepadChanged:this.orgOnGamepadChanged,this.onGamepadInput=enable?this.patchedOnGamepadInput:this.orgOnGamepadInput;let gamepads=window.navigator.getGamepads();for(let gamepad of gamepads){if(!gamepad?.connected)continue;if(gamepad.id.includes("Better xCloud"))continue;gamepad._noToast=!0,window.dispatchEvent(new GamepadEvent("gamepaddisconnected",{gamepad})),window.dispatchEvent(new GamepadEvent("gamepadconnected",{gamepad}))}};window.BX_EXPOSED.toggleLocalCoOp=this.toggleLocalCoOp.bind(null);\n'; var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`; var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`; -var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; +var stream_hud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; +var create_portal_default = `var $dom=arguments[1];if($dom&&$dom instanceof HTMLElement&&$dom.id==="gamepass-dialog-root"){let showing=!1,$dialog=$dom.firstElementChild?.firstElementChild;if($dialog)showing=!$dialog.className.includes("pageChangeExit");window.BxEventBus.Script.emit(showing?"dialog.shown":"dialog.dismissed",{})}`; class PatcherUtils { static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) { if (startIndex < 0) return -1; @@ -5177,6 +5178,10 @@ class PatcherUtils { end += 1; return str.substring(start, end); } + static injectUseEffect(str, index, group, eventName) { + let newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.${group}.emit('${eventName}', {}));`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + } } var LOG_TAG2 = "Patcher", PATCHES = { disableAiTrack(str) { @@ -5385,7 +5390,7 @@ if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFak patchStreamHud(str) { let index = str.indexOf("let{onCollapse"); if (index < 0) return !1; - let newCode = streamhud_default; + let newCode = stream_hud_default; if (getGlobalPref("touchController.mode") === "off") newCode += "options.canShowTakHUD = false;"; return str = PatcherUtils.insertAt(str, index, newCode), str; }, @@ -5690,20 +5695,22 @@ ${subsVar} = subs; 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; + return PatcherUtils.injectUseEffect(str, index, "Script", "ui.header.rendered"); }, injectErrorPageUseEffect(str) { let index = str.indexOf('"PureErrorPage-module__container'); if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; - let newCode = "window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('error.rendered', {}));"; - return str = PatcherUtils.insertAt(str, index, newCode), str; + return PatcherUtils.injectUseEffect(str, index, "Script", "ui.error.rendered"); }, injectStreamMenuUseEffect(str) { let index = str.indexOf('"StreamMenu-module__container'); if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; - let newCode = "window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Stream.emit('ui.streamMenu.rendered', {}));"; - return str = PatcherUtils.insertAt(str, index, newCode), str; + return PatcherUtils.injectUseEffect(str, index, "Stream", "ui.streamMenu.rendered"); + }, + injectCreatePortal(str) { + let index = str.indexOf(".createPortal=function"); + if (index > -1 && (index = PatcherUtils.indexOf(str, "{", index, 50, !0)), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, create_portal_default), str; } }, PATCH_ORDERS = PatcherUtils.filterPatches([ ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [ @@ -5711,6 +5718,7 @@ ${subsVar} = subs; "disableAbsoluteMouse" ] : [], "exposeReactCreateComponent", + "injectCreatePortal", "injectHeaderUseEffect", "injectErrorPageUseEffect", "gameCardCustomIcons", @@ -10164,68 +10172,6 @@ class ProductDetailsPage { }, 500); } } -class RootDialogObserver { - static $btnShortcut = AppInterface && createButton({ - icon: BxIcon.CREATE_SHORTCUT, - label: t("create-shortcut"), - style: 64 | 8 | 128 | 4096 | 8192, - onClick: (e) => { - window.BX_EXPOSED.dialogRoutes?.closeAll(); - let $btn = e.target.closest("button"); - AppInterface.createShortcut($btn?.dataset.path); - } - }); - static $btnWallpaper = AppInterface && createButton({ - icon: BxIcon.DOWNLOAD, - label: t("wallpaper"), - style: 64 | 8 | 128 | 4096 | 8192, - onClick: (e) => { - window.BX_EXPOSED.dialogRoutes?.closeAll(); - let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); - details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); - } - }); - static handleGameCardMenu($root) { - let $detail = $root.querySelector('a[href^="/play/"]'); - if (!$detail) return; - let path = $detail.getAttribute("href"); - RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper); - } - static handleAddedElement($root, $addedElm) { - if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) { - let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]"); - if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0; - } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0; - return !1; - } - static observe($root) { - let beingShown = !1; - new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - if (mutation.type !== "childList") continue; - if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) { - let $addedElm = mutation.addedNodes[0]; - if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm); - } - let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); - if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {}); - } - }).observe($root, { subtree: !0, childList: !0 }); - } - static waitForRootDialog() { - let observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - if (mutation.type !== "childList") continue; - let $target = mutation.target; - if ($target.id && $target.id === "gamepass-dialog-root") { - observer.disconnect(), RootDialogObserver.observe($target); - break; - } - } - }); - observer.observe(document.documentElement, { subtree: !0, childList: !0 }); - } -} class KeyboardShortcutHandler { static instance; static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler); @@ -10388,6 +10334,68 @@ class StreamUiHandler { StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0; } } +class RootDialogObserver { + static $btnShortcut = AppInterface && createButton({ + icon: BxIcon.CREATE_SHORTCUT, + label: t("create-shortcut"), + style: 64 | 8 | 128 | 4096 | 8192, + onClick: (e) => { + window.BX_EXPOSED.dialogRoutes?.closeAll(); + let $btn = e.target.closest("button"); + AppInterface.createShortcut($btn?.dataset.path); + } + }); + static $btnWallpaper = AppInterface && createButton({ + icon: BxIcon.DOWNLOAD, + label: t("wallpaper"), + style: 64 | 8 | 128 | 4096 | 8192, + onClick: (e) => { + window.BX_EXPOSED.dialogRoutes?.closeAll(); + let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path); + details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); + } + }); + static handleGameCardMenu($root) { + let $detail = $root.querySelector('a[href^="/play/"]'); + if (!$detail) return; + let path = $detail.getAttribute("href"); + RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper); + } + static handleAddedElement($root, $addedElm) { + if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) { + let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]"); + if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0; + } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0; + return !1; + } + static observe($root) { + let beingShown = !1; + new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) { + let $addedElm = mutation.addedNodes[0]; + if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm); + } + let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); + if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {}); + } + }).observe($root, { subtree: !0, childList: !0 }); + } + static waitForRootDialog() { + let observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + let $target = mutation.target; + if ($target.id && $target.id === "gamepass-dialog-root") { + observer.disconnect(), RootDialogObserver.observe($target); + break; + } + } + }); + observer.observe(document.documentElement, { subtree: !0, childList: !0 }); + } +} SettingsManager.getInstance(); if (window.location.pathname.includes("/auth/msa")) { let nativePushState = window.history.pushState; @@ -10435,7 +10443,7 @@ window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.on("header.rendered", () => { +BxEventBus.Script.on("ui.header.rendered", () => { HeaderSection.getInstance().checkHeader(); }); BxEventBus.Stream.on("state.loading", () => { @@ -10461,7 +10469,7 @@ BxEventBus.Stream.on("state.playing", (payload) => { } updateVideoPlayer(); }); -BxEventBus.Script.on("error.rendered", () => { +BxEventBus.Script.on("ui.error.rendered", () => { BxEventBus.Stream.emit("state.stopped", {}); }); BxEventBus.Stream.on("ui.streamMenu.rendered", async () => { diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 6c2ae0f..3ef221e 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -203,8 +203,9 @@ var game_card_icons_default = `var supportedInputIcons=$supportedInputIcons$,{pr var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged;this.orgOnGamepadInput=this.onGamepadInput;var match,onGamepadChangedStr=this.onGamepadChanged.toString();if(onGamepadChangedStr.startsWith("function "))onGamepadChangedStr=onGamepadChangedStr.substring(9);onGamepadChangedStr=onGamepadChangedStr.replaceAll("0","arguments[1]");eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);var onGamepadInputStr=this.onGamepadInput.toString();if(onGamepadInputStr.startsWith("function "))onGamepadInputStr=onGamepadInputStr.substring(9);match=onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);if(match){let gamepadIndexVar=match[0];onGamepadInputStr=onGamepadInputStr.replace("this.gamepadStates.get(",`this.gamepadStates.get(${gamepadIndexVar},`),eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`),BxLogger.info("supportLocalCoOp","✅ Successfully patched local co-op support")}else BxLogger.error("supportLocalCoOp","❌ Unable to patch local co-op support");this.toggleLocalCoOp=(enable)=>{BxLogger.info("toggleLocalCoOp",enable?"Enabled":"Disabled"),this.onGamepadChanged=enable?this.patchedOnGamepadChanged:this.orgOnGamepadChanged,this.onGamepadInput=enable?this.patchedOnGamepadInput:this.orgOnGamepadInput;let gamepads=window.navigator.getGamepads();for(let gamepad of gamepads){if(!gamepad?.connected)continue;if(gamepad.id.includes("Better xCloud"))continue;gamepad._noToast=!0,window.dispatchEvent(new GamepadEvent("gamepaddisconnected",{gamepad})),window.dispatchEvent(new GamepadEvent("gamepadconnected",{gamepad}))}};window.BX_EXPOSED.toggleLocalCoOp=this.toggleLocalCoOp.bind(null);\n'; var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`; var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`; -var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; -class PatcherUtils {static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.indexOf(searchString, startIndex);if (index < 0 || maxRange && index - startIndex > maxRange) return -1;return after ? index + searchString.length : index;}static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.lastIndexOf(searchString, startIndex);if (index < 0 || maxRange && startIndex - index > maxRange) return -1;return after ? index + searchString.length : index;}static insertAt(txt, index, insertString) {return txt.substring(0, index) + insertString + txt.substring(index);}static replaceWith(txt, index, fromString, toString) {return txt.substring(0, index) + toString + txt.substring(index + fromString.length);}static filterPatches(patches) {return patches.filter((item2) => !!item2);}static patchBeforePageLoad(str, page) {let text = `chunkName:()=>"${page}-page",`;if (!str.includes(text)) return !1;return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str;}static isVarCharacter(char) {let code = char.charCodeAt(0), isUppercase = code >= 65 && code <= 90, isLowercase = code >= 97 && code <= 122, isDigit = code >= 48 && code <= 57;return isUppercase || isLowercase || isDigit || (char === "_" || char === "$");}static getVariableNameBefore(str, index) {if (index < 0) return null;let end = index, start = end - 1;while (PatcherUtils.isVarCharacter(str[start]))start -= 1;return str.substring(start + 1, end);}static getVariableNameAfter(str, index) {if (index < 0) return null;let start = index, end = start + 1;while (PatcherUtils.isVarCharacter(str[end]))end += 1;return str.substring(start, end);}} +var stream_hud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; +var create_portal_default = `var $dom=arguments[1];if($dom&&$dom instanceof HTMLElement&&$dom.id==="gamepass-dialog-root"){let showing=!1,$dialog=$dom.firstElementChild?.firstElementChild;if($dialog)showing=!$dialog.className.includes("pageChangeExit");window.BxEventBus.Script.emit(showing?"dialog.shown":"dialog.dismissed",{})}`; +class PatcherUtils {static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.indexOf(searchString, startIndex);if (index < 0 || maxRange && index - startIndex > maxRange) return -1;return after ? index + searchString.length : index;}static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.lastIndexOf(searchString, startIndex);if (index < 0 || maxRange && startIndex - index > maxRange) return -1;return after ? index + searchString.length : index;}static insertAt(txt, index, insertString) {return txt.substring(0, index) + insertString + txt.substring(index);}static replaceWith(txt, index, fromString, toString) {return txt.substring(0, index) + toString + txt.substring(index + fromString.length);}static filterPatches(patches) {return patches.filter((item2) => !!item2);}static patchBeforePageLoad(str, page) {let text = `chunkName:()=>"${page}-page",`;if (!str.includes(text)) return !1;return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str;}static isVarCharacter(char) {let code = char.charCodeAt(0), isUppercase = code >= 65 && code <= 90, isLowercase = code >= 97 && code <= 122, isDigit = code >= 48 && code <= 57;return isUppercase || isLowercase || isDigit || (char === "_" || char === "$");}static getVariableNameBefore(str, index) {if (index < 0) return null;let end = index, start = end - 1;while (PatcherUtils.isVarCharacter(str[start]))start -= 1;return str.substring(start + 1, end);}static getVariableNameAfter(str, index) {if (index < 0) return null;let start = index, end = start + 1;while (PatcherUtils.isVarCharacter(str[end]))end += 1;return str.substring(start, end);}static injectUseEffect(str, index, group, eventName) {let newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.${group}.emit('${eventName}', {}));`;return str = PatcherUtils.insertAt(str, index, newCode), str;}} var LOG_TAG2 = "Patcher", PATCHES = {disableAiTrack(str) {let text = ".track=function(", index = str.indexOf(text);if (index < 0) return !1;if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) return !1;return PatcherUtils.replaceWith(str, index, text, ".track=function(e){},!!function(");},disableTelemetry(str) {let text = ".disableTelemetry=function(){return!1}";if (!str.includes(text)) return !1;return str.replace(text, ".disableTelemetry=function(){return!0}");},disableTelemetryProvider(str) {let text = "this.enableLightweightTelemetry=!";if (!str.includes(text)) return !1;let newCode = ["this.trackEvent","this.trackPageView","this.trackHttpCompleted","this.trackHttpFailed","this.trackError","this.trackErrorLike","this.onTrackEvent","()=>{}"].join("=");return str.replace(text, newCode + ";" + text);},disableIndexDbLogging(str) {let text = ",this.logsDb=new";if (!str.includes(text)) return !1;let newCode = ",this.log=()=>{}";return str.replace(text, newCode + text);},websiteLayout(str) {let text = '?"tv":"default"';if (!str.includes(text)) return !1;let layout = getGlobalPref("ui.layout") === "tv" ? "tv" : "default";return str.replace(text, `?"${layout}":"${layout}"`);},remotePlayDirectConnectUrl(str) {let index = str.indexOf("/direct-connect");if (index < 0) return !1;return str.replace(str.substring(index - 9, index + 15), "https://www.xbox.com/play");},remotePlayKeepAlive(str) {let text = "onServerDisconnectMessage(e){";if (!str.includes(text)) return !1;return str = str.replace(text, text + remote_play_keep_alive_default), str;},remotePlayConnectMode(str) {let text = 'connectMode:"cloud-connect",';if (!str.includes(text)) return !1;let newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`;return str.replace(text, newCode);},remotePlayDisableAchievementToast(str) {let text = ".AchievementUnlock:{";if (!str.includes(text)) return !1;let newCode = "if (!!window.BX_REMOTE_PLAY_CONFIG) return;";return str.replace(text, text + newCode);},remotePlayRecentlyUsedTitleIds(str) {let text = "(e.data.recentlyUsedTitleIds)){";if (!str.includes(text)) return !1;let newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;";return str.replace(text, text + newCode);},remotePlayWebTitle(str) {let text = "titleTemplate:void 0,title:", index = str.indexOf(text);if (index < 0) return !1;return str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t("remote-play")} - Better xCloud" :`), str;},blockWebRtcStatsCollector(str) {let text = "this.shouldCollectStats=!0";if (!str.includes(text)) return !1;return str.replace(text, "this.shouldCollectStats=!1");},patchPollGamepads(str) {let index = str.indexOf("},this.pollGamepads=()=>{");if (index < 0) return !1;let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index);if (setTimeoutIndex < 0) return !1;let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ");if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getGlobalPref("block.tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", "");let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/);if (!match) return !1;let newCode = renderString(poll_gamepad_default, {gamepadVar: match[1]});if (codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"), match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/), !match) return !1;let xCloudGamepadVar = match[1], inputFeedbackManager = PatcherUtils.indexOf(codeBlock, "this.inputFeedbackManager.onGamepadConnected(", 0, 1e4), backetIndex = PatcherUtils.indexOf(codeBlock, "}", inputFeedbackManager, 100);if (backetIndex < 0) return !1;let customizationCode = ";";return customizationCode += renderString(controller_customization_default, { xCloudGamepadVar }), codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode), str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str;},enableXcloudLogger(str) {let text = "this.telemetryProvider=e}log(e,t,r){";if (!str.includes(text)) return !1;let newCode = ` const [logTag, logLevel, logMessage] = Array.from(arguments); @@ -225,7 +226,7 @@ if (gamepadFound) {return;} ${autoOffCode} const titleInfo = window.BX_EXPOSED.getTitleInfo(); if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {return;} -`;return str = str.replace(text, newCode + text), str;},streamCombineSources(str) {let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen";if (!str.includes(text)) return !1;return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str;},patchStreamHud(str) {let index = str.indexOf("let{onCollapse");if (index < 0) return !1;let newCode = streamhud_default;if (getGlobalPref("touchController.mode") === "off") newCode += "options.canShowTakHUD = false;";return str = PatcherUtils.insertAt(str, index, newCode), str;},broadcastPollingMode(str) {let text = ".setPollingMode=e=>{";if (!str.includes(text)) return !1;let newCode = ` +`;return str = str.replace(text, newCode + text), str;},streamCombineSources(str) {let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen";if (!str.includes(text)) return !1;return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str;},patchStreamHud(str) {let index = str.indexOf("let{onCollapse");if (index < 0) return !1;let newCode = stream_hud_default;if (getGlobalPref("touchController.mode") === "off") newCode += "options.canShowTakHUD = false;";return str = PatcherUtils.insertAt(str, index, newCode), str;},broadcastPollingMode(str) {let text = ".setPollingMode=e=>{";if (!str.includes(text)) return !1;let newCode = ` window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase(); BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED); `;return str = str.replace(text, text + newCode), str;},patchGamepadPolling(str) {let index = str.indexOf(".shouldHandleGamepadInput)())return void");if (index < 0) return !1;return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str;},patchXcloudTitleInfo(str) {let text = "async cloudConnect", index = str.indexOf(text);if (index < 0) return !1;let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];if (!params) return !1;let titleInfoVar = params.split(",")[0], newCode = ` @@ -245,7 +246,7 @@ 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;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;},injectErrorPageUseEffect(str) {let index = str.indexOf('"PureErrorPage-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;let newCode = "window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('error.rendered', {}));";return str = PatcherUtils.insertAt(str, index, newCode), str;},injectStreamMenuUseEffect(str) {let index = str.indexOf('"StreamMenu-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;let newCode = "window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Stream.emit('ui.streamMenu.rendered', {}));";return str = PatcherUtils.insertAt(str, index, newCode), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectHeaderUseEffect","injectErrorPageUseEffect","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;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.header.rendered");},injectErrorPageUseEffect(str) {let index = str.indexOf('"PureErrorPage-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.error.rendered");},injectStreamMenuUseEffect(str) {let index = str.indexOf('"StreamMenu-module__container');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Stream", "ui.streamMenu.rendered");},injectCreatePortal(str) {let index = str.indexOf(".createPortal=function");if (index > -1 && (index = PatcherUtils.indexOf(str, "{", index, 50, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, create_portal_default), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectCreatePortal","injectHeaderUseEffect","injectErrorPageUseEffect","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","injectStreamMenuUseEffect",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" @@ -384,11 +385,11 @@ class GameBar {static instance;static getInstance() {if (typeof GameBar.instance class XcloudApi {static instance;static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi);LOG_TAG = "XcloudApi";CACHE_TITLES = {};CACHE_WAIT_TIME = {};constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}async getTitleInfo(id) {if (id in this.CACHE_TITLES) return this.CACHE_TITLES[id];let baseUri = STATES.selectedRegion.baseUri;if (!baseUri || !STATES.gsToken) return;let json;try {json = (await (await NATIVE_FETCH(`${baseUri}/v2/titles`, {method: "POST",headers: {Authorization: `Bearer ${STATES.gsToken}`,"Content-Type": "application/json"},body: JSON.stringify({alternateIds: [id],alternateIdType: "productId"})})).json()).results[0];} catch (e) {json = {};}return this.CACHE_TITLES[id] = json, json;}async getWaitTime(id) {if (id in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id];let baseUri = STATES.selectedRegion.baseUri;if (!baseUri || !STATES.gsToken) return null;let json;try {json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, {method: "GET",headers: {Authorization: `Bearer ${STATES.gsToken}`}})).json();} catch (e) {json = {};}return this.CACHE_WAIT_TIME[id] = json, json;}} class GameTile {static timeoutId;static async showWaitTime($elm, productId) {if ($elm.hasWaitTime) return;$elm.hasWaitTime = !0;let totalWaitTime, api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId);if (info) {let waitTime = await api.getWaitTime(info.titleId);if (waitTime) totalWaitTime = waitTime.estimatedAllocationTimeInSeconds;}if (typeof totalWaitTime === "number" && isElementVisible($elm)) {let $div = CE("div", { class: "bx-game-tile-wait-time" }, createSvgIcon(BxIcon.PLAYTIME), CE("span", !1, totalWaitTime < 60 ? totalWaitTime + "s" : secondsToHm(totalWaitTime))), duration = totalWaitTime >= 900 ? "long" : totalWaitTime >= 600 ? "medium" : totalWaitTime >= 300 ? "short" : "";if (duration) $div.dataset.duration = duration;$elm.insertAdjacentElement("afterbegin", $div);}}static requestWaitTime($elm, productId) {GameTile.timeoutId && clearTimeout(GameTile.timeoutId), GameTile.timeoutId = window.setTimeout(async () => {GameTile.showWaitTime($elm, productId);}, 500);}static findProductId($elm) {let productId = null;try {if ($elm.tagName === "BUTTON" && $elm.className.includes("MruGameCard") || $elm.tagName === "A" && $elm.className.includes("GameCard")) {let props = getReactProps($elm.parentElement);if (Array.isArray(props.children)) productId = props.children[0].props.productId;else productId = props.children.props.productId;} else if ($elm.tagName === "A" && $elm.className.includes("GameItem")) {let props = getReactProps($elm.parentElement);if (props = props.children.props, props.location !== "NonStreamableGameItem") if ("productId" in props) productId = props.productId;else productId = props.children.props.productId;}} catch (e) {}return productId;}static setup() {window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, (e) => {let $elm = e.element;if (($elm.className || "").includes("MruGameCard")) {let $ol = $elm.closest("ol");if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => {let productId = GameTile.findProductId($elm2);productId && GameTile.showWaitTime($elm2, productId);});} else {let productId = GameTile.findProductId($elm);productId && GameTile.requestWaitTime($elm, productId);}});}} class ProductDetailsPage {static $btnShortcut = AppInterface && createButton({icon: BxIcon.CREATE_SHORTCUT,label: t("create-shortcut"),style: 64,onClick: (e) => {AppInterface.createShortcut(window.location.pathname.substring(6));}});static $btnWallpaper = AppInterface && createButton({icon: BxIcon.DOWNLOAD,label: t("wallpaper"),style: 64,onClick: (e) => {let details = parseDetailsPath(window.location.pathname);details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);}});static injectTimeoutId = null;static injectButtons() {ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => {let $inputsContainer = document.querySelector('div[class*="Header-module__gamePassAndInputsContainer"]');if ($inputsContainer && !$inputsContainer.dataset.bxInjected) {$inputsContainer.dataset.bxInjected = "true";let { productId } = parseDetailsPath(window.location.pathname);if (LocalCoOpManager.getInstance().isSupported(productId || "")) $inputsContainer.insertAdjacentElement("afterend", CE("div", {class: "bx-product-details-icons bx-frosted"}, createSvgIcon(BxIcon.LOCAL_CO_OP), t("local-co-op")));}if (AppInterface) {let $container = document.querySelector("div[class*=ActionButtons-module__container]");if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", {class: "bx-product-details-buttons"}, ["android-handheld", "android"].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper));}}, 500);}} -class RootDialogObserver {static $btnShortcut = AppInterface && createButton({icon: BxIcon.CREATE_SHORTCUT,label: t("create-shortcut"),style: 64 | 8 | 128 | 4096 | 8192,onClick: (e) => {window.BX_EXPOSED.dialogRoutes?.closeAll();let $btn = e.target.closest("button");AppInterface.createShortcut($btn?.dataset.path);}});static $btnWallpaper = AppInterface && createButton({icon: BxIcon.DOWNLOAD,label: t("wallpaper"),style: 64 | 8 | 128 | 4096 | 8192,onClick: (e) => {window.BX_EXPOSED.dialogRoutes?.closeAll();let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);}});static handleGameCardMenu($root) {let $detail = $root.querySelector('a[href^="/play/"]');if (!$detail) return;let path = $detail.getAttribute("href");RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);}static handleAddedElement($root, $addedElm) {if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;} else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;return !1;}static observe($root) {let beingShown = !1;new MutationObserver((mutationList) => {for (let mutation of mutationList) {if (mutation.type !== "childList") continue;if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {let $addedElm = mutation.addedNodes[0];if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);}let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});}}).observe($root, { subtree: !0, childList: !0 });}static waitForRootDialog() {let observer = new MutationObserver((mutationList) => {for (let mutation of mutationList) {if (mutation.type !== "childList") continue;let $target = mutation.target;if ($target.id && $target.id === "gamepass-dialog-root") {observer.disconnect(), RootDialogObserver.observe($target);break;}}});observer.observe(document.documentElement, { subtree: !0, childList: !0 });}} class KeyboardShortcutHandler {static instance;static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler);start() {window.addEventListener("keydown", this.onKeyDown);}stop() {window.removeEventListener("keydown", this.onKeyDown);}onKeyDown = (e) => {if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;if (e.repeat) return;let fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);if (!fullKeyCode) return;let action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];if (action) e.preventDefault(), e.stopPropagation(), ShortcutHandler.runAction(action);};} var VIBRATION_DATA_MAP = {gamepadIndex: 8,leftMotorPercent: 8,rightMotorPercent: 8,leftTriggerMotorPercent: 8,rightTriggerMotorPercent: 8,durationMs: 16}; class DeviceVibrationManager {static instance;static getInstance() {if (typeof DeviceVibrationManager.instance === "undefined") if (STATES.browser.capabilities.deviceVibration) DeviceVibrationManager.instance = new DeviceVibrationManager;else DeviceVibrationManager.instance = null;return DeviceVibrationManager.instance;}dataChannel = null;boundOnMessage;constructor() {this.boundOnMessage = this.onMessage.bind(this), BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label === "input") this.reset(), this.dataChannel = dataChannel, this.setupDataChannel();}), BxEventBus.Stream.on("deviceVibration.updated", () => this.setupDataChannel());}setupDataChannel() {if (!this.dataChannel) return;if (this.removeEventListeners(), window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) this.dataChannel.addEventListener("message", this.boundOnMessage);}playVibration(data) {let vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity;if (AppInterface) {AppInterface.vibrate(JSON.stringify(data), vibrationIntensity);return;}let realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity;if (realIntensity === 0 || realIntensity === 100) {window.navigator.vibrate(realIntensity ? data.durationMs : 0);return;}let pulseDuration = 200, onDuration = Math.floor(pulseDuration * realIntensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat();window.navigator.vibrate(pulses);}onMessage(e) {if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return;let dataView = new DataView(e.data), offset = 0, messageType;if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;if (!(messageType & 128)) return;let vibrationType = dataView.getUint8(offset);if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return;let data = {}, key;for (key in VIBRATION_DATA_MAP)if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;this.playVibration(data);}removeEventListeners() {try {this.dataChannel?.removeEventListener("message", this.boundOnMessage);} catch (e) {}}reset() {this.removeEventListeners(), this.dataChannel = null;}} class StreamUiHandler {static $btnStreamSettings;static $btnStreamStats;static $btnRefresh;static $btnHome;static cloneStreamHudButton($btnOrg, label, svgIcon) {if (!$btnOrg) return null;let $container = $btnOrg.cloneNode(!0), timeout;if (STATES.browser.capabilities.touch) {let onTransitionStart = (e) => {if (e.propertyName !== "opacity") return;timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none";}, onTransitionEnd = (e) => {if (e.propertyName !== "opacity") return;let $streamHud = e.target.closest("#StreamHud");if (!$streamHud) return;if ($streamHud.style.left === "0px") {let $target = e.target;timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {$target.style.pointerEvents = "auto";}, 100);}};$container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);}let $button = $container.querySelector("button");if (!$button) return null;$button.setAttribute("title", label);let $orgSvg = $button.querySelector("svg");if (!$orgSvg) return null;let $svg = createSvgIcon(svgIcon);return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;}static cloneCloseButton($btnOrg, icon, className, onChange) {if (!$btnOrg) return null;let $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;}static async handleStreamMenu() {let $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");if (!$btnCloseHud) return;let { $btnRefresh, $btnHome } = StreamUiHandler;if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {confirm(t("confirm-reload-stream")) && window.location.reload();});if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));});if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome);document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render());}static handleSystemMenu($streamHud) {let $orgButton = $streamHud.querySelector("div[class^=HUDButton]");if (!$orgButton) return;if (StreamUiHandler.$btnStreamSettings && $streamHud.contains(StreamUiHandler.$btnStreamSettings)) return;let hideGripHandle = () => {let $gripHandle = document.querySelector("#StreamHud button[class^=GripHandle]");if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();}, $btnStreamSettings = StreamUiHandler.$btnStreamSettings;if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => {hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show();}), StreamUiHandler.$btnStreamSettings = $btnStreamSettings;let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats;if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => {hideGripHandle(), e.preventDefault(), await streamStats.toggle();let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);}), StreamUiHandler.$btnStreamStats = $btnStreamStats;let $btnParent = $orgButton.parentElement;if ($btnStreamSettings && $btnStreamStats) {let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);}let $dotsButton = $btnParent.lastElementChild;$dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);}static reset() {StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0;}} +class RootDialogObserver {static $btnShortcut = AppInterface && createButton({icon: BxIcon.CREATE_SHORTCUT,label: t("create-shortcut"),style: 64 | 8 | 128 | 4096 | 8192,onClick: (e) => {window.BX_EXPOSED.dialogRoutes?.closeAll();let $btn = e.target.closest("button");AppInterface.createShortcut($btn?.dataset.path);}});static $btnWallpaper = AppInterface && createButton({icon: BxIcon.DOWNLOAD,label: t("wallpaper"),style: 64 | 8 | 128 | 4096 | 8192,onClick: (e) => {window.BX_EXPOSED.dialogRoutes?.closeAll();let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);}});static handleGameCardMenu($root) {let $detail = $root.querySelector('a[href^="/play/"]');if (!$detail) return;let path = $detail.getAttribute("href");RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);}static handleAddedElement($root, $addedElm) {if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;} else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;return !1;}static observe($root) {let beingShown = !1;new MutationObserver((mutationList) => {for (let mutation of mutationList) {if (mutation.type !== "childList") continue;if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {let $addedElm = mutation.addedNodes[0];if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);}let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});}}).observe($root, { subtree: !0, childList: !0 });}static waitForRootDialog() {let observer = new MutationObserver((mutationList) => {for (let mutation of mutationList) {if (mutation.type !== "childList") continue;let $target = mutation.target;if ($target.id && $target.id === "gamepass-dialog-root") {observer.disconnect(), RootDialogObserver.observe($target);break;}}});observer.observe(document.documentElement, { subtree: !0, childList: !0 });}} SettingsManager.getInstance(); if (window.location.pathname.includes("/auth/msa")) {let nativePushState = window.history.pushState;throw window.history.pushState = function(...args) {let url = args[2];if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) {console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url;return;}return nativePushState.apply(this, arguments);}, new Error("[Better xCloud] Refreshing the page after logging in");} BxLogger.info("readyState", document.readyState); @@ -400,12 +401,12 @@ window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.on("header.rendered", () => {HeaderSection.getInstance().checkHeader();}); +BxEventBus.Script.on("ui.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();}}); BxEventBus.Stream.on("state.playing", (payload) => {window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0;{let gameBar = GameBar.getInstance();if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar();KeyboardShortcutHandler.getInstance().start();let $video = payload.$video;if (ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), getStreamPref("localCoOp.enabled")) BxExposed.toggleLocalCoOp(!0), Toast.show(t("local-co-op"), t("enabled"));}updateVideoPlayer();}); -BxEventBus.Script.on("error.rendered", () => {BxEventBus.Stream.emit("state.stopped", {});}); +BxEventBus.Script.on("ui.error.rendered", () => {BxEventBus.Stream.emit("state.stopped", {});}); BxEventBus.Stream.on("ui.streamMenu.rendered", async () => {await StreamUiHandler.handleStreamMenu();}); BxEventBus.Stream.on("ui.streamHud.rendered", async () => {let $elm = document.querySelector("#StreamHud");$elm && StreamUiHandler.handleSystemMenu($elm);}); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => {if (e.component === "product-detail") ProductDetailsPage.injectButtons();}); diff --git a/src/index.ts b/src/index.ts index 4ae82ea..6948b30 100755 --- a/src/index.ts +++ b/src/index.ts @@ -25,7 +25,6 @@ import { BxLogger } from "@utils/bx-logger"; import { GameBar } from "./modules/game-bar/game-bar"; import { ScreenshotManager } from "./utils/screenshot-manager"; import { NativeMkbHandler } from "./modules/mkb/native-mkb-handler"; -import { GuideMenu } from "./modules/ui/guide-menu"; import { updateVideoPlayer } from "./modules/stream/stream-settings-utils"; import { BlockFeature, NativeMkbMode, TouchControllerMode, UiSection } from "./enums/pref-values"; import { HeaderSection } from "./modules/ui/header"; @@ -36,7 +35,6 @@ import { GlobalPref, StreamPref } from "./enums/pref-keys"; import { UserAgent } from "./utils/user-agent"; import { XboxApi } from "./utils/xbox-api"; import { StreamStatsCollector } from "./utils/stream-stats-collector"; -import { RootDialogObserver } from "./utils/root-dialog-observer"; import { StreamSettings } from "./utils/stream-settings"; import { KeyboardShortcutHandler } from "./modules/mkb/keyboard-shortcut-handler"; import { GhPagesUtils } from "./utils/gh-pages"; @@ -47,6 +45,8 @@ import { SettingsManager } from "./modules/settings-manager"; import { Toast } from "./utils/toast"; import { WebGPUPlayer } from "./modules/player/webgpu/webgpu-player"; import { StreamUiHandler } from "./modules/stream/stream-ui"; +import { RootDialogObserver } from "./utils/root-dialog-observer"; +import { GuideMenu } from "./modules/ui/guide-menu"; SettingsManager.getInstance(); @@ -197,7 +197,7 @@ window.addEventListener('popstate', onHistoryChanged); window.history.pushState = patchHistoryMethod('pushState'); window.history.replaceState = patchHistoryMethod('replaceState'); -BxEventBus.Script.on('header.rendered', () => { +BxEventBus.Script.on('ui.header.rendered', () => { HeaderSection.getInstance().checkHeader(); }); @@ -260,7 +260,7 @@ BxEventBus.Stream.on('state.playing', payload => { updateVideoPlayer(); }); -BxEventBus.Script.on('error.rendered', () => { +BxEventBus.Script.on('ui.error.rendered', () => { BxEventBus.Stream.emit('state.stopped', {}); }); diff --git a/src/modules/patcher/patcher-utils.ts b/src/modules/patcher/patcher-utils.ts index 109468b..e528daf 100644 --- a/src/modules/patcher/patcher-utils.ts +++ b/src/modules/patcher/patcher-utils.ts @@ -96,4 +96,11 @@ export class PatcherUtils { return str.substring(start, end); } + + static injectUseEffect(str: string, index: number, group: 'Stream' | 'Script', eventName: string) { + const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.${group}.emit('${eventName}', {}));`; + str = PatcherUtils.insertAt(str, index, newCode); + + return str; + } } diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index 575676c..d4f52b0 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -11,7 +11,8 @@ import codeGameCardIcons from "./patches/game-card-icons.js" with { type: "text" import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" }; import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" }; import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" }; -import codeStreamHud from "./patches/streamhud.js" with { type: "text" }; +import codeStreamHud from "./patches/stream-hud.js" with { type: "text" }; +import codeCreatePortal from "./patches/create-portal.js" with { type: "text" }; import { GlobalPref, StorageKey } from "@/enums/pref-keys.js"; import { getGlobalPref } from "@/utils/pref-utils.js"; import { GamePassCloudGallery } from "@/enums/game-pass-gallery"; @@ -1140,9 +1141,7 @@ ${subsVar} = subs; return false; } - const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('header.rendered', {}));`; - str = PatcherUtils.insertAt(str, index, newCode); - return str; + return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.header.rendered'); }, injectErrorPageUseEffect(str: string) { @@ -1152,9 +1151,7 @@ ${subsVar} = subs; return false; } - const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('error.rendered', {}));`; - str = PatcherUtils.insertAt(str, index, newCode); - return str; + return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.error.rendered'); }, injectStreamMenuUseEffect(str: string) { @@ -1164,8 +1161,17 @@ ${subsVar} = subs; return false; } - const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Stream.emit('ui.streamMenu.rendered', {}));`; - str = PatcherUtils.insertAt(str, index, newCode); + return PatcherUtils.injectUseEffect(str, index, 'Stream', 'ui.streamMenu.rendered'); + }, + + injectCreatePortal(str: string) { + let index = str.indexOf('.createPortal=function'); + index > -1 && (index = PatcherUtils.indexOf(str, '{', index, 50, true)); + if (index < 0) { + return false; + } + + str = PatcherUtils.insertAt(str, index, codeCreatePortal); return str; }, }; @@ -1178,6 +1184,7 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ 'exposeReactCreateComponent', + 'injectCreatePortal', 'injectHeaderUseEffect', 'injectErrorPageUseEffect', diff --git a/src/modules/patcher/patches/src/create-portal.ts b/src/modules/patcher/patches/src/create-portal.ts new file mode 100644 index 0000000..9f1f5b7 --- /dev/null +++ b/src/modules/patcher/patches/src/create-portal.ts @@ -0,0 +1,13 @@ +// @ts-ignore +declare const arguments: any; + +const $dom = arguments[1]; +if ($dom && $dom instanceof HTMLElement && $dom.id === 'gamepass-dialog-root') { + let showing = false; + const $child = $dom.firstElementChild; + const $dialog = $child?.firstElementChild; + if ($dialog) { + showing = !$dialog.className.includes('pageChangeExit'); + } + window.BxEventBus.Script.emit(showing ? 'dialog.shown' : 'dialog.dismissed', {}); +} diff --git a/src/modules/patcher/patches/src/streamhud.ts b/src/modules/patcher/patches/src/stream-hud.ts similarity index 100% rename from src/modules/patcher/patches/src/streamhud.ts rename to src/modules/patcher/patches/src/stream-hud.ts diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index 21f8035..5fb83a0 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -36,8 +36,8 @@ type ScriptEvents = { 'webgpu.ready': {}, - 'header.rendered': {}, - 'error.rendered': {}, + 'ui.header.rendered': {}, + 'ui.error.rendered': {}, }; type StreamEvents = {