From 4b06d9fcffcdb4d67a596a61f505838e9224143d Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:01:03 +0700 Subject: [PATCH] useEffect() for Error page --- dist/better-xcloud.pretty.user.js | 29 ++++++++++---------- dist/better-xcloud.user.js | 14 ++++------ src/index.ts | 3 +- src/modules/patcher/patcher.ts | 22 +++++++++++---- src/modules/patcher/patches/src/streamhud.ts | 2 +- src/modules/stream/stream-stats.ts | 6 ++-- src/modules/stream/stream-ui.ts | 8 ------ src/utils/bx-event-bus.ts | 4 +-- 8 files changed, 44 insertions(+), 44 deletions(-) diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index 63def54..5d3b266 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -3094,7 +3094,7 @@ class StreamStats { $container; boundOnStreamHudStateChanged; constructor() { - BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnStreamHudStateChanged = this.onStreamHudStateChanged.bind(this), BxEventBus.Stream.on("ui.streamHud.expanded", this.boundOnStreamHudStateChanged), this.render(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnStreamHudStateChanged = this.onStreamHudStateChanged.bind(this), BxEventBus.Stream.on("ui.streamHud.rendered", this.boundOnStreamHudStateChanged), this.render(); } async start(glancing = !1) { if (!this.isHidden() || glancing && this.isGlancing()) return; @@ -3113,9 +3113,9 @@ class StreamStats { } isHidden = () => this.$container.classList.contains("bx-gone"); isGlancing = () => this.$container.dataset.display === "glancing"; - onStreamHudStateChanged({ state }) { + onStreamHudStateChanged({ expanded }) { if (!getStreamPref("stats.quickGlance.enabled")) return; - if (state === "expanded") this.isHidden() && this.start(!0); + if (expanded) this.isHidden() && this.start(!0); else this.stop(!0); } update = async (forceUpdate = !1) => { @@ -5130,7 +5130,7 @@ var game_card_icons_default = `var supportedInputIcons=$supportedInputIcons$,{pr var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged;this.orgOnGamepadInput=this.onGamepadInput;var match,onGamepadChangedStr=this.onGamepadChanged.toString();if(onGamepadChangedStr.startsWith("function "))onGamepadChangedStr=onGamepadChangedStr.substring(9);onGamepadChangedStr=onGamepadChangedStr.replaceAll("0","arguments[1]");eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);var onGamepadInputStr=this.onGamepadInput.toString();if(onGamepadInputStr.startsWith("function "))onGamepadInputStr=onGamepadInputStr.substring(9);match=onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);if(match){let gamepadIndexVar=match[0];onGamepadInputStr=onGamepadInputStr.replace("this.gamepadStates.get(",`this.gamepadStates.get(${gamepadIndexVar},`),eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`),BxLogger.info("supportLocalCoOp","✅ Successfully patched local co-op support")}else BxLogger.error("supportLocalCoOp","❌ Unable to patch local co-op support");this.toggleLocalCoOp=(enable)=>{BxLogger.info("toggleLocalCoOp",enable?"Enabled":"Disabled"),this.onGamepadChanged=enable?this.patchedOnGamepadChanged:this.orgOnGamepadChanged,this.onGamepadInput=enable?this.patchedOnGamepadInput:this.orgOnGamepadInput;let gamepads=window.navigator.getGamepads();for(let gamepad of gamepads){if(!gamepad?.connected)continue;if(gamepad.id.includes("Better xCloud"))continue;gamepad._noToast=!0,window.dispatchEvent(new GamepadEvent("gamepaddisconnected",{gamepad})),window.dispatchEvent(new GamepadEvent("gamepadconnected",{gamepad}))}};window.BX_EXPOSED.toggleLocalCoOp=this.toggleLocalCoOp.bind(null);\n'; var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`; var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`; -var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.expanded",{state:options.offset.x<0?"collapsed":"expanded"})});`; +var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; class PatcherUtils { static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) { if (startIndex < 0) return -1; @@ -5689,11 +5689,13 @@ ${subsVar} = subs; injectHeaderUseEffect(str) { let index = str.indexOf('"EdgewaterHeader-module__spaceBetween'); if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1; - let newCode = ` -window.BX_EXPOSED.reactUseEffect(() => { - window.BxEventBus.Script.emit('header.rendered', {}); -}); -`; + 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([ @@ -5703,6 +5705,7 @@ window.BX_EXPOSED.reactUseEffect(() => { ] : [], "exposeReactCreateComponent", "injectHeaderUseEffect", + "injectErrorPageUseEffect", "gameCardCustomIcons", ...getGlobalPref("ui.imageQuality") < 90 ? [ "setImageQuality" @@ -10245,10 +10248,6 @@ class StreamUiHandler { let $elm = $node; if (!($elm instanceof HTMLElement)) return; let className = $elm.className || ""; - if (className.includes("PureErrorPage")) { - BxEventBus.Stream.emit("state.error", {}); - return; - } if (className.startsWith("StreamMenu-module__container")) { StreamUiHandler.handleStreamMenu(); return; @@ -10479,7 +10478,7 @@ BxEventBus.Stream.on("state.playing", (payload) => { } updateVideoPlayer(); }); -BxEventBus.Stream.on("state.error", () => { +BxEventBus.Script.on("error.rendered", () => { BxEventBus.Stream.emit("state.stopped", {}); }); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { @@ -10504,7 +10503,7 @@ BxEventBus.Stream.on("dataChannelCreated", (payload) => { }); function unload() { if (!STATES.isPlaying) return; - 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 }); + 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 }); } BxEventBus.Stream.on("state.stopped", unload); window.addEventListener("pagehide", (e) => { diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 907b080..9f2b58f 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -162,7 +162,7 @@ function isPlainObject(input) {return typeof input === "object" && input !== nul class SoundShortcut {static adjustGainNodeVolume(amount) {if (!getGlobalPref("audio.volume.booster.enabled")) return 0;let currentValue = getStreamPref("audio.volume"), nearestValue;if (amount > 0) nearestValue = ceilToNearest(currentValue, amount);else nearestValue = floorToNearest(currentValue, -1 * amount);let newValue;if (currentValue !== nearestValue) newValue = nearestValue;else newValue = currentValue + amount;return newValue = setStreamPref("audio.volume", newValue, "direct"), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue;}static setGainNodeVolume(value) {STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100);}static muteUnmute() {if (getGlobalPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) {let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getStreamPref("audio.volume"), targetValue;if (settingValue === 0) targetValue = 100, setStreamPref("audio.volume", targetValue, "direct");else if (gainValue === 0) targetValue = settingValue;else targetValue = 0;let status;if (targetValue === 0) status = t("muted");else status = targetValue + "%";SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {state: targetValue === 0 ? 1 : 0});return;}let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");if ($media) {$media.muted = !$media.muted;let status = $media.muted ? t("muted") : t("unmuted");Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {state: $media.muted ? 1 : 0});}}} class StreamUiShortcut {static showHideStreamMenu() {window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();}} class StreamStatsCollector {static instance;static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);LOG_TAG = "StreamStatsCollector";static INTERVAL_BACKGROUND = 60000;calculateGrade(value, grades) {return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";}currentStats = {ping: {current: -1,grades: [40, 75, 100],toString() {return this.current === -1 ? "???" : this.current.toString().padStart(3);}},jit: {current: 0,grades: [30, 40, 60],toString() {return `${this.current.toFixed(1)}ms`.padStart(6);}},fps: {current: 0,toString() {let maxFps = getStreamPref("video.maxFps");return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString();}},btr: {current: 0,toString() {return `${this.current.toFixed(1)} Mbps`.padStart(9);}},fl: {received: 0,dropped: 0,toString() {let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;}},pl: {received: 0,dropped: 0,toString() {let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;}},dt: {current: 0,total: 0,grades: [6, 9, 12],toString() {return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6);}},dl: {total: 0,toString() {return humanFileSize(this.total).padStart(8);}},ul: {total: 0,toString() {return humanFileSize(this.total);}},play: {seconds: 0,startTime: 0,toString() {return secondsToHm(this.seconds);}},batt: {current: 100,start: 100,isCharging: !1,toString() {let text = `${this.current}%`;if (this.current !== this.start) {let diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : "";text += ` (${sign}${diffLevel}%)`;}return text;}},time: {toString() {return (new Date()).toLocaleTimeString([], {hour: "2-digit",minute: "2-digit",hour12: !1});}}};lastVideoStat;selectedCandidatePairId = null;constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}async collect() {let stats = await STATES.currentStream.peerConnection?.getStats();if (!stats) return;if (!this.selectedCandidatePairId) {let found = !1;stats.forEach((stat) => {if (found || stat.type !== "transport") return;if (stat = stat, stat.iceState === "connected" && stat.selectedCandidatePairId) this.selectedCandidatePairId = stat.selectedCandidatePairId, found = !0;});}stats.forEach((stat) => {if (stat.type === "inbound-rtp" && stat.kind === "video") {let fps = this.currentStats["fps"];fps.current = stat.framesPerSecond || 0;let pl = this.currentStats["pl"];pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived;let fl = this.currentStats["fl"];if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) {this.lastVideoStat = stat;return;}let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount;if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000;let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp;btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;let dt = this.currentStats["dt"];dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat;} else if (this.selectedCandidatePairId && stat.type === "candidate-pair" && stat.id === this.selectedCandidatePairId) {let ping = this.currentStats["ping"];ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;let dl = this.currentStats["dl"];dl.total = stat.bytesReceived;let ul = this.currentStats["ul"];ul.total = stat.bytesSent;}});let batteryLevel = 100, isCharging = !1;if (STATES.browser.capabilities.batteryApi) try {let bm = await navigator.getBattery();isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100);} catch (e) {}let battery = this.currentStats["batt"];battery.current = batteryLevel, battery.isCharging = isCharging;let playTime = this.currentStats["play"], now = +new Date;playTime.seconds = Math.ceil((now - playTime.startTime) / 1000);}getStat(kind) {return this.currentStats[kind];}reset() {let playTime = this.currentStats["play"];playTime.seconds = 0, playTime.startTime = +new Date;try {STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => {this.currentStats["batt"].start = Math.round(bm.level * 100);});} catch (e) {}}static setupEvents() {BxEventBus.Stream.on("state.playing", () => {StreamStatsCollector.getInstance().reset();});}} -class StreamStats {static instance;static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);LOG_TAG = "StreamStats";intervalId;REFRESH_INTERVAL = 1000;stats = {time: {name: t("clock"),$element: CE("span")},play: {name: t("playtime"),$element: CE("span")},batt: {name: t("battery"),$element: CE("span")},ping: {name: t("stat-ping"),$element: CE("span")},jit: {name: t("jitter"),$element: CE("span")},fps: {name: t("stat-fps"),$element: CE("span")},btr: {name: t("stat-bitrate"),$element: CE("span")},dt: {name: t("stat-decode-time"),$element: CE("span")},pl: {name: t("stat-packets-lost"),$element: CE("span")},fl: {name: t("stat-frames-lost"),$element: CE("span")},dl: {name: t("downloaded"),$element: CE("span")},ul: {name: t("uploaded"),$element: CE("span")}};$container;boundOnStreamHudStateChanged;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnStreamHudStateChanged = this.onStreamHudStateChanged.bind(this), BxEventBus.Stream.on("ui.streamHud.expanded", this.boundOnStreamHudStateChanged), this.render();}async start(glancing = !1) {if (!this.isHidden() || glancing && this.isGlancing()) return;this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);}async stop(glancing = !1) {if (glancing && !this.isGlancing()) return;this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");}async toggle() {if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");else this.isHidden() ? await this.start() : await this.stop();}destroy() {this.stop(), this.hideSettingsUi();}isHidden = () => this.$container.classList.contains("bx-gone");isGlancing = () => this.$container.dataset.display === "glancing";onStreamHudStateChanged({ state }) {if (!getStreamPref("stats.quickGlance.enabled")) return;if (state === "expanded") this.isHidden() && this.start(!0);else this.stop(!0);}update = async (forceUpdate = !1) => {if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {this.destroy();return;}let PREF_STATS_CONDITIONAL_FORMATTING = getStreamPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();await statsCollector.collect();let statKey;for (statKey in this.stats) {grade = "";let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);if ($element.dataset.grade !== grade) $element.dataset.grade = grade;}};refreshStyles() {let PREF_ITEMS = getStreamPref("stats.items"), PREF_OPACITY_BG = getStreamPref("stats.opacity.background"), $container = this.$container;if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getStreamPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;$container.style.opacity = getStreamPref("stats.opacity.all") + "%", $container.style.fontSize = getStreamPref("stats.textSize");}hideSettingsUi() {if (this.isGlancing() && !getStreamPref("stats.quickGlance.enabled")) this.stop();}async render() {this.$container = CE("div", { class: "bx-stats-bar bx-gone" });let statKey;for (statKey in this.stats) {let stat = this.stats[statKey], $div = CE("div", {class: `bx-stat-${statKey}`,title: stat.name}, CE("label", !1, statKey.toUpperCase()), stat.$element);this.$container.appendChild($div);}this.refreshStyles(), document.documentElement.appendChild(this.$container);}static setupEvents() {BxEventBus.Stream.on("state.playing", () => {let PREF_STATS_QUICK_GLANCE = getStreamPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getStreamPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();else if (PREF_STATS_QUICK_GLANCE) !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);});}static refreshStyles() {StreamStats.getInstance().refreshStyles();}} +class StreamStats {static instance;static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);LOG_TAG = "StreamStats";intervalId;REFRESH_INTERVAL = 1000;stats = {time: {name: t("clock"),$element: CE("span")},play: {name: t("playtime"),$element: CE("span")},batt: {name: t("battery"),$element: CE("span")},ping: {name: t("stat-ping"),$element: CE("span")},jit: {name: t("jitter"),$element: CE("span")},fps: {name: t("stat-fps"),$element: CE("span")},btr: {name: t("stat-bitrate"),$element: CE("span")},dt: {name: t("stat-decode-time"),$element: CE("span")},pl: {name: t("stat-packets-lost"),$element: CE("span")},fl: {name: t("stat-frames-lost"),$element: CE("span")},dl: {name: t("downloaded"),$element: CE("span")},ul: {name: t("uploaded"),$element: CE("span")}};$container;boundOnStreamHudStateChanged;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnStreamHudStateChanged = this.onStreamHudStateChanged.bind(this), BxEventBus.Stream.on("ui.streamHud.rendered", this.boundOnStreamHudStateChanged), this.render();}async start(glancing = !1) {if (!this.isHidden() || glancing && this.isGlancing()) return;this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);}async stop(glancing = !1) {if (glancing && !this.isGlancing()) return;this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");}async toggle() {if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");else this.isHidden() ? await this.start() : await this.stop();}destroy() {this.stop(), this.hideSettingsUi();}isHidden = () => this.$container.classList.contains("bx-gone");isGlancing = () => this.$container.dataset.display === "glancing";onStreamHudStateChanged({ expanded }) {if (!getStreamPref("stats.quickGlance.enabled")) return;if (expanded) this.isHidden() && this.start(!0);else this.stop(!0);}update = async (forceUpdate = !1) => {if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {this.destroy();return;}let PREF_STATS_CONDITIONAL_FORMATTING = getStreamPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();await statsCollector.collect();let statKey;for (statKey in this.stats) {grade = "";let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);if ($element.dataset.grade !== grade) $element.dataset.grade = grade;}};refreshStyles() {let PREF_ITEMS = getStreamPref("stats.items"), PREF_OPACITY_BG = getStreamPref("stats.opacity.background"), $container = this.$container;if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getStreamPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;$container.style.opacity = getStreamPref("stats.opacity.all") + "%", $container.style.fontSize = getStreamPref("stats.textSize");}hideSettingsUi() {if (this.isGlancing() && !getStreamPref("stats.quickGlance.enabled")) this.stop();}async render() {this.$container = CE("div", { class: "bx-stats-bar bx-gone" });let statKey;for (statKey in this.stats) {let stat = this.stats[statKey], $div = CE("div", {class: `bx-stat-${statKey}`,title: stat.name}, CE("label", !1, statKey.toUpperCase()), stat.$element);this.$container.appendChild($div);}this.refreshStyles(), document.documentElement.appendChild(this.$container);}static setupEvents() {BxEventBus.Stream.on("state.playing", () => {let PREF_STATS_QUICK_GLANCE = getStreamPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getStreamPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();else if (PREF_STATS_QUICK_GLANCE) !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);});}static refreshStyles() {StreamStats.getInstance().refreshStyles();}} class KeyHelper {static NON_PRINTABLE_KEYS = {Backquote: "`",Minus: "-",Equal: "=",BracketLeft: "[",BracketRight: "]",Backslash: "\\",Semicolon: ";",Quote: "'",Comma: ",",Period: ".",Slash: "/",NumpadMultiply: "Numpad *",NumpadAdd: "Numpad +",NumpadSubtract: "Numpad -",NumpadDecimal: "Numpad .",NumpadDivide: "Numpad /",NumpadEqual: "Numpad =",Mouse0: "Left Click",Mouse2: "Right Click",Mouse1: "Middle Click",ScrollUp: "Scroll Up",ScrollDown: "Scroll Down",ScrollLeft: "Scroll Left",ScrollRight: "Scroll Right"};static getKeyFromEvent(e) {let code = null, modifiers;if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0;else if (e instanceof WheelEvent) {if (e.deltaY < 0) code = "ScrollUp";else if (e.deltaY > 0) code = "ScrollDown";else if (e.deltaX < 0) code = "ScrollLeft";else if (e.deltaX > 0) code = "ScrollRight";} else if (e instanceof MouseEvent) code = "Mouse" + e.button;if (code) {let results = { code };if (modifiers) results.modifiers = modifiers;return results;}return null;}static getFullKeyCodeFromEvent(e) {let key = KeyHelper.getKeyFromEvent(e);return key ? `${key.code}:${key.modifiers || 0}` : "";}static parseFullKeyCode(str) {if (!str) return null;let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]);return {code,modifiers};}static codeToKeyName(key) {let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code];if (modifiers && modifiers !== 0) {if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) {if (modifiers & 2) text.unshift("Shift");if (modifiers & 4) text.unshift("Alt");if (modifiers & 1) text.unshift("Ctrl");}}return text.join(" + ");}} class PointerClient {static instance;static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);LOG_TAG = "PointerClient";REQUIRED_PROTOCOL_VERSION = 2;socket;mkbHandler;constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}start(port, mkbHandler) {if (!port) throw new Error("PointerServer port is 0");this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {BxLogger.info(this.LOG_TAG, "connected");}), this.socket.addEventListener("error", (event) => {BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);}), this.socket.addEventListener("close", (event) => {this.socket = null;}), this.socket.addEventListener("message", (event) => {let dataView = new DataView(event.data), messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;switch (messageType) {case 127:let protocolVersion = this.onProtocolVersion(dataView, offset);if (BxLogger.info(this.LOG_TAG, "Protocol version", protocolVersion), protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) alert("Required MKB protocol: " + protocolVersion), this.stop();break;case 1:this.onMove(dataView, offset);break;case 2:case 3:this.onPress(messageType, dataView, offset);break;case 4:this.onScroll(dataView, offset);break;case 5:this.onPointerCaptureChanged(dataView, offset);}});}onProtocolVersion(dataView, offset) {return dataView.getUint16(offset);}onMove(dataView, offset) {let x = dataView.getInt16(offset);offset += Int16Array.BYTES_PER_ELEMENT;let y = dataView.getInt16(offset);this.mkbHandler?.handleMouseMove({movementX: x,movementY: y});}onPress(messageType, dataView, offset) {let button = dataView.getUint8(offset);this.mkbHandler?.handleMouseClick({pointerButton: button,pressed: messageType === 2});}onScroll(dataView, offset) {let vScroll = dataView.getInt16(offset);offset += Int16Array.BYTES_PER_ELEMENT;let hScroll = dataView.getInt16(offset);this.mkbHandler?.handleMouseWheel({vertical: vScroll,horizontal: hScroll});}onPointerCaptureChanged(dataView, offset) {dataView.getInt8(offset) !== 1 && this.mkbHandler?.stop();}stop() {try {this.socket?.close();} catch (e) {}this.socket = null;}} class MouseDataProvider {mkbHandler;constructor(handler) {this.mkbHandler = handler;}init() {}destroy() {}} @@ -203,7 +203,7 @@ var game_card_icons_default = `var supportedInputIcons=$supportedInputIcons$,{pr var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged;this.orgOnGamepadInput=this.onGamepadInput;var match,onGamepadChangedStr=this.onGamepadChanged.toString();if(onGamepadChangedStr.startsWith("function "))onGamepadChangedStr=onGamepadChangedStr.substring(9);onGamepadChangedStr=onGamepadChangedStr.replaceAll("0","arguments[1]");eval(`this.patchedOnGamepadChanged = function ${onGamepadChangedStr}`);var onGamepadInputStr=this.onGamepadInput.toString();if(onGamepadInputStr.startsWith("function "))onGamepadInputStr=onGamepadInputStr.substring(9);match=onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);if(match){let gamepadIndexVar=match[0];onGamepadInputStr=onGamepadInputStr.replace("this.gamepadStates.get(",`this.gamepadStates.get(${gamepadIndexVar},`),eval(`this.patchedOnGamepadInput = function ${onGamepadInputStr}`),BxLogger.info("supportLocalCoOp","✅ Successfully patched local co-op support")}else BxLogger.error("supportLocalCoOp","❌ Unable to patch local co-op support");this.toggleLocalCoOp=(enable)=>{BxLogger.info("toggleLocalCoOp",enable?"Enabled":"Disabled"),this.onGamepadChanged=enable?this.patchedOnGamepadChanged:this.orgOnGamepadChanged,this.onGamepadInput=enable?this.patchedOnGamepadInput:this.orgOnGamepadInput;let gamepads=window.navigator.getGamepads();for(let gamepad of gamepads){if(!gamepad?.connected)continue;if(gamepad.id.includes("Better xCloud"))continue;gamepad._noToast=!0,window.dispatchEvent(new GamepadEvent("gamepaddisconnected",{gamepad})),window.dispatchEvent(new GamepadEvent("gamepadconnected",{gamepad}))}};window.BX_EXPOSED.toggleLocalCoOp=this.toggleLocalCoOp.bind(null);\n'; var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`; var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`; -var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.expanded",{state:options.offset.x<0?"collapsed":"expanded"})});`; +var streamhud_default = `var options=arguments[0];window.BX_EXPOSED.showStreamMenu=options.onShowStreamMenu;options.guideUI=null;window.BX_EXPOSED.reactUseEffect(()=>{window.BxEventBus.Stream.emit("ui.streamHud.rendered",{expanded:options.offset.x===0})});`; class PatcherUtils {static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.indexOf(searchString, startIndex);if (index < 0 || maxRange && index - startIndex > maxRange) return -1;return after ? index + searchString.length : index;}static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {if (startIndex < 0) return -1;let index = txt.lastIndexOf(searchString, startIndex);if (index < 0 || maxRange && startIndex - index > maxRange) return -1;return after ? index + searchString.length : index;}static insertAt(txt, index, insertString) {return txt.substring(0, index) + insertString + txt.substring(index);}static replaceWith(txt, index, fromString, toString) {return txt.substring(0, index) + toString + txt.substring(index + fromString.length);}static filterPatches(patches) {return patches.filter((item2) => !!item2);}static patchBeforePageLoad(str, page) {let text = `chunkName:()=>"${page}-page",`;if (!str.includes(text)) return !1;return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str;}static isVarCharacter(char) {let code = char.charCodeAt(0), isUppercase = code >= 65 && code <= 90, isLowercase = code >= 97 && code <= 122, isDigit = code >= 48 && code <= 57;return isUppercase || isLowercase || isDigit || (char === "_" || char === "$");}static getVariableNameBefore(str, index) {if (index < 0) return null;let end = index, start = end - 1;while (PatcherUtils.isVarCharacter(str[start]))start -= 1;return str.substring(start + 1, end);}static getVariableNameAfter(str, index) {if (index < 0) return null;let start = index, end = start + 1;while (PatcherUtils.isVarCharacter(str[end]))end += 1;return str.substring(start, end);}} var LOG_TAG2 = "Patcher", PATCHES = {disableAiTrack(str) {let text = ".track=function(", index = str.indexOf(text);if (index < 0) return !1;if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) return !1;return PatcherUtils.replaceWith(str, index, text, ".track=function(e){},!!function(");},disableTelemetry(str) {let text = ".disableTelemetry=function(){return!1}";if (!str.includes(text)) return !1;return str.replace(text, ".disableTelemetry=function(){return!0}");},disableTelemetryProvider(str) {let text = "this.enableLightweightTelemetry=!";if (!str.includes(text)) return !1;let newCode = ["this.trackEvent","this.trackPageView","this.trackHttpCompleted","this.trackHttpFailed","this.trackError","this.trackErrorLike","this.onTrackEvent","()=>{}"].join("=");return str.replace(text, newCode + ";" + text);},disableIndexDbLogging(str) {let text = ",this.logsDb=new";if (!str.includes(text)) return !1;let newCode = ",this.log=()=>{}";return str.replace(text, newCode + text);},websiteLayout(str) {let text = '?"tv":"default"';if (!str.includes(text)) return !1;let layout = getGlobalPref("ui.layout") === "tv" ? "tv" : "default";return str.replace(text, `?"${layout}":"${layout}"`);},remotePlayDirectConnectUrl(str) {let index = str.indexOf("/direct-connect");if (index < 0) return !1;return str.replace(str.substring(index - 9, index + 15), "https://www.xbox.com/play");},remotePlayKeepAlive(str) {let text = "onServerDisconnectMessage(e){";if (!str.includes(text)) return !1;return str = str.replace(text, text + remote_play_keep_alive_default), str;},remotePlayConnectMode(str) {let text = 'connectMode:"cloud-connect",';if (!str.includes(text)) return !1;let newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect", remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`;return str.replace(text, newCode);},remotePlayDisableAchievementToast(str) {let text = ".AchievementUnlock:{";if (!str.includes(text)) return !1;let newCode = "if (!!window.BX_REMOTE_PLAY_CONFIG) return;";return str.replace(text, text + newCode);},remotePlayRecentlyUsedTitleIds(str) {let text = "(e.data.recentlyUsedTitleIds)){";if (!str.includes(text)) return !1;let newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;";return str.replace(text, text + newCode);},remotePlayWebTitle(str) {let text = "titleTemplate:void 0,title:", index = str.indexOf(text);if (index < 0) return !1;return str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t("remote-play")} - Better xCloud" :`), str;},blockWebRtcStatsCollector(str) {let text = "this.shouldCollectStats=!0";if (!str.includes(text)) return !1;return str.replace(text, "this.shouldCollectStats=!1");},patchPollGamepads(str) {let index = str.indexOf("},this.pollGamepads=()=>{");if (index < 0) return !1;let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index);if (setTimeoutIndex < 0) return !1;let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ");if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getGlobalPref("block.tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", "");let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/);if (!match) return !1;let newCode = renderString(poll_gamepad_default, {gamepadVar: match[1]});if (codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"), match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/), !match) return !1;let xCloudGamepadVar = match[1], inputFeedbackManager = PatcherUtils.indexOf(codeBlock, "this.inputFeedbackManager.onGamepadConnected(", 0, 1e4), backetIndex = PatcherUtils.indexOf(codeBlock, "}", inputFeedbackManager, 100);if (backetIndex < 0) return !1;let customizationCode = ";";return customizationCode += renderString(controller_customization_default, { xCloudGamepadVar }), codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode), str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str;},enableXcloudLogger(str) {let text = "this.telemetryProvider=e}log(e,t,r){";if (!str.includes(text)) return !1;let newCode = ` @@ -245,9 +245,7 @@ if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {const settings = let subs = ${subsVar}; subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); ${subsVar} = subs; -`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;if (str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactCreateElement="), index = PatcherUtils.indexOf(str, ".useEffect=", index), index < 0) return !1;return str = PatcherUtils.insertAt(str, index - 1, "window.BX_EXPOSED.reactUseEffect="), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);if (arrowIndex < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!paramVar || !supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {param: paramVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;},injectHeaderUseEffect(str) {let index = str.indexOf('"EdgewaterHeader-module__spaceBetween');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "return", index, 300)), index < 0) return !1;let newCode = ` -window.BX_EXPOSED.reactUseEffect(() => {window.BxEventBus.Script.emit('header.rendered', {});}); -`;return str = PatcherUtils.insertAt(str, index, newCode), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","injectHeaderUseEffect","gameCardCustomIcons",...getGlobalPref("ui.imageQuality") < 90 ? ["setImageQuality"] : [],"modifyPreloadedState","optimizeGameSlugGenerator","detectBrowserRouterReady","patchRequestInfoCrash","disableStreamGate","broadcastPollingMode","patchGamepadPolling","exposeStreamSession","exposeDialogRoutes","homePageBeforeLoad","productDetailPageBeforeLoad","streamPageBeforeLoad","guideAchievementsDefaultLocked","enableTvRoutes","supportLocalCoOp","overrideStorageGetSettings",getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",getGlobalPref("ui.layout") !== "default" && "websiteLayout",getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",...STATES.userAgent.capabilities.touch ? ["disableTouchContextMenu"] : [],...getGlobalPref("block.tracking") ? ["disableAiTrack","disableTelemetry","blockWebRtcStatsCollector","disableIndexDbLogging","disableTelemetryProvider"] : [],...getGlobalPref("xhome.enabled") ? ["remotePlayKeepAlive","remotePlayDirectConnectUrl","remotePlayDisableAchievementToast","remotePlayRecentlyUsedTitleIds","remotePlayWebTitle",STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"] : [],...BX_FLAGS.EnableXcloudLogging ? ["enableConsoleLogging","enableXcloudLogger"] : [] +`;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"] : [] ]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([hideSections.includes("news") && "ignoreNewsSection",(getGlobalPref("block.features").includes("friends") || hideSections.includes("friends")) && "ignorePlayWithFriendsSection",hideSections.includes("all-games") && "ignoreAllGamesSection",hideSections.includes("genres") && "ignoreGenresSection",!getGlobalPref("block.features").includes("byog") && hideSections.includes("byog") && "ignoreByogSection",STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",...getGlobalPref("ui.imageQuality") < 90 ? ["setBackgroundImageQuality"] : [],...blockSomeNotifications() ? ["changeNotificationsSubscription"] : [] ]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["exposeInputChannel","patchXcloudTitleInfo","disableGamepadDisconnectedScreen","patchStreamHud","playVibration","alwaysShowStreamHud",getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",...STATES.userAgent.capabilities.touch ? [getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"] : [],BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging","patchPollGamepads",getGlobalPref("stream.video.combineAudio") && "streamCombineSources",...getGlobalPref("xhome.enabled") ? ["patchRemotePlayMkb","remotePlayConnectMode"] : [],...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["patchMouseAndKeyboardEnabled","disableNativeRequestPointerLock"] : [] ]), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches(["detectProductDetailPage" @@ -386,7 +384,7 @@ 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.includes("PureErrorPage")) {BxEventBus.Stream.emit("state.error", {});return;}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 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}; @@ -407,10 +405,10 @@ BxEventBus.Stream.on("state.loading", () => {if (window.location.pathname.includ 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.error", () => {BxEventBus.Stream.emit("state.stopped", {});}); +BxEventBus.Script.on("error.rendered", () => {BxEventBus.Stream.emit("state.stopped", {});}); 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;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 });} +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 });} BxEventBus.Stream.on("state.stopped", unload); window.addEventListener("pagehide", (e) => {BxEventBus.Stream.emit("state.stopped", {});}); window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {ScreenshotManager.getInstance().takeScreenshot();}); diff --git a/src/index.ts b/src/index.ts index bb4ec3b..d6e226e 100755 --- a/src/index.ts +++ b/src/index.ts @@ -261,7 +261,7 @@ BxEventBus.Stream.on('state.playing', payload => { updateVideoPlayer(); }); -BxEventBus.Stream.on('state.error', () => { +BxEventBus.Script.on('error.rendered', () => { BxEventBus.Stream.emit('state.stopped', {}); }); @@ -324,6 +324,7 @@ function unload() { return; } + BxLogger.warning('Unloading'); if (isFullVersion()) { KeyboardShortcutHandler.getInstance().stop(); diff --git a/src/modules/patcher/patcher.ts b/src/modules/patcher/patcher.ts index 5d3f402..fc22a9e 100755 --- a/src/modules/patcher/patcher.ts +++ b/src/modules/patcher/patcher.ts @@ -1136,16 +1136,23 @@ ${subsVar} = subs; injectHeaderUseEffect(str: string) { let index = str.indexOf('"EdgewaterHeader-module__spaceBetween'); index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 300)); - if (index < 0) { return false; } - const newCode = ` -window.BX_EXPOSED.reactUseEffect(() => { - window.BxEventBus.Script.emit('header.rendered', {}); -}); -`; + const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('header.rendered', {}));`; + str = PatcherUtils.insertAt(str, index, newCode); + return str; + }, + + injectErrorPageUseEffect(str: string) { + let index = str.indexOf('"PureErrorPage-module__container'); + index > -1 && (index = PatcherUtils.lastIndexOf(str, 'return', index, 200)); + if (index < 0) { + return false; + } + + const newCode = `window.BX_EXPOSED.reactUseEffect(() => window.BxEventBus.Script.emit('error.rendered', {}));`; str = PatcherUtils.insertAt(str, index, newCode); return str; }, @@ -1158,7 +1165,10 @@ let PATCH_ORDERS = PatcherUtils.filterPatches([ ] : []), 'exposeReactCreateComponent', + 'injectHeaderUseEffect', + 'injectErrorPageUseEffect', + 'gameCardCustomIcons', // 'gameCardPassTitle', diff --git a/src/modules/patcher/patches/src/streamhud.ts b/src/modules/patcher/patches/src/streamhud.ts index 952bc0f..324c2b0 100644 --- a/src/modules/patcher/patches/src/streamhud.ts +++ b/src/modules/patcher/patches/src/streamhud.ts @@ -9,5 +9,5 @@ window.BX_EXPOSED.showStreamMenu = options.onShowStreamMenu; options.guideUI = null; window.BX_EXPOSED.reactUseEffect(() => { - window.BxEventBus.Stream.emit('ui.streamHud.expanded', { state: options.offset.x < 0 ? 'collapsed' : 'expanded' }); + window.BxEventBus.Stream.emit('ui.streamHud.rendered', { expanded: options.offset.x === 0 }); }); diff --git a/src/modules/stream/stream-stats.ts b/src/modules/stream/stream-stats.ts index ee5b34b..05a9c54 100755 --- a/src/modules/stream/stream-stats.ts +++ b/src/modules/stream/stream-stats.ts @@ -75,7 +75,7 @@ export class StreamStats { BxLogger.info(this.LOG_TAG, 'constructor()'); this.boundOnStreamHudStateChanged = this.onStreamHudStateChanged.bind(this); - BxEventBus.Stream.on('ui.streamHud.expanded', this.boundOnStreamHudStateChanged); + BxEventBus.Stream.on('ui.streamHud.rendered', this.boundOnStreamHudStateChanged); this.render(); } @@ -122,12 +122,12 @@ export class StreamStats { isHidden = () => this.$container.classList.contains('bx-gone'); isGlancing = () => this.$container.dataset.display === 'glancing'; - onStreamHudStateChanged({ state }: { state: string }) { + onStreamHudStateChanged({ expanded }: { expanded: boolean }) { if (!getStreamPref(StreamPref.STATS_QUICK_GLANCE_ENABLED)) { return; } - if (state === 'expanded') { + if (expanded) { this.isHidden() && this.start(true); } else { this.stop(true); diff --git a/src/modules/stream/stream-ui.ts b/src/modules/stream/stream-ui.ts index 38662c5..ac36cb5 100755 --- a/src/modules/stream/stream-ui.ts +++ b/src/modules/stream/stream-ui.ts @@ -5,7 +5,6 @@ import { t } from "@utils/translation.ts"; import { StreamBadges } from "./stream-badges.ts"; import { StreamStats } from "./stream-stats.ts"; import { SettingsDialog } from "../ui/dialog/settings-dialog.ts"; -import { BxEventBus } from "@/utils/bx-event-bus.ts"; export class StreamUiHandler { @@ -240,13 +239,6 @@ export class StreamUiHandler { } const className = $elm.className || ''; - - // Error Page: .PureErrorPage.ErrorScreen - if (className.includes('PureErrorPage')) { - BxEventBus.Stream.emit('state.error', {}); - return; - } - // Render badges if (className.startsWith('StreamMenu-module__container')) { StreamUiHandler.handleStreamMenu(); diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index e07b105..8cd49d6 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -37,6 +37,7 @@ type ScriptEvents = { 'webgpu.ready': {}, 'header.rendered': {}, + 'error.rendered': {}, }; type StreamEvents = { @@ -44,7 +45,6 @@ type StreamEvents = { 'state.starting': {}; 'state.playing': { $video?: HTMLVideoElement }; 'state.stopped': {}; - 'state.error': {}; 'xboxTitleId.changed': { id: number; @@ -68,7 +68,7 @@ type StreamEvents = { // Inside patch 'microphone.state.changed': { state: MicrophoneState }; - 'ui.streamHud.expanded': { state: 'expanded' | 'collapsed' }, + 'ui.streamHud.rendered': { expanded: boolean }, dataChannelCreated: { dataChannel: RTCDataChannel }; };