diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index 6376ba4..b1daede 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -5707,10 +5707,25 @@ ${subsVar} = subs; if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; return PatcherUtils.injectUseEffect(str, index, "Stream", "ui.streamMenu.rendered"); }, + injectGuideHomeUseEffect(str) { + let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer'); + if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; + return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideHome.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; + }, + injectAchievementsProgressUseEffect(str) { + let index = str.indexOf('"AchievementsButton-module__progressBarContainer'); + if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; + return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementProgress.rendered"); + }, + injectAchievementsDetailUseEffect(str) { + let index = str.indexOf("GuideAchievementDetail.useParams()"); + if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const", index, 200)), index < 0) return !1; + return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementDetail.rendered"); } }, PATCH_ORDERS = PatcherUtils.filterPatches([ ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [ @@ -5719,8 +5734,11 @@ ${subsVar} = subs; ] : [], "exposeReactCreateComponent", "injectCreatePortal", + "injectGuideHomeUseEffect", "injectHeaderUseEffect", "injectErrorPageUseEffect", + "injectAchievementsProgressUseEffect", + "injectAchievementsDetailUseEffect", "gameCardCustomIcons", ...getGlobalPref("ui.imageQuality") < 90 ? [ "setImageQuality" @@ -8830,10 +8848,8 @@ class GuideMenu { 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 $buttons = this.renderButtons(); + if ($root.contains($buttons)) return; let $target = null; if (isPlaying) { $target = $root.querySelector("a[class*=QuitGameButton]"); @@ -8844,42 +8860,8 @@ class GuideMenu { 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; @@ -10334,68 +10316,6 @@ 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; @@ -10472,6 +10392,18 @@ BxEventBus.Stream.on("state.playing", (payload) => { BxEventBus.Script.on("ui.error.rendered", () => { BxEventBus.Stream.emit("state.stopped", {}); }); +BxEventBus.Script.on("ui.guideHome.rendered", () => { + let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && GuideMenu.getInstance().injectHome($root, STATES.isPlaying); +}); +BxEventBus.Script.on("ui.guideAchievementProgress.rendered", () => { + let $elm = document.querySelector("#gamepass-dialog-root button[class*=AchievementsButton-module__progressBarContainer]"); + if ($elm) TrueAchievements.getInstance().injectAchievementsProgress($elm); +}); +BxEventBus.Script.on("ui.guideAchievementDetail.rendered", () => { + let $elm = document.querySelector("#gamepass-dialog-root div[class^=AchievementDetailPage-module]"); + if ($elm) TrueAchievements.getInstance().injectAchievementDetailPage($elm); +}); BxEventBus.Stream.on("ui.streamMenu.rendered", async () => { await StreamUiHandler.handleStreamMenu(); }); @@ -10516,7 +10448,7 @@ function main() { BX_FLAGS.ForceNativeMkbTitles.push(...customList); } if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect(); + if (addCss(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect(); if (getGlobalPref("touchController.mode") === "all") TouchController.setup(); if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 710138a..7f9731b 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -246,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;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"] : [] +`;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");},injectGuideHomeUseEffect(str) {let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideHome.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;},injectAchievementsProgressUseEffect(str) {let index = str.indexOf('"AchievementsButton-module__progressBarContainer');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementProgress.rendered");},injectAchievementsDetailUseEffect(str) {let index = str.indexOf("GuideAchievementDetail.useParams()");if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const", index, 200)), index < 0) return !1;return PatcherUtils.injectUseEffect(str, index, "Script", "ui.guideAchievementDetail.rendered");}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectCreatePortal","injectGuideHomeUseEffect","injectHeaderUseEffect","injectErrorPageUseEffect","injectAchievementsProgressUseEffect","injectAchievementsDetailUseEffect","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" @@ -290,7 +290,7 @@ class RemotePlayDialog extends NavigationDialog {static instance;static getInsta 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 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 $buttons = this.renderButtons();if ($root.contains($buttons)) return;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;$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);}} 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", { 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");} @@ -389,7 +389,6 @@ class KeyboardShortcutHandler {static instance;static getInstance = () => Keyboa 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); @@ -407,6 +406,9 @@ getGlobalPref("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.r 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("ui.error.rendered", () => {BxEventBus.Stream.emit("state.stopped", {});}); +BxEventBus.Script.on("ui.guideHome.rendered", () => {let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");$root && GuideMenu.getInstance().injectHome($root, STATES.isPlaying);}); +BxEventBus.Script.on("ui.guideAchievementProgress.rendered", () => {let $elm = document.querySelector("#gamepass-dialog-root button[class*=AchievementsButton-module__progressBarContainer]");if ($elm) TrueAchievements.getInstance().injectAchievementsProgress($elm);}); +BxEventBus.Script.on("ui.guideAchievementDetail.rendered", () => {let $elm = document.querySelector("#gamepass-dialog-root div[class^=AchievementDetailPage-module]");if ($elm) TrueAchievements.getInstance().injectAchievementDetailPage($elm);}); 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();}); @@ -415,5 +417,5 @@ function unload() {if (!STATES.isPlaying) return;BxLogger.warning("Unloading"), BxEventBus.Stream.on("state.stopped", unload); window.addEventListener("pagehide", (e) => {BxEventBus.Stream.emit("state.stopped", {});}); window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {ScreenshotManager.getInstance().takeScreenshot();}); -function main() {if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {let customList = getGlobalPref("nativeMkb.forcedGames");BX_FLAGS.ForceNativeMkbTitles.push(...customList);}if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();if (getGlobalPref("touchController.mode") === "all") TouchController.setup();if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));} +function main() {if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {let customList = getGlobalPref("nativeMkb.forcedGames");BX_FLAGS.ForceNativeMkbTitles.push(...customList);}if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();if (addCss(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), WebGPUPlayer.prepare(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();if (getGlobalPref("touchController.mode") === "all") TouchController.setup();if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));} main(); diff --git a/src/index.ts b/src/index.ts index 6948b30..d4f3177 100755 --- a/src/index.ts +++ b/src/index.ts @@ -25,6 +25,7 @@ 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"; @@ -45,8 +46,7 @@ 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"; +import { TrueAchievements } from "./utils/true-achievements"; SettingsManager.getInstance(); @@ -264,6 +264,25 @@ BxEventBus.Script.on('ui.error.rendered', () => { BxEventBus.Stream.emit('state.stopped', {}); }); +BxEventBus.Script.on('ui.guideHome.rendered', () => { + const $root = document.querySelector('#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]'); + $root && GuideMenu.getInstance().injectHome($root, STATES.isPlaying); +}); + +BxEventBus.Script.on('ui.guideAchievementProgress.rendered', () => { + const $elm = document.querySelector('#gamepass-dialog-root button[class*=AchievementsButton-module__progressBarContainer]'); + if ($elm) { + TrueAchievements.getInstance().injectAchievementsProgress($elm as HTMLElement); + } +}); + +BxEventBus.Script.on('ui.guideAchievementDetail.rendered', () => { + const $elm = document.querySelector('#gamepass-dialog-root div[class^=AchievementDetailPage-module]'); + if ($elm) { + TrueAchievements.getInstance().injectAchievementDetailPage($elm as HTMLElement); + } +}); + BxEventBus.Stream.on('ui.streamMenu.rendered', async () => { await StreamUiHandler.handleStreamMenu(); }); @@ -403,12 +422,9 @@ function main() { disableAdobeAudienceManager(); } - RootDialogObserver.waitForRootDialog(); - // Setup UI addCss(); - GuideMenu.getInstance().addEventListeners(); StreamStatsCollector.setupEvents(); StreamBadges.setupEvents(); StreamStats.setupEvents(); diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index d4f52b0..13fc771 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1164,6 +1164,16 @@ ${subsVar} = subs; return PatcherUtils.injectUseEffect(str, index, 'Stream', 'ui.streamMenu.rendered'); }, + injectGuideHomeUseEffect(str: string) { + let index = str.indexOf('"HomeLandingPage-module__authenticatedContentContainer'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200)); + if (index < 0) { + return false; + } + + return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideHome.rendered'); + }, + injectCreatePortal(str: string) { let index = str.indexOf('.createPortal=function'); index > -1 && (index = PatcherUtils.indexOf(str, '{', index, 50, true)); @@ -1174,6 +1184,26 @@ ${subsVar} = subs; str = PatcherUtils.insertAt(str, index, codeCreatePortal); return str; }, + + injectAchievementsProgressUseEffect(str: string) { + let index = str.indexOf('"AchievementsButton-module__progressBarContainer'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200)); + if (index < 0) { + return false; + } + + return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideAchievementProgress.rendered'); + }, + + injectAchievementsDetailUseEffect(str: string) { + let index = str.indexOf('GuideAchievementDetail.useParams()'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'const', index, 200)); + if (index < 0) { + return false; + } + + return PatcherUtils.injectUseEffect(str, index, 'Script', 'ui.guideAchievementDetail.rendered'); + }, }; let PATCH_ORDERS = PatcherUtils.filterPatches([ @@ -1185,8 +1215,11 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ 'exposeReactCreateComponent', 'injectCreatePortal', + 'injectGuideHomeUseEffect', 'injectHeaderUseEffect', 'injectErrorPageUseEffect', + 'injectAchievementsProgressUseEffect', + 'injectAchievementsDetailUseEffect', 'gameCardCustomIcons', // 'gameCardPassTitle', diff --git a/src/modules/ui/guide-menu.ts b/src/modules/ui/guide-menu.ts index 2ca07ed..80be49f 100755 --- a/src/modules/ui/guide-menu.ts +++ b/src/modules/ui/guide-menu.ts @@ -1,11 +1,7 @@ -import { isFullVersion } from "@macros/build" with { type: "macro" }; - -import { BxEvent } from "@/utils/bx-event"; import { AppInterface, STATES } from "@/utils/global"; import { createButton, ButtonStyle, CE } from "@/utils/html"; import { t } from "@/utils/translation"; import { SettingsDialog } from "./dialog/settings-dialog"; -import { TrueAchievements } from "@/utils/true-achievements"; import { BxIcon } from "@/utils/bx-icon"; import { BxEventBus } from "@/utils/bx-event-bus"; import { getGlobalPref } from "@/utils/pref-utils"; @@ -141,11 +137,9 @@ export class GuideMenu { } injectHome($root: HTMLElement, isPlaying = false) { - if (isFullVersion()) { - const $achievementsProgress = $root.querySelector('button[class*=AchievementsButton-module__progressBarContainer]'); - if ($achievementsProgress) { - TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress as HTMLElement); - } + const $buttons = this.renderButtons(); + if ($root.contains($buttons)) { + return; } // Find the element to add buttons to @@ -169,67 +163,7 @@ export class GuideMenu { return false; } - const $buttons = this.renderButtons(); $buttons.dataset.isPlaying = isPlaying.toString(); $target.insertAdjacentElement('afterend', $buttons); } - - private onShown = async (e: Event) => { - const where = (e as any).where as GuideMenuTab; - - if (where === GuideMenuTab.HOME) { - const $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: HTMLElement) { - let className = $addedElm.className; - - // Fix custom buttons disappearing in Guide Menu (#551) - if (!className) { - className = $addedElm.firstElementChild?.className ?? ''; - } - - if (!className || className.startsWith('bx-')) { - return; - } - - // TrueAchievements - if (isFullVersion() && className.includes('AchievementsButton-module__progressBarContainer')) { - TrueAchievements.getInstance().injectAchievementsProgress($addedElm); - return; - } - - if (!className.startsWith('NavigationAnimation') && - !className.startsWith('DialogRoutes') && - !className.startsWith('Dialog-module__container')) { - return; - } - - // Achievement Details page - if (isFullVersion()) { - const $achievDetailPage = $addedElm.querySelector('div[class*=AchievementDetailPage]'); - if ($achievDetailPage) { - TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage as HTMLElement); - return; - } - } - - // Find navigation bar - const $selectedTab = $addedElm.querySelector('div[class^=NavigationMenu] button[aria-selected=true'); - if ($selectedTab) { - let $elm: Element | null = $selectedTab; - let index; - for (index = 0; ($elm = $elm?.previousElementSibling); index++); - - if (index === 0) { - BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: GuideMenuTab.HOME }); - } - } - } } diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index 5fb83a0..1662c45 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -38,6 +38,10 @@ type ScriptEvents = { 'ui.header.rendered': {}, 'ui.error.rendered': {}, + + 'ui.guideHome.rendered': {}, + 'ui.guideAchievementProgress.rendered': {}, + 'ui.guideAchievementDetail.rendered': {}, }; type StreamEvents = { diff --git a/src/utils/root-dialog-observer.ts b/src/utils/root-dialog-observer.ts deleted file mode 100755 index ca93ac0..0000000 --- a/src/utils/root-dialog-observer.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { GuideMenu } from "@/modules/ui/guide-menu"; -import { BX_FLAGS } from "./bx-flags"; -import { BxLogger } from "./bx-logger"; -import { BxIcon } from "./bx-icon"; -import { AppInterface } from "./global"; -import { createButton, ButtonStyle } from "./html"; -import { t } from "./translation"; -import { parseDetailsPath } from "./utils"; -import { BxEventBus } from "./bx-event-bus"; - - -export class RootDialogObserver { - private static $btnShortcut = AppInterface && createButton({ - icon: BxIcon.CREATE_SHORTCUT, - label: t('create-shortcut'), - style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK, - onClick: e => { - window.BX_EXPOSED.dialogRoutes?.closeAll(); - - const $btn = (e.target as HTMLElement).closest('button'); - AppInterface.createShortcut($btn?.dataset.path); - }, - }); - - private static $btnWallpaper = AppInterface && createButton({ - icon: BxIcon.DOWNLOAD, - label: t('wallpaper'), - style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_CASE | ButtonStyle.NORMAL_LINK, - onClick: e => { - window.BX_EXPOSED.dialogRoutes?.closeAll(); - - const $btn = (e.target as HTMLElement).closest('button'); - const details = parseDetailsPath($btn!.dataset.path!); - details && AppInterface.downloadWallpapers(details.titleSlug, details.productId); - }, - }); - - private static handleGameCardMenu($root: HTMLElement) { - const $detail = $root.querySelector('a[href^="/play/"]') as HTMLAnchorElement; - if (!$detail) { - return; - } - - const path = $detail.getAttribute('href')!; - RootDialogObserver.$btnShortcut.dataset.path = path; - RootDialogObserver.$btnWallpaper.dataset.path = path; - - $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper); - } - - private static handleAddedElement($root: HTMLElement, $addedElm: HTMLElement): boolean { - if (AppInterface && $addedElm.className.startsWith('SlideSheet-module__container')) { - // Game card's context menu - const $gameCardMenu = $addedElm.querySelector('div[class^=MruContextMenu],div[class^=GameCardContextMenu]'); - if ($gameCardMenu) { - RootDialogObserver.handleGameCardMenu($gameCardMenu); - return true; - } - } else if ($root.querySelector('div[class*=GuideDialog]')) { - // Guide menu - GuideMenu.getInstance().observe($addedElm); - return true; - } - - return false; - } - - private static observe($root: HTMLElement) { - let beingShown = false; - - const observer = new MutationObserver(mutationList => { - for (const mutation of mutationList) { - if (mutation.type !== 'childList') { - continue; - } - - BX_FLAGS.Debug && BxLogger.warning('RootDialog', 'added', mutation.addedNodes); - if (mutation.addedNodes.length === 1) { - const $addedElm = mutation.addedNodes[0]; - if ($addedElm instanceof HTMLElement) { - RootDialogObserver.handleAddedElement($root, $addedElm); - } - } - - const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); - if (shown !== beingShown) { - beingShown = shown; - BxEventBus.Script.emit(shown ? 'dialog.shown' : 'dialog.dismissed', {}); - } - } - }); - observer.observe($root, { subtree: true, childList: true }); - } - - public static waitForRootDialog() { - const observer = new MutationObserver(mutationList => { - for (const mutation of mutationList) { - if (mutation.type !== 'childList') { - continue; - } - - const $target = mutation.target as HTMLElement; - if ($target.id && $target.id === 'gamepass-dialog-root') { - observer.disconnect(); - RootDialogObserver.observe($target); - break; - } - }; - }); - observer.observe(document.documentElement, { subtree: true, childList: true }); - } -}