diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index 6f3f3da..9174de4 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -5697,6 +5697,12 @@ ${subsVar} = subs; 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" ? [ @@ -5772,6 +5778,7 @@ ${subsVar} = subs; "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", @@ -10156,111 +10163,6 @@ class ProductDetailsPage { }, 500); } } -class StreamUiHandler { - static $btnStreamSettings; - static $btnStreamStats; - static $btnRefresh; - static $btnHome; - static observer; - 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; - 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, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0; - } - static observe() { - StreamUiHandler.reset(); - let $screen = document.querySelector("#PageContent section[class*=PureScreens]"); - if (!$screen) return; - let observer = new MutationObserver((mutationList) => { - let item2; - for (item2 of mutationList) { - if (item2.type !== "childList") continue; - item2.addedNodes.forEach(async ($node) => { - if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; - let $elm = $node; - if (!($elm instanceof HTMLElement)) return; - let className = $elm.className || ""; - if (className.startsWith("StreamMenu-module__container")) { - StreamUiHandler.handleStreamMenu(); - return; - } - if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud"); - if (!$elm || ($elm.id || "") !== "StreamHud") return; - StreamUiHandler.handleSystemMenu($elm); - }); - } - }); - observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer; - } -} class RootDialogObserver { static $btnShortcut = AppInterface && createButton({ icon: BxIcon.CREATE_SHORTCUT, @@ -10405,6 +10307,86 @@ class DeviceVibrationManager { 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; + } +} SettingsManager.getInstance(); if (window.location.pathname.includes("/auth/msa")) { let nativePushState = window.history.pushState; @@ -10468,7 +10450,7 @@ BxEventBus.Stream.on("state.starting", () => { } }); BxEventBus.Stream.on("state.playing", (payload) => { - window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0, StreamUiHandler.observe(); + window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0; { let gameBar = GameBar.getInstance(); if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar(); @@ -10481,6 +10463,13 @@ BxEventBus.Stream.on("state.playing", (payload) => { BxEventBus.Script.on("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/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index d8e5867..27ae640 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -245,9 +245,9 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {const settings = let subs = ${subsVar}; subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); ${subsVar} = subs; -`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;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;}}, 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;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"] : [] ]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([hideSections.includes("news") && "ignoreNewsSection",(getGlobalPref("block.features").includes("friends") || hideSections.includes("friends")) && "ignorePlayWithFriendsSection",hideSections.includes("all-games") && "ignoreAllGamesSection",hideSections.includes("genres") && "ignoreGenresSection",!getGlobalPref("block.features").includes("byog") && hideSections.includes("byog") && "ignoreByogSection",STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",...getGlobalPref("ui.imageQuality") < 90 ? ["setBackgroundImageQuality"] : [],...blockSomeNotifications() ? ["changeNotificationsSubscription"] : [] -]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["exposeInputChannel","patchXcloudTitleInfo","disableGamepadDisconnectedScreen","patchStreamHud","playVibration","alwaysShowStreamHud",getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",...STATES.userAgent.capabilities.touch ? [getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"] : [],BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging","patchPollGamepads",getGlobalPref("stream.video.combineAudio") && "streamCombineSources",...getGlobalPref("xhome.enabled") ? ["patchRemotePlayMkb","remotePlayConnectMode"] : [],...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["patchMouseAndKeyboardEnabled","disableNativeRequestPointerLock"] : [] +]), 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" ]), ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS]; class Patcher {static remainingPatches = {home: HOME_PAGE_PATCH_ORDERS,stream: STREAM_PAGE_PATCH_ORDERS,"product-detail": PRODUCT_DETAIL_PAGE_PATCH_ORDERS};static patchPage(page) {let remaining = Patcher.remainingPatches[page];if (!remaining) return;PATCH_ORDERS = PATCH_ORDERS.concat(remaining), delete Patcher.remainingPatches[page];}static patchNativeBind() {let nativeBind = Function.prototype.bind;Function.prototype.bind = function() {let valid = !1;if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;}if (!valid) return nativeBind.apply(this, arguments);if (typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;let orgFunc = this, newFunc = (a, item2) => {Patcher.checkChunks(item2), orgFunc(a, item2);};return nativeBind.apply(newFunc, arguments);};}static checkChunks(item) {let patchesToCheck, appliedPatches, chunkData = item[1], patchesMap = {}, patcherCache = PatcherCache.getInstance();for (let chunkId in chunkData) {appliedPatches = [];let cachedPatches = patcherCache.getPatches(chunkId);if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);else patchesToCheck = PATCH_ORDERS.slice(0);if (!patchesToCheck.length) continue;let func = chunkData[chunkId], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1, chunkAppliedPatches = [];for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) {let patchName = patchesToCheck[patchIndex];if (appliedPatches.indexOf(patchName) > -1) continue;if (!PATCHES[patchName]) continue;let tmpStr = PATCHES[patchName].call(null, patchedFuncStr);if (!tmpStr) continue;modified = !0, patchedFuncStr = tmpStr, appliedPatches.push(patchName), chunkAppliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName);}if (modified) {BxLogger.info(LOG_TAG2, `✅ [${chunkId}] ${chunkAppliedPatches.join(", ")}`), PATCH_ORDERS.length && BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS), BX_FLAGS.Debug && console.time(LOG_TAG2);try {chunkData[chunkId] = eval(patchedFuncStr);} catch (e) {if (e instanceof Error) BxLogger.error(LOG_TAG2, "Error", appliedPatches, e.message, patchedFuncStr);}BX_FLAGS.Debug && console.timeEnd(LOG_TAG2);}if (appliedPatches.length) patchesMap[chunkId] = appliedPatches;}if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);}static init() {Patcher.patchNativeBind();}} @@ -384,11 +384,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 StreamUiHandler {static $btnStreamSettings;static $btnStreamStats;static $btnRefresh;static $btnHome;static observer;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;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, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0;}static observe() {StreamUiHandler.reset();let $screen = document.querySelector("#PageContent section[class*=PureScreens]");if (!$screen) return;let observer = new MutationObserver((mutationList) => {let item2;for (item2 of mutationList) {if (item2.type !== "childList") continue;item2.addedNodes.forEach(async ($node) => {if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return;let $elm = $node;if (!($elm instanceof HTMLElement)) return;let className = $elm.className || "";if (className.startsWith("StreamMenu-module__container")) {StreamUiHandler.handleStreamMenu();return;}if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud");if (!$elm || ($elm.id || "") !== "StreamHud") return;StreamUiHandler.handleSystemMenu($elm);});}});observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer;}} 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;}} 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); @@ -404,8 +404,10 @@ BxEventBus.Script.on("header.rendered", () => {HeaderSection.getInstance().check 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, StreamUiHandler.observe();{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.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.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();}); BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label !== "message") return;dataChannel.addEventListener("message", async (msg) => {if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;if (!msg.data.includes("/titleinfo")) return;let currentStream = STATES.currentStream, json = JSON.parse(JSON.parse(msg.data).content), currentId = currentStream.xboxTitleId ?? null, newId = parseInt(json.titleid, 16);if (STATES.remotePlay.isPlaying) if (currentStream.titleSlug = "remote-play", json.focused) {let productTitle = await XboxApi.getProductTitle(newId);if (productTitle) currentStream.titleSlug = productTitleToSlug(productTitle);else newId = -1;} else newId = 0;if (currentId !== newId) currentStream.xboxTitleId = newId, BxEventBus.Stream.emit("xboxTitleId.changed", {id: newId});});}); function unload() {if (!STATES.isPlaying) return;BxLogger.warning("Unloading"), KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayerManager?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });} diff --git a/src/index.ts b/src/index.ts index d6e226e..4ae82ea 100755 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,6 @@ import { GameTile } from "./modules/ui/game-tile"; import { ProductDetailsPage } from "./modules/ui/product-details"; import { NavigationDialogManager } from "./modules/ui/dialog/navigation-dialog"; import { GlobalPref, StreamPref } from "./enums/pref-keys"; -import { StreamUiHandler } from "./modules/stream/stream-ui"; import { UserAgent } from "./utils/user-agent"; import { XboxApi } from "./utils/xbox-api"; import { StreamStatsCollector } from "./utils/stream-stats-collector"; @@ -47,6 +46,7 @@ import { getGlobalPref, getStreamPref } from "./utils/pref-utils"; 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"; SettingsManager.getInstance(); @@ -234,7 +234,6 @@ BxEventBus.Stream.on('state.playing', payload => { } STATES.isPlaying = true; - StreamUiHandler.observe(); if (isFullVersion()) { const gameBar = GameBar.getInstance(); @@ -265,6 +264,16 @@ BxEventBus.Script.on('error.rendered', () => { BxEventBus.Stream.emit('state.stopped', {}); }); +BxEventBus.Stream.on('ui.streamMenu.rendered', async () => { + await StreamUiHandler.handleStreamMenu(); +}); + +BxEventBus.Stream.on('ui.streamHud.rendered', async () => { + const $elm = document.querySelector('#StreamHud'); + $elm && StreamUiHandler.handleSystemMenu($elm); +}); + + isFullVersion() && window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, e => { const component = (e as any).component; if (component === 'product-detail') { diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index e16b295..575676c 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1156,6 +1156,18 @@ ${subsVar} = subs; str = PatcherUtils.insertAt(str, index, newCode); return str; }, + + injectStreamMenuUseEffect(str: string) { + let index = str.indexOf('"StreamMenu-module__container'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200)); + if (index < 0) { + return false; + } + + const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Stream.emit('ui.streamMenu.rendered', {}));`; + str = PatcherUtils.insertAt(str, index, newCode); + return str; + }, }; let PATCH_ORDERS = PatcherUtils.filterPatches([ @@ -1265,6 +1277,8 @@ let STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ 'alwaysShowStreamHud', + 'injectStreamMenuUseEffect', + // 'exposeEventTarget', // Patch volume control for normal stream diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts index ac36cb5..0242c06 100755 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -12,7 +12,6 @@ export class StreamUiHandler { private static $btnStreamStats: HTMLElement | null | undefined; private static $btnRefresh: HTMLElement | null | undefined; private static $btnHome: HTMLElement | null | undefined; - private static observer: MutationObserver | undefined; private static cloneStreamHudButton($btnOrg: HTMLElement, label: string, svgIcon: BxIconRaw): HTMLElement | null { if (!$btnOrg) { @@ -100,7 +99,7 @@ export class StreamUiHandler { return $btn; } - private static async handleStreamMenu() { + static async handleStreamMenu() { const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]'); if (!$btnCloseHud) { return; @@ -133,13 +132,17 @@ export class StreamUiHandler { $menu?.appendChild(await StreamBadges.getInstance().render()); } - private static handleSystemMenu($streamHud: HTMLElement) { + static handleSystemMenu($streamHud: HTMLElement) { // Get the last button const $orgButton = $streamHud.querySelector('div[class^=HUDButton]'); if (!$orgButton) { return; } + if (StreamUiHandler.$btnStreamSettings && $streamHud.contains(StreamUiHandler.$btnStreamSettings)) { + return; + } + const hideGripHandle = () => { // Grip handle const $gripHandle = document.querySelector('#StreamHud button[class^=GripHandle]'); @@ -206,60 +209,5 @@ export class StreamUiHandler { StreamUiHandler.$btnStreamStats = undefined; StreamUiHandler.$btnRefresh = undefined; StreamUiHandler.$btnHome = undefined; - - StreamUiHandler.observer && StreamUiHandler.observer.disconnect(); - StreamUiHandler.observer = undefined; - } - - static observe() { - StreamUiHandler.reset(); - - const $screen = document.querySelector('#PageContent section[class*=PureScreens]'); - if (!$screen) { - return; - } - - const observer = new MutationObserver(mutationList => { - let item: MutationRecord; - for (item of mutationList) { - if (item.type !== 'childList') { - continue; - } - - item.addedNodes.forEach(async $node => { - if (!$node || $node.nodeType !== Node.ELEMENT_NODE) { - return; - } - - let $elm: HTMLElement | null = $node as HTMLElement; - - // Ignore non-HTML elements - if (!($elm instanceof HTMLElement)) { - return; - } - - const className = $elm.className || ''; - // Render badges - if (className.startsWith('StreamMenu-module__container')) { - StreamUiHandler.handleStreamMenu(); - return; - } - - if (className.startsWith('Overlay-module_') || className.startsWith('InProgressScreen')) { - $elm = $elm.querySelector('#StreamHud'); - } - - if (!$elm || ($elm.id || '') !== 'StreamHud') { - return; - } - - // Handle System Menu bar - StreamUiHandler.handleSystemMenu($elm); - }); - }; - }); - - observer.observe($screen, { subtree: true, childList: true }); - StreamUiHandler.observer = observer; } } diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index 8cd49d6..21f8035 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -69,6 +69,7 @@ type StreamEvents = { 'microphone.state.changed': { state: MicrophoneState }; 'ui.streamHud.rendered': { expanded: boolean }, + 'ui.streamMenu.rendered': {}, dataChannelCreated: { dataChannel: RTCDataChannel }; };