diff --git a/build.sh b/build.sh index db34dc6..2c767c9 100755 --- a/build.sh +++ b/build.sh @@ -5,8 +5,9 @@ build_all () { printf "\033c" # Build all variants - bun build.ts --version $1 --variant full - bun build.ts --version $1 --variant lite + bun build.ts --version $1 --variant full --meta + bun build.ts --version $1 --variant full --pretty + # bun build.ts --version $1 --variant lite # Wait for key read -p ">> Press Enter to build again..." diff --git a/build.ts b/build.ts index c3e5070..cf7d015 100755 --- a/build.ts +++ b/build.ts @@ -21,7 +21,6 @@ enum BuildTarget { type BuildVariant = 'full' | 'lite'; const MINIFY_SYNTAX = true; -const INDENT_SPACES = false; function minifySvgImports(str: string): string { // Minify SVG imports @@ -73,7 +72,7 @@ function removeComments(str: string): string { return str; } -function postProcess(str: string): string { +function postProcess(str: string, pretty: boolean): string { // Unescape unicode charaters str = unescape((str.replace(/\\u/g, '%u'))); // Replace \x00 to normal character @@ -128,12 +127,16 @@ function postProcess(str: string): string { if (MINIFY_SYNTAX) { str = minifyIfElse(str); - str = str.replaceAll(/\n(\s+)/g, (match, p1) => { - if (INDENT_SPACES) { - const len = p1.length / 2; - return '\n' + ' '.repeat(len); + str = str.replaceAll(/\n(\s+|\})/g, (match, p1) => { + if (pretty) { + if (p1 === '}') { + return '\n}'; + } else { + const len = p1.length / 2; + return '\n' + ' '.repeat(len); + } } else { - return '\n'; + return (p1 === '}') ? '}' : ''; } }); } @@ -189,7 +192,9 @@ async function buildPatches() { }); } -async function build(target: BuildTarget, version: string, variant: BuildVariant, config: any={}) { +async function build(target: BuildTarget, params: { version: string, variant: BuildVariant, pretty: boolean, meta: boolean }, config: any={}) { + const { version, variant, pretty, meta } = params; + console.log('-- Target:', target); const startTime = performance.now(); @@ -203,6 +208,9 @@ async function build(target: BuildTarget, version: string, variant: BuildVariant } let outputMetaName = outputScriptName; + if (pretty) { + outputScriptName += '.pretty'; + } outputScriptName += '.user.js'; outputMetaName += '.meta.js'; @@ -231,7 +239,7 @@ async function build(target: BuildTarget, version: string, variant: BuildVariant const {path} = output.outputs[0]; // Get generated file - let result = postProcess(await readFile(path, 'utf-8')); + let result = postProcess(await readFile(path, 'utf-8'), pretty); // Replace [[VERSION]] with real value let scriptHeader: string; @@ -246,7 +254,7 @@ async function build(target: BuildTarget, version: string, variant: BuildVariant await Bun.write(path, scriptHeader + result); // Create meta file (don't build if it's beta version) - if (!version.includes('beta') && variant === 'full') { + if (meta && !version.includes('beta') && variant === 'full') { await Bun.write(outDir + '/' + outputMetaName, txtMetaHeader.replace('[[VERSION]]', version)); } @@ -279,6 +287,16 @@ const { values, positionals } = parseArgs({ type: 'string', default: 'full', }, + + pretty: { + type: 'boolean', + default: false, + }, + + meta: { + type: 'boolean', + default: false, + }, }, strict: true, allowPositionals: true, @@ -286,6 +304,8 @@ const { values, positionals } = parseArgs({ values: { version: string, variant: BuildVariant, + pretty: boolean, + meta: boolean, }, positionals: string[], }; @@ -304,7 +324,7 @@ async function main() { const config = {}; console.log(`Building: VERSION=${values['version']}, VARIANT=${values['variant']}`); for (const target of buildTargets) { - await build(target, values['version']!!, values['variant'], config); + await build(target, values, config); } console.log('') diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js new file mode 100644 index 0000000..3af46bc --- /dev/null +++ b/dist/better-xcloud.pretty.user.js @@ -0,0 +1,10259 @@ +// ==UserScript== +// @name Better xCloud +// @namespace https://github.com/redphx +// @version 6.3.0-beta-1 +// @description Improve Xbox Cloud Gaming (xCloud) experience +// @author redphx +// @license MIT +// @match https://www.xbox.com/*/play* +// @match https://www.xbox.com/*/auth/msa?*loggedIn* +// @run-at document-start +// @grant none +// @updateURL https://raw.githubusercontent.com/redphx/better-xcloud/typescript/dist/better-xcloud.meta.js +// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js +// ==/UserScript== +"use strict"; +class BxLogger { + static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args); + static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args); + static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args); + static log(color, tag, ...args) { + console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args); + } +} +window.BxLogger = BxLogger; +/* ADDITIONAL CODE */ +var DEFAULT_FLAGS = { + Debug: !1, + CheckForUpdate: !0, + EnableXcloudLogging: !1, + SafariWorkaround: !0, + ForceNativeMkbTitles: [], + FeatureGates: null, + DeviceInfo: { + deviceType: "unknown" + } +}, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); +try { + delete window.BX_FLAGS; +} catch (e) {} +if (!BX_FLAGS.DeviceInfo.userAgent) BX_FLAGS.DeviceInfo.userAgent = window.navigator.userAgent; +BxLogger.info("BxFlags", BX_FLAGS); +var NATIVE_FETCH = window.fetch; +var ALL_PREFS = { + global: [ + "audio.mic.onPlaying", + "audio.volume.booster.enabled", + "block.features", + "block.tracking", + "gameBar.position", + "game.fortnite.forceConsole", + "loadingScreen.gameArt.show", + "loadingScreen.rocket", + "loadingScreen.waitTime.show", + "mkb.enabled", + "mkb.cursor.hideIdle", + "nativeMkb.forcedGames", + "nativeMkb.mode", + "xhome.enabled", + "xhome.video.resolution", + "screenshot.applyFilters", + "server.bypassRestriction", + "server.ipv6.prefer", + "server.region", + "stream.video.codecProfile", + "stream.video.combineAudio", + "stream.video.maxBitrate", + "stream.locale", + "stream.video.resolution", + "touchController.autoOff", + "touchController.opacity.default", + "touchController.mode", + "touchController.style.custom", + "touchController.style.standard", + "ui.controllerFriendly", + "ui.controllerStatus.show", + "ui.feedbackDialog.disabled", + "ui.gameCard.waitTime.show", + "ui.hideSections", + "ui.systemMenu.hideHandle", + "ui.imageQuality", + "ui.layout", + "ui.reduceAnimations", + "ui.hideScrollbar", + "ui.streamMenu.simplify", + "ui.splashVideo.skip", + "version.current", + "version.lastCheck", + "version.latest", + "bx.locale", + "userAgent.profile" + ], + stream: [ + "audio.volume", + "controller.pollingRate", + "controller.settings", + "deviceVibration.intensity", + "deviceVibration.mode", + "keyboardShortcuts.preset.inGameId", + "localCoOp.enabled", + "mkb.p1.preset.mappingId", + "mkb.p1.slot", + "mkb.p2.preset.mappingId", + "mkb.p2.slot", + "nativeMkb.scroll.sensitivityX", + "nativeMkb.scroll.sensitivityY", + "stats.colors", + "stats.items", + "stats.opacity.all", + "stats.opacity.background", + "stats.position", + "stats.quickGlance.enabled", + "stats.showWhenPlaying", + "stats.textSize", + "video.brightness", + "video.contrast", + "video.maxFps", + "video.player.type", + "video.position", + "video.player.powerPreference", + "video.processing", + "video.ratio", + "video.saturation", + "video.processing.sharpness" + ] +}; +var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0"; +if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { + let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); + if (match) CHROMIUM_VERSION = match[1]; +} +class UserAgent { + static STORAGE_KEY = "BetterXcloud.UserAgent"; + static #config; + static #isMobile = null; + static #isSafari = null; + static #isSafariMobile = null; + static #USER_AGENTS = { + "windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`, + "macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1", + "smarttv-generic": `${window.navigator.userAgent} Smart-TV`, + "smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`, + "vr-oculus": window.navigator.userAgent + " OculusBrowser VR" + }; + static init() { + if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default"; + if (!UserAgent.#config.custom) UserAgent.#config.custom = ""; + UserAgent.spoof(); + } + static updateStorage(profile, custom) { + let config = UserAgent.#config; + if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom; + window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config)); + } + static getDefault() { + return window.navigator.orgUserAgent || window.navigator.userAgent; + } + static get(profile) { + let defaultUserAgent = window.navigator.userAgent; + switch (profile) { + case "default": + return defaultUserAgent; + case "custom": + return UserAgent.#config.custom || defaultUserAgent; + default: + return UserAgent.#USER_AGENTS[profile] || defaultUserAgent; + } + } + static isSafari() { + if (this.#isSafari !== null) return this.#isSafari; + let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom"); + return this.#isSafari = result, result; + } + static isSafariMobile() { + if (this.#isSafariMobile !== null) return this.#isSafariMobile; + let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); + return this.#isSafariMobile = result, result; + } + static isMobile() { + if (this.#isMobile !== null) return this.#isMobile; + let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); + return this.#isMobile = result, result; + } + static spoof() { + let profile = UserAgent.#config.profile; + if (profile === "default") return; + let newUserAgent = UserAgent.get(profile); + if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {}); + window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", { + value: newUserAgent + }); + } +} +var SCRIPT_VERSION = "6.3.0-beta-1", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +UserAgent.init(); +var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { + supportedRegion: !0, + serverRegions: {}, + selectedRegion: {}, + gsToken: "", + isSignedIn: !1, + isPlaying: !1, + browser: { + capabilities: { + touch: browserHasTouchSupport, + batteryApi: "getBattery" in window.navigator, + deviceVibration: !!window.navigator.vibrate, + mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/), + emulatedNativeMkb: !!AppInterface + } + }, + userAgent: { + isTv, + capabilities: { + touch: userAgentHasTouchSupport, + mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/) + } + }, + currentStream: {}, + remotePlay: {}, + pointerServerPort: 9269 +}; +function deepClone(obj) { + if (!obj) return {}; + if ("structuredClone" in window) return structuredClone(obj); + return JSON.parse(JSON.stringify(obj)); +} +var BxEvent; +((BxEvent) => { + BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; + function dispatch(target, eventName, data) { + if (!target) return; + if (!eventName) { + alert("BxEvent.dispatch(): eventName is null"); + return; + } + let event = new Event(eventName); + if (data) for (let key in data) + event[key] = data[key]; + target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data); + } + BxEvent.dispatch = dispatch; +})(BxEvent ||= {}); +window.BxEvent = BxEvent; +var GamepadKeyName = { + 0: ["A", "⇓"], + 1: ["B", "⇒"], + 2: ["X", "⇐"], + 3: ["Y", "⇑"], + 4: ["LB", "↘"], + 5: ["RB", "↙"], + 6: ["LT", "↖"], + 7: ["RT", "↗"], + 8: ["Select", "⇺"], + 9: ["Start", "⇻"], + 16: ["Home", ""], + 12: ["D-Pad Up", "≻"], + 13: ["D-Pad Down", "≽"], + 14: ["D-Pad Left", "≺"], + 15: ["D-Pad Right", "≼"], + 10: ["L3", "↺"], + 100: ["Left Stick Up", "↾"], + 101: ["Left Stick Down", "⇂"], + 102: ["Left Stick Left", "↼"], + 103: ["Left Stick Right", "⇀"], + 104: ["Left Stick", "⇱"], + 11: ["R3", "↻"], + 200: ["Right Stick Up", "↿"], + 201: ["Right Stick Down", "⇃"], + 202: ["Right Stick Left", "↽"], + 203: ["Right Stick Right", "⇁"], + 204: ["Right Stick", "⇲"], + 17: ["Screenshot", "⇧"] +}; +class BxEventBus { + listeners = new Map; + group; + appJsInterfaces; + static Script = new BxEventBus("script", { + "dialog.shown": "onDialogShown", + "dialog.dismissed": "onDialogDismissed" + }); + static Stream = new BxEventBus("stream", { + "state.loading": "onStreamPlaying", + "state.playing": "onStreamPlaying", + "state.stopped": "onStreamStopped" + }); + constructor(group, appJsInterfaces) { + this.group = group, this.appJsInterfaces = appJsInterfaces; + } + on(event, callback) { + if (!this.listeners.has(event)) this.listeners.set(event, new Set); + this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback); + } + once(event, callback) { + let wrapper = (...args) => { + callback(...args), this.off(event, wrapper); + }; + this.on(event, wrapper); + } + off(event, callback) { + if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) { + this.listeners.delete(event); + return; + } + let callbacks = this.listeners.get(event); + if (!callbacks) return; + if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event); + } + offAll() { + this.listeners.clear(); + } + emit(event, payload) { + let callbacks = this.listeners.get(event) || []; + for (let callback of callbacks) + callback(payload); + if (AppInterface) try { + if (event in this.appJsInterfaces) { + let method = this.appJsInterfaces[event]; + AppInterface[method] && AppInterface[method](); + } else AppInterface.onEventBus(this.group + "." + event); + } catch (e) { + console.log(e); + } + BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload); + } +} +window.BxEventBus = BxEventBus; +class GhPagesUtils { + static fetchLatestCommit() { + NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", { + method: "GET", + headers: { + Accept: "application/vnd.github.v3+json" + } + }).then((response) => response.json()).then((data) => { + let latestCommitHash = data.commit.sha; + window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash); + }).catch((error) => { + BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error); + }); + } + static getUrl(path) { + if (path[0] === "/") alert('`path` must not starts with "/"'); + let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash"); + if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; + else return `${prefix}/refs/heads/gh-pages/${path}`; + } + static getNativeMkbCustomList(update = !1) { + let key = "BetterXcloud.GhPages.ForceNativeMkb"; + update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { + if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", { + data: json + }); + else window.localStorage.removeItem(key); + }); + let info = JSON.parse(window.localStorage.getItem(key) || "{}"); + if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; + return info.data; + } + static getTouchControlCustomList() { + let key = "BetterXcloud.GhPages.CustomTouchLayouts"; + return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => { + if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json)); + }), JSON.parse(window.localStorage.getItem(key) || "[]"); + } + static getLocalCoOpList() { + let key = "BetterXcloud.GhPages.LocalCoOp"; + NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => { + if (json.$schemaVersion === 1) { + window.localStorage.setItem(key, JSON.stringify(json)); + let ids = new Set(Object.keys(json.data)); + BxEventBus.Script.emit("list.localCoOp.updated", { ids }); + } else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set }); + }); + let info = JSON.parse(window.localStorage.getItem(key) || "{}"); + if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set; + return new Set(Object.keys(info.data || {})); + } +} +var SUPPORTED_LANGUAGES = { + "en-US": "English (US)", + "ca-CA": "Català", + "da-DK": "dansk", + "de-DE": "Deutsch", + "en-ID": "Bahasa Indonesia", + "es-ES": "español (España)", + "fr-FR": "français", + "it-IT": "italiano", + "ja-JP": "日本語", + "ko-KR": "한국어", + "pl-PL": "polski", + "pt-BR": "português (Brasil)", + "ru-RU": "русский", + "th-TH": "ภาษาไทย", + "tr-TR": "Türkçe", + "uk-UA": "українська", + "vi-VN": "Tiếng Việt", + "zh-CN": "中文(简体)", + "zh-TW": "中文(繁體)" +}, Texts = { + achievements: "Achievements", + activate: "Activate", + activated: "Activated", + active: "Active", + advanced: "Advanced", + "all-games": "All games", + "always-off": "Always off", + "always-on": "Always on", + "amd-fidelity-cas": "AMD FidelityFX CAS", + "app-settings": "App settings", + apply: "Apply", + "aspect-ratio": "Aspect ratio", + "aspect-ratio-note": "Don't use with native touch games", + audio: "Audio", + auto: "Auto", + availability: "Availability", + "back-to-home": "Back to home", + "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", + "background-opacity": "Background opacity", + battery: "Battery", + "battery-saving": "Battery saving", + "better-xcloud": "Better xCloud", + "bitrate-audio-maximum": "Maximum audio bitrate", + "bitrate-video-maximum": "Maximum video bitrate", + bottom: "Bottom", + "bottom-half": "Bottom half", + "bottom-left": "Bottom-left", + "bottom-right": "Bottom-right", + brazil: "Brazil", + brightness: "Brightness", + "browser-unsupported-feature": "Your browser doesn't support this feature", + "button-xbox": "Xbox button", + "bypass-region-restriction": "Bypass region restriction", + "can-stream-xbox-360-games": "Can stream Xbox 360 games", + cancel: "Cancel", + "cant-stream-xbox-360-games": "Can't stream Xbox 360 games", + center: "Center", + chat: "Chat", + "clarity-boost": "Clarity boost", + "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", + clear: "Clear", + "clear-data": "Clear data", + "clear-data-confirm": "Do you want to clear all Better xCloud settings and data?", + "clear-data-success": "Data cleared! Refresh the page to apply the changes.", + clock: "Clock", + close: "Close", + "close-app": "Close app", + "combine-audio-video-streams": "Combine audio & video streams", + "combine-audio-video-streams-summary": "May fix the laggy audio problem", + "conditional-formatting": "Conditional formatting text color", + "confirm-delete-preset": "Do you want to delete this preset?", + "confirm-reload-stream": "Do you want to refresh the stream?", + connected: "Connected", + "console-connect": "Connect", + "continent-asia": "Asia", + "continent-australia": "Australia", + "continent-europe": "Europe", + "continent-north-america": "North America", + "continent-south-america": "South America", + contrast: "Contrast", + controller: "Controller", + "controller-customization": "Controller customization", + "controller-customization-input-latency-note": "May slightly increase input latency", + "controller-friendly-ui": "Controller-friendly UI", + "controller-shortcuts": "Controller shortcuts", + "controller-shortcuts-connect-note": "Connect a controller to use this feature", + "controller-shortcuts-xbox-note": "Button to open the Guide menu", + "controller-vibration": "Controller vibration", + copy: "Copy", + "create-shortcut": "Shortcut", + custom: "Custom", + "deadzone-counterweight": "Deadzone counterweight", + decrease: "Decrease", + default: "Default", + "default-opacity": "Default opacity", + "default-preset-note": "You can't modify default presets. Create a new one to customize it.", + delete: "Delete", + "detect-controller-button": "Detect controller button", + device: "Device", + "device-unsupported-touch": "Your device doesn't have touch support", + "device-vibration": "Device vibration", + "device-vibration-not-using-gamepad": "On when not using gamepad", + disable: "Disable", + "disable-features": "Disable features", + "disable-home-context-menu": "Disable context menu in Home page", + "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", + "disable-social-features": "Disable social features", + "disable-xcloud-analytics": "Disable xCloud analytics", + disabled: "Disabled", + disconnected: "Disconnected", + download: "Download", + downloaded: "Downloaded", + edit: "Edit", + "enable-controller-shortcuts": "Enable controller shortcuts", + "enable-local-co-op-support": "Enable local co-op support", + "enable-local-co-op-support-note": "Only works with some games", + "enable-mic-on-startup": "Enable microphone on game launch", + "enable-mkb": "Emulate controller with Mouse & Keyboard", + "enable-quick-glance-mode": 'Enable "Quick Glance" mode', + "enable-remote-play-feature": 'Enable the "Remote Play" feature', + "enable-volume-control": "Enable volume control feature", + enabled: "Enabled", + experimental: "Experimental", + export: "Export", + fast: "Fast", + "force-native-mkb-games": "Force native Mouse & Keyboard for these games", + "fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile', + "fortnite-force-console-version": "Fortnite: force console version", + "friends-followers": "Friends and followers", + "game-bar": "Game Bar", + "getting-consoles-list": "Getting the list of consoles...", + guide: "Guide", + help: "Help", + hide: "Hide", + "hide-idle-cursor": "Hide mouse cursor on idle", + "hide-scrollbar": "Hide web page's scrollbar", + "hide-sections": "Hide sections", + "hide-system-menu-icon": "Hide System menu's icon", + "hide-touch-controller": "Hide touch controller", + "high-performance": "High performance", + "highest-quality": "Highest quality", + "highest-quality-note": "Your device may not be powerful enough to use these settings", + "horizontal-scroll-sensitivity": "Horizontal scroll sensitivity", + "horizontal-sensitivity": "Horizontal sensitivity", + "how-to-fix": "How to fix", + "how-to-improve-app-performance": "How to improve app's performance", + ignore: "Ignore", + "image-quality": "Website's image quality", + import: "Import", + "in-game-controller-customization": "In-game controller customization", + "in-game-controller-shortcuts": "In-game controller shortcuts", + "in-game-keyboard-shortcuts": "In-game keyboard shortcuts", + "in-game-shortcuts": "In-game shortcuts", + increase: "Increase", + "install-android": "Better xCloud app for Android", + invites: "Invites", + japan: "Japan", + jitter: "Jitter", + "keyboard-key": "Keyboard key", + "keyboard-shortcuts": "Keyboard shortcuts", + korea: "Korea", + language: "Language", + large: "Large", + layout: "Layout", + "left-stick": "Left stick", + "left-stick-deadzone": "Left stick deadzone", + "left-trigger-range": "Left trigger range", + "limit-fps": "Limit FPS", + "load-failed-message": "Failed to run Better xCloud", + "loading-screen": "Loading screen", + "local-co-op": "Local co-op", + "lowest-quality": "Lowest quality", + manage: "Manage", + "map-mouse-to": "Map mouse to", + "may-not-work-properly": "May not work properly!", + menu: "Menu", + microphone: "Microphone", + "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", + "mkb-click-to-activate": "Click to activate", + "mkb-disclaimer": "This could be viewed as cheating when playing online", + "modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.", + "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-click": "Mouse click", + "mouse-wheel": "Mouse wheel", + muted: "Muted", + name: "Name", + "native-mkb": "Native Mouse & Keyboard", + new: "New", + "new-version-available": [ + e => `Version ${e.version} available`, + e => `Versió ${e.version} disponible`, + , + e => `Version ${e.version} verfügbar`, + e => `Versi ${e.version} tersedia`, + e => `Versión ${e.version} disponible`, + e => `Version ${e.version} disponible`, + e => `Disponibile la versione ${e.version}`, + e => `Ver ${e.version} が利用可能です`, + e => `${e.version} 버전 사용가능`, + e => `Dostępna jest nowa wersja ${e.version}`, + e => `Versão ${e.version} disponível`, + e => `Версия ${e.version} доступна`, + e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`, + e => `${e.version} sayılı yeni sürüm mevcut`, + e => `Доступна версія ${e.version}`, + e => `Đã có phiên bản ${e.version}`, + e => `版本 ${e.version} 可供更新`, + e => `已可更新為 ${e.version} 版` + ], + "no-consoles-found": "No consoles found", + "no-controllers-connected": "No controllers connected", + normal: "Normal", + notifications: "Notifications", + off: "Off", + official: "Official", + on: "On", + "only-supports-some-games": "Only supports some games", + opacity: "Opacity", + other: "Other", + playing: "Playing", + playtime: "Playtime", + poland: "Poland", + "polling-rate": "Polling rate", + position: "Position", + "powered-off": "Powered off", + "powered-on": "Powered on", + "prefer-ipv6-server": "Prefer IPv6 server", + "preferred-game-language": "Preferred game's language", + preset: "Preset", + press: "Press", + "press-any-button": "Press any button...", + "press-esc-to-cancel": "Press Esc to cancel", + "press-key-to-toggle-mkb": [ + e => `Press ${e.key} to toggle this feature`, + e => `Premeu ${e.key} per alternar aquesta funció`, + e => `Tryk på ${e.key} for at slå denne funktion til`, + e => `${e.key}: Funktion an-/ausschalten`, + e => `Tekan ${e.key} untuk mengaktifkan fitur ini`, + e => `Pulsa ${e.key} para alternar esta función`, + e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`, + e => `Premi ${e.key} per attivare questa funzionalità`, + e => `${e.key} でこの機能を切替`, + e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`, + e => `Naciśnij ${e.key} aby przełączyć tę funkcję`, + e => `Pressione ${e.key} para alternar este recurso`, + e => `Нажмите ${e.key} для переключения этой функции`, + e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`, + e => `Etkinleştirmek için ${e.key} tuşuna basın`, + e => `Натисніть ${e.key} щоб перемкнути цю функцію`, + e => `Nhấn ${e.key} để bật/tắt tính năng này`, + e => `按下 ${e.key} 来切换此功能`, + e => `按下 ${e.key} 來啟用此功能` + ], + "press-to-bind": "Press a key or do a mouse click to bind...", + "prompt-preset-name": "Preset's name:", + recommended: "Recommended", + "recommended-settings-for-device": [ + e => `Recommended settings for ${e.device}`, + e => `Configuració recomanada per a ${e.device}`, + , + e => `Empfohlene Einstellungen für ${e.device}`, + e => `Rekomendasi pengaturan untuk ${e.device}`, + e => `Ajustes recomendados para ${e.device}`, + e => `Paramètres recommandés pour ${e.device}`, + e => `Configurazioni consigliate per ${e.device}`, + e => `${e.device} の推奨設定`, + e => `다음 기기에서 권장되는 설정: ${e.device}`, + e => `Zalecane ustawienia dla ${e.device}`, + e => `Configurações recomendadas para ${e.device}`, + e => `Рекомендуемые настройки для ${e.device}`, + e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`, + e => `${e.device} için önerilen ayarlar`, + e => `Рекомендовані налаштування для ${e.device}`, + e => `Cấu hình được đề xuất cho ${e.device}`, + e => `${e.device} 的推荐设置`, + e => `${e.device} 推薦的設定` + ], + "reduce-animations": "Reduce UI animations", + region: "Region", + "reload-page": "Reload page", + "remote-play": "Remote Play", + rename: "Rename", + renderer: "Renderer", + "renderer-configuration": "Renderer configuration", + "reset-highlighted-setting": "Reset highlighted setting", + "right-click-to-unbind": "Right-click on a key to unbind it", + "right-stick": "Right stick", + "right-stick-deadzone": "Right stick deadzone", + "right-trigger-range": "Right trigger range", + "rocket-always-hide": "Always hide", + "rocket-always-show": "Always show", + "rocket-animation": "Rocket animation", + "rocket-hide-queue": "Hide when queuing", + saturation: "Saturation", + save: "Save", + screen: "Screen", + "screenshot-apply-filters": "Apply video filters to screenshots", + "section-all-games": "All games", + "section-most-popular": "Most popular", + "section-native-mkb": "Play with mouse & keyboard", + "section-news": "News", + "section-play-with-friends": "Play with friends", + "section-touch": "Play with touch", + "separate-touch-controller": "Separate Touch controller & Controller #1", + "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", + server: "Server", + "server-locations": "Server locations", + settings: "Settings", + "settings-for": "Settings for", + "settings-reload": "Reload page to reflect changes", + "settings-reload-note": "Settings in this tab only go into effect on the next page load", + "settings-reloading": "Reloading...", + sharpness: "Sharpness", + "shortcut-keys": "Shortcut keys", + show: "Show", + "show-controller-connection-status": "Show controller connection status", + "show-game-art": "Show game art", + "show-hide": "Show/hide", + "show-stats-on-startup": "Show stats when starting the game", + "show-touch-controller": "Show touch controller", + "show-wait-time": "Show the estimated wait time", + "show-wait-time-in-game-card": "Show wait time in game card", + "simplify-stream-menu": "Simplify Stream's menu", + "skip-splash-video": "Skip Xbox splash video", + slow: "Slow", + small: "Small", + "smart-tv": "Smart TV", + sound: "Sound", + standard: "Standard", + standby: "Standby", + "stat-bitrate": "Bitrate", + "stat-decode-time": "Decode time", + "stat-fps": "FPS", + "stat-frames-lost": "Frames lost", + "stat-packets-lost": "Packets lost", + "stat-ping": "Ping", + stats: "Stats", + "stick-decay-minimum": "Stick decay minimum", + "stick-decay-strength": "Stick decay strength", + stream: "Stream", + "stream-settings": "Stream settings", + "stream-stats": "Stream stats", + "stream-your-own-game": "Stream your own game", + stretch: "Stretch", + "suggest-settings": "Suggest settings", + "suggest-settings-link": "Suggest recommended settings for this device", + "support-better-xcloud": "Support Better xCloud", + "swap-buttons": "Swap buttons", + "take-screenshot": "Take screenshot", + "target-resolution": "Target resolution", + "tc-all-white": "All white", + "tc-auto-off": "Off when controller found", + "tc-custom-layout-style": "Custom layout's button style", + "tc-muted-colors": "Muted colors", + "tc-standard-layout-style": "Standard layout's button style", + "text-size": "Text size", + toggle: "Toggle", + top: "Top", + "top-center": "Top-center", + "top-half": "Top half", + "top-left": "Top-left", + "top-right": "Top-right", + "touch-control-layout": "Touch control layout", + "touch-control-layout-by": [ + e => `Touch control layout by ${e.name}`, + e => `Format del control tàctil per ${e.name}`, + e => `Touch-kontrol layout af ${e.name}`, + e => `Touch-Steuerungslayout von ${e.name}`, + e => `Tata letak Sentuhan layar oleh ${e.name}`, + e => `Disposición del control táctil por ${e.nombre}`, + e => `Disposition du contrôleur tactile par ${e.name}`, + e => `Configurazione dei comandi su schermo creata da ${e.name}`, + e => `タッチ操作レイアウト作成者: ${e.name}`, + e => `${e.name} 제작, 터치 컨트롤 레이아웃`, + e => `Układ sterowania dotykowego stworzony przez ${e.name}`, + e => `Disposição de controle por toque feito por ${e.name}`, + e => `Сенсорная раскладка по ${e.name}`, + e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`, + e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, + e => `Розташування сенсорного керування від ${e.name}`, + e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`, + e => `由 ${e.name} 提供的虚拟按键样式`, + e => `觸控遊玩佈局由 ${e.name} 提供` + ], + "touch-controller": "Touch controller", + "true-achievements": "TrueAchievements", + ui: "UI", + "unexpected-behavior": "May cause unexpected behavior", + "united-states": "United States", + unknown: "Unknown", + unlimited: "Unlimited", + unmuted: "Unmuted", + unofficial: "Unofficial", + "unofficial-game-list": "Unofficial game list", + "unsharp-masking": "Unsharp masking", + upload: "Upload", + uploaded: "Uploaded", + "use-mouse-absolute-position": "Use mouse's absolute position", + "use-this-at-your-own-risk": "Use this at your own risk", + "user-agent-profile": "User-Agent profile", + "vertical-scroll-sensitivity": "Vertical scroll sensitivity", + "vertical-sensitivity": "Vertical sensitivity", + "vibration-intensity": "Vibration intensity", + "vibration-status": "Vibration", + video: "Video", + "virtual-controller": "Virtual controller", + "virtual-controller-slot": "Virtual controller slot", + "visual-quality": "Visual quality", + "visual-quality-high": "High", + "visual-quality-low": "Low", + "visual-quality-normal": "Normal", + volume: "Volume", + "wait-time-countdown": "Countdown", + "wait-time-estimated": "Estimated finish time", + "waiting-for-input": "Waiting for input...", + wallpaper: "Wallpaper", + webgl2: "WebGL2" +}; +class Translations { + static EN_US = "en-US"; + static KEY_LOCALE = "BetterXcloud.Locale"; + static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations"; + static selectedLocaleIndex = -1; + static selectedLocale = "en-US"; + static supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static foreignTranslations = {}; + static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US); + static async init() { + Translations.refreshLocale(), await Translations.loadTranslations(); + } + static refreshLocale(newLocale) { + let locale; + if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale; + else locale = localStorage.getItem(Translations.KEY_LOCALE); + let supportedLocales = Translations.supportedLocales; + if (!locale) { + if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US; + localStorage.setItem(Translations.KEY_LOCALE, locale); + } + Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale); + } + static get(key, values) { + let text = null; + if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key]; + if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`); + let translation; + if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values); + return translation = text, translation; + } + static async loadTranslations() { + if (Translations.selectedLocale === Translations.EN_US) return; + try { + Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)); + } catch (e) {} + if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale); + } + static async updateTranslations(async = !1) { + if (Translations.selectedLocale === Translations.EN_US) { + localStorage.removeItem(Translations.KEY_TRANSLATIONS); + return; + } + if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale); + else await Translations.downloadTranslations(Translations.selectedLocale); + } + static async downloadTranslations(locale) { + try { + let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json(); + if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; + return !0; + } catch (e) { + debugger; + } + return !1; + } + static downloadTranslationsAsync(locale) { + NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => { + window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; + }); + } + static switchLocale(locale) { + localStorage.setItem(Translations.KEY_LOCALE, locale); + } +} +var t = Translations.get; +Translations.init(); +class NavigationUtils { + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } +} +var setNearby = NavigationUtils.setNearby; +var ButtonStyleClass = { + 1: "bx-primary", + 2: "bx-warning", + 4: "bx-danger", + 8: "bx-ghost", + 16: "bx-frosted", + 32: "bx-drop-shadow", + 64: "bx-focusable", + 128: "bx-full-width", + 256: "bx-full-height", + 512: "bx-auto-height", + 1024: "bx-tall", + 2048: "bx-circular", + 4096: "bx-normal-case", + 8192: "bx-normal-link" +}; +function createElement(elmName, props, ..._) { + let $elm, hasNs = props && "xmlns" in props; + if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; + else $elm = document.createElement(elmName); + if (props) { + if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; + if (props._on) { + for (let name in props._on) + $elm.addEventListener(name, props._on[name]); + delete props._on; + } + if (props._dataset) { + for (let name in props._dataset) + $elm.dataset[name] = props._dataset[name]; + delete props._dataset; + } + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + let value = props[key]; + if (hasNs) $elm.setAttributeNS(null, key, value); + else $elm.setAttribute(key, value); + } + } + for (let i = 2, size = arguments.length;i < size; i++) { + let arg = arguments[i]; + if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg); + } + return $elm; +} +var domParser = new DOMParser; +function createSvgIcon(icon) { + return domParser.parseFromString(icon, "image/svg+xml").documentElement; +} +var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); +function createButton(options) { + let $btn; + if (options.url) $btn = CE("a", { + class: "bx-button", + href: options.url, + target: "_blank" + }); + else $btn = CE("button", { + class: "bx-button", + type: "button" + }), options.disabled && ($btn.disabled = !0); + let style = options.style || 0; + if (style) { + let index; + for (index of ButtonStyleIndices) + style & index && $btn.classList.add(ButtonStyleClass[index]); + } + if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText)); + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; +} +function createSettingRow(label, $control, options = {}) { + let $label, $row = CE("label", { + class: "bx-settings-row" + }, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control); + if (options.pref) $row.prefKey = options.pref; + if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu); + let $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + if (setNearby($row, { + orientation: options.multiLines ? "vertical" : "horizontal" + }), options.multiLines) + $row.dataset.multiLines = "true"; + if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id; + return $row; +} +function getReactProps($elm) { + for (let key in $elm) + if (key.startsWith("__reactProps")) return $elm[key]; + return null; +} +function escapeHtml(html) { + let text = document.createTextNode(html), $span = document.createElement("span"); + return $span.appendChild(text), $span.innerHTML; +} +function isElementVisible($elm) { + let rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; +} +function removeChildElements($parent) { + if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select"); + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); +} +function clearDataSet($elm) { + Object.keys($elm.dataset).forEach((key) => { + delete $elm.dataset[key]; + }); +} +function renderPresetsList($select, allPresets, selectedValue, options = {}) { + if (removeChildElements($select), options.addOffValue) { + let $option = CE("option", { value: 0 }, t("off")); + $option.selected = selectedValue === 0, $select.appendChild($option); + } + let groups = { + default: t("default") + " 🔒", + custom: t("custom") + }, key; + for (key in groups) { + let $optGroup = CE("optgroup", { label: groups[key] }); + for (let id of allPresets[key]) { + let record = allPresets.data[id], selected = selectedValue === record.id, name = options.selectedIndicator && selected ? "✅ " + record.name : record.name, $option = CE("option", { value: record.id }, name); + if (selected) $option.selected = !0; + $optGroup.appendChild($option); + } + if ($optGroup.hasChildNodes()) $select.appendChild($optGroup); + } +} +function calculateSelectBoxes($root) { + let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select")); + for (let $select of selects) { + let $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + continue; + } + let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); + if (!width) continue; + if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) { + if ($select.multiple) width += 20; + if ($select.querySelector("optgroup")) width -= 15; + } else width += 10; + $select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + } +} +var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; +function humanFileSize(size) { + let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i]; +} +function secondsToHm(seconds) { + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + let output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); +} +function escapeCssSelector(name) { + return name.replaceAll(".", "-"); +} +var CE = createElement; +window.BX_CE = createElement; +class Toast { + static instance; + static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); + LOG_TAG = "Toast"; + $wrapper; + $msg; + $status; + stack = []; + isShowing = !1; + timeoutId; + DURATION = 3000; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { + let classList = this.$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); + }), document.documentElement.appendChild(this.$wrapper); + } + show(msg, status, options = {}) { + options = options || {}; + let args = Array.from(arguments); + if (options.instant) this.stack = [args], this.showNext(); + else this.stack.push(args), !this.isShowing && this.showNext(); + } + showNext() { + if (!this.stack.length) { + this.isShowing = !1; + return; + } + this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION); + let [msg, status, options] = this.stack.shift(); + if (options && options.html) this.$msg.innerHTML = msg; + else this.$msg.textContent = msg; + if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; + else this.$status.classList.add("bx-gone"); + let classList = this.$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + hide = () => { + this.timeoutId = null; + let classList = this.$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + }; + static show(msg, status, options = {}) { + Toast.getInstance().show(msg, status, options); + } + static showNext() { + Toast.getInstance().showNext(); + } +} +class MicrophoneShortcut { + static toggle(showToast = !0) { + if (!window.BX_EXPOSED.streamSession) return !1; + let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; + try { + return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; + } catch (e) { + console.log(e); + } + return !1; + } +} +class LocalDb { + static instance; + static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 4; + static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; + static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; + static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations"; + static TABLE_CONTROLLER_SETTINGS = "controller_settings"; + static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; + db; + open() { + return new Promise((resolve, reject) => { + if (this.db) { + resolve(this.db); + return; + } + let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + let db = e.target.result; + if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); + if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { + keyPath: "id" + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, { + keyPath: "id", + autoIncrement: !0 + }); + if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { + keyPath: "id", + autoIncrement: !0 + }); + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.db = e.target.result, resolve(this.db); + }; + }); + } +} +var BypassServers = { + br: t("brazil"), + jp: t("japan"), + kr: t("korea"), + pl: t("poland"), + us: t("united-states") +}, BypassServerIps = { + br: "169.150.198.66", + kr: "121.125.60.151", + jp: "138.199.21.239", + pl: "45.134.212.66", + us: "143.244.47.65" +}; +class BaseSettingsStorage { + storage; + storageKey; + _settings; + definitions; + constructor(storageKey, definitions) { + this.storage = window.localStorage, this.storageKey = storageKey; + for (let [_, setting] of Object.entries(definitions)) { + if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; + setting.ready && setting.ready.call(this, setting); + } + this.definitions = definitions, this._settings = null; + } + get settings() { + if (this._settings) return this._settings; + let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + for (let key in settings) { + if (!this.definitions.hasOwnProperty(key)) { + delete settings[key]; + continue; + } + settings[key] = this.validateValue("get", key, settings[key]); + } + return this._settings = settings, settings; + } + getDefinition(key) { + if (!this.definitions[key]) return alert("Request invalid definition: " + key), {}; + return this.definitions[key]; + } + hasSetting(key) { + return key in this.settings; + } + getSetting(key, checkUnsupported = !0) { + let definition = this.definitions[key]; + if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; + if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue; + else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null); + return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key]; + } + setSetting(key, value, origin) { + if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", { + storageKey: this.storageKey, + settingKey: key + }); + else BxEventBus.Script.emit("setting.changed", { + storageKey: this.storageKey, + settingKey: key + }); + return value; + } + saveSettings() { + this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); + } + validateValue(action, key, value) { + let def = this.definitions[key]; + if (!def) return value; + if (typeof value === "undefined" || value === null) value = def.default; + if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value); + if ("min" in def) value = Math.max(def.min, value); + if ("max" in def) value = Math.min(def.max, value); + if ("options" in def) { + if (!(value in def.options)) value = def.default; + } else if ("multipleOptions" in def) { + if (value.length) { + let validOptions = Object.keys(def.multipleOptions); + value.forEach((item2, idx) => { + validOptions.indexOf(item2) === -1 && value.splice(idx, 1); + }); + } + if (!value.length) value = def.default; + } + if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value); + return value; + } + getLabel(key) { + return this.definitions[key].label || key; + } + getValueText(key, value) { + let definition = this.definitions[key]; + if ("min" in definition) { + let params = definition.params; + if (params.customTextValue) { + if (definition.transformValue) value = definition.transformValue.get.call(definition, value); + let text = params.customTextValue(value, definition.min, definition.max); + if (text) return text; + } + return value.toString(); + } else if ("options" in definition) { + let options = definition.options; + if (value in options) return options[value]; + } else if (typeof value === "boolean") return value ? t("on") : t("off"); + return value.toString(); + } +} +var BxIcon = { + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + LOCAL_CO_OP: "", + NATIVE_MKB: "", + NEW: "", + MANAGE: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" +}; +function getSupportedCodecProfiles() { + let options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + let fmtp = codec.sdpFmtpLine.toLowerCase(); + if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; + else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; + else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; + } + if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`; + else options["low"] = t("visual-quality-low"); + if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`; + else options["normal"] = t("visual-quality-normal"); + if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`; + else options["high"] = t("visual-quality-high"); + return options; +} +class GlobalSettingsStorage extends BaseSettingsStorage { + static DEFINITIONS = { + "version.lastCheck": { + default: 0 + }, + "version.latest": { + default: "" + }, + "version.current": { + default: "" + }, + "bx.locale": { + label: t("language"), + default: localStorage.getItem("BetterXcloud.Locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + "server.region": { + label: t("region"), + note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), + default: "default" + }, + "server.bypassRestriction": { + label: t("bypass-region-restriction"), + note: "⚠️ " + t("use-this-at-your-own-risk"), + default: "off", + optionsGroup: t("region"), + options: Object.assign({ + off: t("off") + }, BypassServers) + }, + "stream.locale": { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "bg-BG": "Български", + "cs-CZ": "čeština", + "da-DK": "dansk", + "de-DE": "Deutsch", + "el-GR": "Ελληνικά", + "en-GB": "English (UK)", + "en-US": "English (US)", + "es-ES": "español (España)", + "es-MX": "español (Latinoamérica)", + "fi-FI": "suomi", + "fr-FR": "français", + "he-IL": "עברית", + "hu-HU": "magyar", + "it-IT": "italiano", + "ja-JP": "日本語", + "ko-KR": "한국어", + "nb-NO": "norsk bokmål", + "nl-NL": "Nederlands", + "pl-PL": "polski", + "pt-BR": "português (Brasil)", + "pt-PT": "português (Portugal)", + "ro-RO": "Română", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "th-TH": "ไทย", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + "stream.video.resolution": { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p", + "1080p-hq": "1080p (HQ)" + }, + suggest: { + lowest: "720p", + highest: "1080p-hq" + } + }, + "stream.video.codecProfile": { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + let options = setting.options, keys = Object.keys(options); + if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); + setting.suggest = { + lowest: keys.length === 1 ? keys[0] : keys[1], + highest: keys[keys.length - 1] + }; + } + }, + "server.ipv6.prefer": { + label: t("prefer-ipv6-server"), + default: !1 + }, + "screenshot.applyFilters": { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + "ui.splashVideo.skip": { + label: t("skip-splash-video"), + default: !1 + }, + "ui.systemMenu.hideHandle": { + label: "⣿ " + t("hide-system-menu-icon"), + default: !1 + }, + "ui.imageQuality": { + requiredVariants: "full", + label: t("image-quality"), + default: 90, + min: 10, + max: 90, + params: { + steps: 10, + exactTicks: 20, + hideSlider: !0, + customTextValue(value, min, max) { + if (value === 90) return t("default"); + return value + "%"; + } + } + }, + "stream.video.combineAudio": { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + "touchController.mode": { + requiredVariants: "full", + label: t("availability"), + default: "all", + options: { + default: t("default"), + off: t("off"), + all: t("all-games") + }, + unsupported: !STATES.userAgent.capabilities.touch, + unsupportedValue: "default" + }, + "touchController.autoOff": { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.opacity.default": { + requiredVariants: "full", + label: t("default-opacity"), + default: 100, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.style.standard": { + requiredVariants: "full", + label: t("tc-standard-layout-style"), + default: "default", + options: { + default: t("default"), + white: t("tc-all-white"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "touchController.style.custom": { + requiredVariants: "full", + label: t("tc-custom-layout-style"), + default: "default", + options: { + default: t("default"), + muted: t("tc-muted-colors") + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + "ui.streamMenu.simplify": { + label: t("simplify-stream-menu"), + default: !1 + }, + "mkb.cursor.hideIdle": { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + "ui.feedbackDialog.disabled": { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + "stream.video.maxBitrate": { + requiredVariants: "full", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 102400, + max: 15360000, + transformValue: { + get(value) { + return value === 0 ? this.max : value; + }, + set(value) { + return value === this.max ? 0 : value; + } + }, + params: { + steps: 102400, + exactTicks: 5120000, + customTextValue: (value, min, max) => { + if (value = parseInt(value), value === max) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; + } + }, + suggest: { + highest: 0 + } + }, + "gameBar.position": { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + off: t("off"), + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right") + } + }, + "ui.controllerStatus.show": { + label: t("show-controller-connection-status"), + default: !0 + }, + "mkb.enabled": { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb, + ready: (setting) => { + let note, url; + if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; + else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; + setting.unsupportedNote = () => CE("a", { + href: url, + target: "_blank" + }, "⚠️ " + note); + } + }, + "nativeMkb.mode": { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + off: t("off"), + on: t("on") + }, + ready: (setting) => { + if (STATES.browser.capabilities.emulatedNativeMkb) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"]; + else delete setting.options["on"]; + } + }, + "nativeMkb.forcedGames": { + label: t("force-native-mkb-games"), + default: [], + unsupported: !AppInterface && UserAgent.isMobile(), + ready: (setting) => { + if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => { + setting.multipleOptions = payload.data.data; + }); + }, + params: { + size: 6 + } + }, + "ui.reduceAnimations": { + label: t("reduce-animations"), + default: !1 + }, + "loadingScreen.gameArt.show": { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + "loadingScreen.waitTime.show": { + label: t("show-wait-time"), + default: !0 + }, + "loadingScreen.rocket": { + label: t("rocket-animation"), + default: "show", + options: { + show: t("rocket-always-show"), + "hide-queue": t("rocket-hide-queue"), + hide: t("rocket-always-hide") + } + }, + "ui.controllerFriendly": { + label: t("controller-friendly-ui"), + default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" + }, + "ui.layout": { + requiredVariants: "full", + label: t("layout"), + default: "default", + options: { + default: t("default"), + normal: t("normal"), + tv: t("smart-tv") + } + }, + "ui.hideScrollbar": { + label: t("hide-scrollbar"), + default: !1 + }, + "ui.hideSections": { + requiredVariants: "full", + label: t("hide-sections"), + default: [], + multipleOptions: { + news: t("section-news"), + friends: t("section-play-with-friends"), + "native-mkb": t("section-native-mkb"), + touch: t("section-touch"), + "most-popular": t("section-most-popular"), + "all-games": t("section-all-games") + }, + params: { + size: 0 + } + }, + "ui.gameCard.waitTime.show": { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !0 + }, + "block.tracking": { + label: t("disable-xcloud-analytics"), + default: !1 + }, + "block.features": { + requiredVariants: "full", + label: t("disable-features"), + default: [], + multipleOptions: { + chat: t("chat"), + friends: t("friends-followers"), + byog: t("stream-your-own-game"), + "notifications-invites": t("notifications") + ": " + t("invites"), + "notifications-achievements": t("notifications") + ": " + t("achievements") + } + }, + "userAgent.profile": { + label: t("user-agent-profile"), + note: "⚠️ " + t("unexpected-behavior"), + default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", + options: { + default: t("default"), + "windows-edge": "Edge + Windows", + "macos-safari": "Safari + macOS", + "vr-oculus": "Android TV", + "smarttv-generic": "Smart TV", + "smarttv-tizen": "Samsung Smart TV", + custom: t("custom") + } + }, + "audio.mic.onPlaying": { + label: t("enable-mic-on-startup"), + default: !1 + }, + "audio.volume.booster.enabled": { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + "xhome.enabled": { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + labelIcon: BxIcon.REMOTE_PLAY, + default: !1 + }, + "xhome.video.resolution": { + requiredVariants: "full", + default: "1080p", + options: { + "720p": "720p", + "1080p": "1080p", + "1080p-hq": "1080p (HQ)" + } + }, + "game.fortnite.forceConsole": { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + } + }; + constructor() { + super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS); + } +} +class BaseLocalTable { + tableName; + constructor(tableName) { + this.tableName = tableName; + } + async prepareTable(type = "readonly") { + return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName); + } + call(method) { + return new Promise((resolve) => { + let request = method.call(null, ...Array.from(arguments).slice(1)); + request.onsuccess = (e) => { + resolve(e.target.result); + }; + }); + } + async count() { + let table = await this.prepareTable(); + return this.call(table.count.bind(table)); + } + async add(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.add.bind(table), ...arguments); + } + async put(data) { + let table = await this.prepareTable("readwrite"); + return this.call(table.put.bind(table), ...arguments); + } + async delete(id) { + let table = await this.prepareTable("readwrite"); + return this.call(table.delete.bind(table), ...arguments); + } + async get(id) { + let table = await this.prepareTable(); + return this.call(table.get.bind(table), ...arguments); + } + async getAll() { + let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {}; + return all.forEach((item2) => { + results[item2.id] = item2; + }), results; + } +} +class BasePresetsTable extends BaseLocalTable { + async newPreset(name, data) { + let newRecord = { name, data }; + return await this.add(newRecord); + } + async updatePreset(preset) { + return await this.put(preset); + } + async deletePreset(id) { + return this.delete(id); + } + async getPreset(id) { + if (id === 0) return null; + if (id < 0) return this.DEFAULT_PRESETS[id]; + let preset = await this.get(id); + if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID]; + return preset; + } + async getPresets() { + let all = deepClone(this.DEFAULT_PRESETS), presets = { + default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)), + custom: [], + data: {} + }; + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item2 = items[id]; + presets.custom.push(item2.id), all[item2.id] = item2; + } + } + return presets.data = all, presets; + } + async getPresetsData() { + let presetsData = {}; + for (let id in this.DEFAULT_PRESETS) { + let preset = this.DEFAULT_PRESETS[id]; + presetsData[id] = deepClone(preset.data); + } + if (await this.count() > 0) { + let items = await this.getAll(), id; + for (id in items) { + let item2 = items[id]; + presetsData[item2.id] = item2.data; + } + } + return presetsData; + } +} +class KeyboardShortcutsTable extends BasePresetsTable { + static instance; + static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable); + LOG_TAG = "KeyboardShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: t("standard"), + data: { + mapping: { + "mkb.toggle": { + code: "F8" + }, + "stream.screenshot.capture": { + code: "Slash" + } + } + } + } + }; + BLANK_PRESET_DATA = { + mapping: {} + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_KEYBOARD_SHORTCUTS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +class MkbMappingPresetsTable extends BasePresetsTable { + static instance; + static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable); + LOG_TAG = "MkbMappingPresetsTable"; + TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: t("standard"), + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp", "Digit1"], + 13: ["ArrowDown", "Digit2"], + 14: ["ArrowLeft", "Digit3"], + 15: ["ArrowRight", "Digit4"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyU"], + 201: ["KeyJ"], + 202: ["KeyH"], + 203: ["KeyK"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["KeyC", "Backspace"], + 3: ["KeyV"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyQ"], + 5: ["KeyF"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["KeyX"], + 11: ["KeyZ"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + }, + [-2]: { + id: -2, + name: "Shooter", + data: { + mapping: { + 16: ["Backquote"], + 12: ["ArrowUp"], + 13: ["ArrowDown"], + 14: ["ArrowLeft"], + 15: ["ArrowRight"], + 100: ["KeyW"], + 101: ["KeyS"], + 102: ["KeyA"], + 103: ["KeyD"], + 200: ["KeyI"], + 201: ["KeyK"], + 202: ["KeyJ"], + 203: ["KeyL"], + 0: ["Space", "KeyE"], + 2: ["KeyR"], + 1: ["ControlLeft", "Backspace"], + 3: ["KeyV"], + 9: ["Enter"], + 8: ["Tab"], + 4: ["KeyC", "KeyG"], + 5: ["KeyQ"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + } + } + }; + BLANK_PRESET_DATA = { + mapping: {}, + mouse: { + mapTo: 2, + sensitivityX: 100, + sensitivityY: 100, + deadzoneCounterweight: 20 + } + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_VIRTUAL_CONTROLLERS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +class GameSettingsStorage extends BaseSettingsStorage { + constructor(id) { + super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS); + } + deleteSetting(pref) { + if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0; + return !1; + } +} +class ControllerCustomizationsTable extends BasePresetsTable { + static instance; + static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)); + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "ABXY ⇄ BAYX", + data: { + mapping: { + 0: 1, + 1: 0, + 2: 3, + 3: 2 + }, + settings: { + leftStickDeadzone: [0, 100], + rightStickDeadzone: [0, 100], + leftTriggerRange: [0, 100], + rightTriggerRange: [0, 100], + vibrationIntensity: 100 + } + } + } + }; + BLANK_PRESET_DATA = { + mapping: {}, + settings: { + leftTriggerRange: [0, 100], + rightTriggerRange: [0, 100], + leftStickDeadzone: [0, 100], + rightStickDeadzone: [0, 100], + vibrationIntensity: 100 + } + }; + DEFAULT_PRESET_ID = 0; +} +class ControllerShortcutsTable extends BasePresetsTable { + static instance; + static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable); + LOG_TAG = "ControllerShortcutsTable"; + TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS; + DEFAULT_PRESETS = { + [-1]: { + id: -1, + name: "Type A", + data: { + mapping: { + 3: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 0: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 2: "stream.stats.toggle", + 1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 5: "stream.screenshot.capture", + 9: "stream.menu.show" + } + } + }, + [-2]: { + id: -2, + name: "Type B", + data: { + mapping: { + 12: AppInterface ? "device.volume.inc" : "stream.volume.inc", + 13: AppInterface ? "device.volume.dec" : "stream.volume.dec", + 15: "stream.stats.toggle", + 14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", + 4: "stream.screenshot.capture", + 8: "stream.menu.show" + } + } + } + }; + BLANK_PRESET_DATA = { + mapping: {} + }; + DEFAULT_PRESET_ID = -1; + constructor() { + super(LocalDb.TABLE_CONTROLLER_SHORTCUTS); + BxLogger.info(this.LOG_TAG, "constructor()"); + } +} +class StreamSettingsStorage extends BaseSettingsStorage { + static DEFINITIONS = { + "deviceVibration.mode": { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + off: t("off"), + on: t("on"), + auto: t("device-vibration-not-using-gamepad") + } + }, + "deviceVibration.intensity": { + requiredVariants: "full", + label: t("vibration-intensity"), + default: 50, + min: 10, + max: 100, + params: { + steps: 10, + suffix: "%", + exactTicks: 20 + } + }, + "controller.pollingRate": { + requiredVariants: "full", + label: t("polling-rate"), + default: 4, + min: 4, + max: 60, + params: { + steps: 4, + exactTicks: 20, + reverse: !0, + customTextValue(value) { + value = parseInt(value); + let text = +(1000 / value).toFixed(2) + " Hz"; + if (value === 4) text = `${text} (${t("default")})`; + return text; + } + } + }, + "controller.settings": { + default: {} + }, + "nativeMkb.scroll.sensitivityX": { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + "nativeMkb.scroll.sensitivityY": { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + default: 0, + min: 0, + max: 1e4, + params: { + steps: 10, + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + "mkb.p1.preset.mappingId": { + requiredVariants: "full", + default: -1 + }, + "mkb.p1.slot": { + requiredVariants: "full", + default: 1, + min: 1, + max: 4, + params: { + hideSlider: !0 + } + }, + "mkb.p2.preset.mappingId": { + requiredVariants: "full", + default: 0 + }, + "mkb.p2.slot": { + requiredVariants: "full", + default: 0, + min: 0, + max: 4, + params: { + hideSlider: !0, + customTextValue(value) { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + } + }, + "keyboardShortcuts.preset.inGameId": { + requiredVariants: "full", + default: -1 + }, + "video.player.type": { + label: t("renderer"), + default: "default", + options: { + default: t("default"), + webgl2: t("webgl2") + }, + suggest: { + lowest: "default", + highest: "webgl2" + } + }, + "video.processing": { + label: t("clarity-boost"), + default: "usm", + options: { + usm: t("unsharp-masking"), + cas: t("amd-fidelity-cas") + }, + suggest: { + lowest: "usm", + highest: "cas" + } + }, + "video.player.powerPreference": { + label: t("renderer-configuration"), + default: "default", + options: { + default: t("default"), + "low-power": t("battery-saving"), + "high-performance": t("high-performance") + }, + suggest: { + highest: "low-power" + } + }, + "video.maxFps": { + label: t("limit-fps"), + default: 60, + min: 10, + max: 60, + params: { + steps: 10, + exactTicks: 10, + customTextValue: (value) => { + return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; + } + } + }, + "video.processing.sharpness": { + label: t("sharpness"), + default: 0, + min: 0, + max: 10, + params: { + exactTicks: 2, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value.toString(); + } + }, + suggest: { + lowest: 0, + highest: 2 + } + }, + "video.ratio": { + label: t("aspect-ratio"), + note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, + default: "16:9", + options: { + "16:9": `16:9 (${t("default")})`, + "18:9": "18:9", + "21:9": "21:9", + "16:10": "16:10", + "4:3": "4:3", + fill: t("stretch") + } + }, + "video.position": { + label: t("position"), + note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, + default: "center", + options: { + top: t("top"), + "top-half": t("top-half"), + center: `${t("center")} (${t("default")})`, + "bottom-half": t("bottom-half"), + bottom: t("bottom") + } + }, + "video.saturation": { + label: t("saturation"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "video.contrast": { + label: t("contrast"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "video.brightness": { + label: t("brightness"), + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + "audio.volume": { + label: t("volume"), + default: 100, + min: 0, + max: 600, + params: { + steps: 10, + suffix: "%", + ticks: 100 + } + }, + "stats.items": { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + time: t("clock"), + play: t("playtime"), + batt: t("battery"), + ping: t("stat-ping"), + jit: t("jitter"), + fps: t("stat-fps"), + btr: t("stat-bitrate"), + dt: t("stat-decode-time"), + pl: t("stat-packets-lost"), + fl: t("stat-frames-lost"), + dl: t("downloaded"), + ul: t("uploaded") + }, + params: { + size: 0 + }, + ready: (setting) => { + let multipleOptions = setting.multipleOptions; + if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; + for (let key in multipleOptions) + multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; + } + }, + "stats.showWhenPlaying": { + label: t("show-stats-on-startup"), + default: !1 + }, + "stats.quickGlance.enabled": { + label: "👀 " + t("enable-quick-glance-mode"), + default: !0 + }, + "stats.position": { + label: t("position"), + default: "top-right", + options: { + "top-left": t("top-left"), + "top-center": t("top-center"), + "top-right": t("top-right") + } + }, + "stats.textSize": { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + "stats.opacity.all": { + label: t("opacity"), + default: 80, + min: 50, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10 + } + }, + "stats.opacity.background": { + label: t("background-opacity"), + default: 100, + min: 0, + max: 100, + params: { + steps: 10, + suffix: "%", + ticks: 10 + } + }, + "stats.colors": { + label: t("conditional-formatting"), + default: !1 + }, + "localCoOp.enabled": { + requiredVariants: "full", + label: t("enable-local-co-op-support"), + labelIcon: BxIcon.LOCAL_CO_OP, + default: !1, + note: () => CE("div", !1, CE("a", { + href: "https://github.com/redphx/better-xcloud/discussions/275", + target: "_blank" + }, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior")) + } + }; + gameSettings = {}; + xboxTitleId = -1; + constructor() { + super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS); + } + setGameId(id) { + this.xboxTitleId = id; + } + getGameSettings(id) { + if (id > -1) { + if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id); + return this.gameSettings[id]; + } + return null; + } + getSetting(key, checkUnsupported) { + return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported); + } + getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) { + let gameSettings = this.getGameSettings(id); + if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported); + if (returnBaseValue) return super.getSetting(key, checkUnsupported); + return; + } + setSetting(key, value, origin) { + return this.setSettingByGame(this.xboxTitleId, key, value, origin); + } + setSettingByGame(id, key, value, origin) { + let gameSettings = this.getGameSettings(id); + if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin); + return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin); + } + hasGameSetting(id, key) { + let gameSettings = this.getGameSettings(id); + return !!(gameSettings && gameSettings.hasSetting(key)); + } + getControllerSetting(gamepadId) { + let controllerSetting = this.getSetting("controller.settings")[gamepadId]; + if (!controllerSetting) controllerSetting = {}; + if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1; + if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0; + return controllerSetting; + } +} +var STORAGE = { + Global: new GlobalSettingsStorage, + Stream: new StreamSettingsStorage +}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage); +STORAGE.Stream = streamSettingsStorage; +var globalSettingsStorage = STORAGE.Global, getGlobalPrefDefinition = globalSettingsStorage.getDefinition.bind(globalSettingsStorage), getGlobalPref = globalSettingsStorage.getSetting.bind(globalSettingsStorage), setGlobalPref = globalSettingsStorage.setSetting.bind(globalSettingsStorage); +function isGlobalPref(prefKey) { + return ALL_PREFS.global.includes(prefKey); +} +function isStreamPref(prefKey) { + return ALL_PREFS.stream.includes(prefKey); +} +function getPrefInfo(prefKey) { + if (isGlobalPref(prefKey)) return { + storage: STORAGE.Global, + definition: getGlobalPrefDefinition(prefKey) + }; + else if (isStreamPref(prefKey)) return { + storage: STORAGE.Stream, + definition: getStreamPrefDefinition(prefKey) + }; + return alert("Missing pref definition: " + prefKey), {}; +} +function setPref(prefKey, value, origin) { + if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin); + else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin); +} +function checkForUpdate() { + if (SCRIPT_VERSION.includes("beta")) return; + let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getGlobalPref("version.current"), lastCheck = getGlobalPref("version.lastCheck"), now = Math.round(+new Date / 1000); + if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; + setGlobalPref("version.lastCheck", now, "direct"), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setGlobalPref("version.latest", json.tag_name.substring(1), "direct"), setGlobalPref("version.current", SCRIPT_VERSION, "direct"); + }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); +} +function disablePwa() { + if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; + if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { + value: !0 + }); +} +function hashCode(str) { + let hash = 0; + for (let i = 0, len = str.length;i < len; i++) { + let chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str, obj) { + return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => { + let name = p1 || p2; + return name in obj ? obj[name] : match; + }); +} +function ceilToNearest(value, interval) { + return Math.ceil(value / interval) * interval; +} +function floorToNearest(value, interval) { + return Math.floor(value / interval) * interval; +} +async function copyToClipboard(text, showToast = !0) { + try { + return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; + } catch (err) { + console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); + } + return !1; +} +function productTitleToSlug(title) { + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); +} +function parseDetailsPath(path) { + let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); + if (!matches?.groups) return {}; + let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; + return { titleSlug, productId }; +} +function clearAllData() { + for (let i = 0;i < localStorage.length; i++) { + let key = localStorage.key(i); + if (!key) continue; + if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key); + } + try { + indexedDB.deleteDatabase(LocalDb.DB_NAME); + } catch (e) {} + alert(t("clear-data-success")); +} +function blockAllNotifications() { + let blockFeatures = getGlobalPref("block.features"); + return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value)); +} +function blockSomeNotifications() { + let blockFeatures = getGlobalPref("block.features"); + if (blockAllNotifications()) return !1; + return ["friends", "notifications-achievements", "notifications-invites"].some((value) => blockFeatures.includes(value)); +} +function isPlainObject(input) { + return typeof input === "object" && input !== null && input.constructor === Object; +} +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; + quickGlanceObserver; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), 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.quickGlanceStop(), this.hideSettingsUi(); + } + isHidden = () => this.$container.classList.contains("bx-gone"); + isGlancing = () => this.$container.dataset.display === "glancing"; + quickGlanceSetup() { + if (!STATES.isPlaying || this.quickGlanceObserver) return; + let $uiContainer = document.querySelector("div[data-testid=ui-container]"); + if (!$uiContainer) return; + this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { + for (let record of mutationList) { + let $target = record.target; + if (!$target.className || !$target.className.startsWith("GripHandle")) continue; + if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0); + else this.stop(!0); + } + }), this.quickGlanceObserver.observe($uiContainer, { + attributes: !0, + attributeFilter: ["aria-expanded"], + subtree: !0 + }); + } + quickGlanceStop() { + this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null; + } + 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) streamStats.quickGlanceSetup(), !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() {} +} +class MkbHandler {} +class MkbPopup { + static instance; + static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); + popupType; + $popup; + $title; + $btnActivate; + mkbHandler; + constructor() { + this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => { + let $newButton = this.createActivateButton(); + this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton; + }); + } + attachMkbHandler(handler) { + this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller"); + } + toggleVisibility(show) { + this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1); + } + moveOffscreen(doMove) { + this.$popup.classList.toggle("bx-offscreen", doMove); + } + createActivateButton() { + let options = { + style: 1 | 1024 | 128, + label: t("activate"), + onClick: this.onActivate + }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); + if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); + return createButton(options); + } + onActivate = (e) => { + e.preventDefault(), this.mkbHandler.toggle(!0); + }; + render() { + this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({ + label: t("ignore"), + style: 8, + onClick: (e) => { + e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1); + } + }), createButton({ + label: t("manage"), + icon: BxIcon.MANAGE, + style: 64, + onClick: () => { + let dialog = SettingsDialog.getInstance(); + dialog.focusTab("mkb"), dialog.show(); + } + }))), document.documentElement.appendChild(this.$popup); + } + reset() { + this.toggleVisibility(!0), this.moveOffscreen(!1); + } +} +class NativeMkbHandler extends MkbHandler { + static instance; + static getInstance() { + if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler; + else NativeMkbHandler.instance = null; + return NativeMkbHandler.instance; + } + LOG_TAG = "NativeMkbHandler"; + static isAllowed = () => { + return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on"; + }; + pointerClient; + enabled = !1; + mouseButtonsPressed = 0; + mouseVerticalMultiply = 0; + mouseHorizontalMultiply = 0; + inputChannel; + popup; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); + } + onKeyboardEvent(e) { + if (e.type === "keyup" && e.code === "F8") { + e.preventDefault(), this.toggle(); + return; + } + } + onPointerLockRequested(e) { + AppInterface.requestPointerCapture(), this.start(); + } + onPointerLockExited(e) { + AppInterface.releasePointerCapture(), this.stop(); + } + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); + }; + onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + handleEvent(event) { + switch (event.type) { + case "keyup": + this.onKeyboardEvent(event); + break; + case BxEvent.POINTER_LOCK_REQUESTED: + this.onPointerLockRequested(event); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.onPointerLockExited(event); + break; + case BxEvent.XCLOUD_POLLING_MODE_CHANGED: + this.onPollingModeChanged(event); + break; + } + } + init() { + this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1); + try { + this.pointerClient.start(STATES.pointerServerPort, this); + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown); + let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); + if (shortcutKey) { + let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); + Toast.show(msg, t("native-mkb"), { html: !0 }); + } + this.waitForMouseData(!1); + } + toggle(force) { + let setEnable; + if (typeof force !== "undefined") setEnable = force; + else setEnable = !this.enabled; + if (setEnable) document.documentElement.requestPointerLock(); + else document.exitPointerLock(); + } + updateInputConfigurationAsync(enabled) { + window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ + enableKeyboardInput: enabled, + enableMouseInput: enabled, + enableAbsoluteMouse: !1, + enableTouchInput: !1 + }); + } + start() { + this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); + } + stop() { + this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0); + } + destroy() { + this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock(); + } + handleMouseMove(data) { + this.sendMouseInput({ + X: data.movementX, + Y: data.movementY, + Buttons: this.mouseButtonsPressed, + WheelX: 0, + WheelY: 0 + }); + } + handleMouseClick(data) { + let { pointerButton, pressed } = data; + if (pressed) this.mouseButtonsPressed |= pointerButton; + else this.mouseButtonsPressed ^= pointerButton; + this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.mouseButtonsPressed, + WheelX: 0, + WheelY: 0 + }); + } + handleMouseWheel(data) { + let { vertical, horizontal } = data, mouseWheelX = horizontal; + if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply; + let mouseWheelY = vertical; + if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply; + return this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.mouseButtonsPressed, + WheelX: mouseWheelX, + WheelY: mouseWheelY + }), !0; + } + setVerticalScrollMultiplier(vertical) { + this.mouseVerticalMultiply = vertical; + } + setHorizontalScrollMultiplier(horizontal) { + this.mouseHorizontalMultiply = horizontal; + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); + } + isEnabled() { + return this.enabled; + } + sendMouseInput(data) { + data.Type = 0, this.inputChannel?.queueMouseInput(data); + } + resetMouseInput() { + this.mouseButtonsPressed = 0, this.sendMouseInput({ + X: 0, + Y: 0, + Buttons: 0, + WheelX: 0, + WheelY: 0 + }); + } +} +function showGamepadToast(gamepad) { + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; + if (gamepad._noToast) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`; + let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); + text += ` - ${gamepadId}`; + let status; + if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); + else status = t("disconnected"); + Toast.show(text, status, { instant: !1 }); +} +function simplifyGamepadName(name) { + return name.replace(/\s+\(.*Vendor: ([0-9a-f]{4}) Product: ([0-9a-f]{4})\)$/, " ($1-$2)"); +} +function getUniqueGamepadNames() { + let gamepads = window.navigator.getGamepads(), names = []; + for (let gamepad of gamepads) + if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id); + return names; +} +function hasGamepad() { + let gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad?.connected) return !0; + return !1; +} +function generateVirtualControllerMapping(index, override = {}) { + return Object.assign({}, { + GamepadIndex: index, + A: 0, + B: 0, + X: 0, + Y: 0, + LeftShoulder: 0, + RightShoulder: 0, + LeftTrigger: 0, + RightTrigger: 0, + View: 0, + Menu: 0, + LeftThumb: 0, + RightThumb: 0, + DPadUp: 0, + DPadDown: 0, + DPadLeft: 0, + DPadRight: 0, + Nexus: 0, + LeftThumbXAxis: 0, + LeftThumbYAxis: 0, + RightThumbXAxis: 0, + RightThumbYAxis: 0, + PhysicalPhysicality: 0, + VirtualPhysicality: 0, + Dirty: !1, + Virtual: !1 + }, override); +} +function getGamepadPrompt(gamepadKey) { + return GamepadKeyName[gamepadKey][1]; +} +var XCLOUD_GAMEPAD_KEY_MAPPING = { + 0: "A", + 1: "B", + 2: "X", + 3: "Y", + 12: "DPadUp", + 15: "DPadRight", + 13: "DPadDown", + 14: "DPadLeft", + 4: "LeftShoulder", + 5: "RightShoulder", + 6: "LeftTrigger", + 7: "RightTrigger", + 10: "LeftThumb", + 11: "RightThumb", + 104: "LeftStickAxes", + 204: "RightStickAxes", + 8: "View", + 9: "Menu", + 16: "Nexus", + 17: "Share", + 102: "LeftThumbXAxis", + 103: "LeftThumbXAxis", + 100: "LeftThumbYAxis", + 101: "LeftThumbYAxis", + 202: "RightThumbXAxis", + 203: "RightThumbXAxis", + 200: "RightThumbYAxis", + 201: "RightThumbYAxis" +}; +function toXcloudGamepadKey(gamepadKey) { + return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey]; +} +var PointerToMouseButton = { + 1: 0, + 2: 2, + 4: 1 +}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; +class WebSocketMouseDataProvider extends MouseDataProvider { + pointerClient; + isConnected = !1; + init() { + this.pointerClient = PointerClient.getInstance(), this.isConnected = !1; + try { + this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); + } + } + start() { + this.isConnected && AppInterface.requestPointerCapture(); + } + stop() { + this.isConnected && AppInterface.releasePointerCapture(); + } + destroy() { + this.isConnected && this.pointerClient?.stop(); + } +} +class PointerLockMouseDataProvider extends MouseDataProvider { + start() { + window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu); + } + stop() { + document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu); + } + onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); + }; + onMouseEvent = (e) => { + e.preventDefault(); + let data = { + mouseButton: e.button, + pressed: e.type === "mousedown" + }; + this.mkbHandler.handleMouseClick(data); + }; + onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + let data = { + vertical: e.deltaY, + horizontal: e.deltaX + }; + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + disableContextMenu = (e) => e.preventDefault(); +} +class EmulatedMkbHandler extends MkbHandler { + static instance; + static getInstance() { + if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler; + else EmulatedMkbHandler.instance = null; + return EmulatedMkbHandler.instance; + } + static LOG_TAG = "EmulatedMkbHandler"; + static isAllowed() { + return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile()); + } + PRESET; + VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 0, + connected: !1, + hapticActuators: null, + mapping: "standard", + axes: [0, 0, 0, 0], + buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), + timestamp: performance.now(), + vibrationActuator: null + }; + nativeGetGamepads; + xCloudGamepad = generateVirtualControllerMapping(0); + initialized = !1; + enabled = !1; + mouseDataProvider; + isPolling = !1; + prevWheelCode = null; + wheelStoppedTimeoutId = null; + detectMouseStoppedTimeoutId = null; + escKeyDownTime = -1; + LEFT_STICK_X = []; + LEFT_STICK_Y = []; + RIGHT_STICK_X = []; + RIGHT_STICK_Y = []; + popup; + STICK_MAP = { + 102: [this.LEFT_STICK_X, -1], + 103: [this.LEFT_STICK_X, 1], + 100: [this.LEFT_STICK_Y, 1], + 101: [this.LEFT_STICK_Y, -1], + 202: [this.RIGHT_STICK_X, -1], + 203: [this.RIGHT_STICK_X, 1], + 200: [this.RIGHT_STICK_Y, 1], + 201: [this.RIGHT_STICK_Y, -1] + }; + constructor() { + super(); + BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); + } + isEnabled = () => this.enabled; + patchedGetGamepads = () => { + let gamepads = this.nativeGetGamepads() || []; + return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads; + }; + getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; + updateStick(stick, x, y) { + let gamepad = this.xCloudGamepad; + if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y; + else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y; + window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); + } + vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + resetXcloudGamepads() { + let index = getStreamPref("mkb.p1.slot") - 1; + this.xCloudGamepad = generateVirtualControllerMapping(0, { + GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0, + Dirty: !0 + }), this.VIRTUAL_GAMEPAD.index = index; + } + pressButton(buttonIndex, pressed) { + let xCloudKey = toXcloudGamepadKey(buttonIndex); + if (buttonIndex >= 100) { + let [valueArr] = this.STICK_MAP[buttonIndex]; + for (let i = valueArr.length - 1;i >= 0; i--) + if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); + pressed && valueArr.push(buttonIndex); + let value; + if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1]; + else value = 0; + this.xCloudGamepad[xCloudKey] = value; + } else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0; + window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); + } + onKeyboardEvent = (e) => { + let isKeyDown = e.type === "keydown"; + if (e.code === "Escape") { + if (e.preventDefault(), this.enabled && isKeyDown) { + if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now(); + else if (performance.now() - this.escKeyDownTime >= 1000) this.stop(); + } else this.escKeyDownTime = -1; + return; + } + if (!this.isPolling || !this.PRESET) return; + if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; + let buttonIndex = this.PRESET.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.pressButton(buttonIndex, isKeyDown); + }; + onMouseStopped = () => { + if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return; + let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1; + this.updateStick(analog, 0, 0); + }; + handleMouseClick(data) { + let mouseButton; + if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; + else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; + let key = { + code: "Mouse" + mouseButton + }; + if (!this.PRESET) return; + let buttonIndex = this.PRESET.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.pressButton(buttonIndex, data.pressed); + } + handleMouseMove(data) { + let preset = this.PRESET; + if (!preset) return; + let mouseMapTo = preset.mouse["mapTo"]; + if (mouseMapTo === 0) return; + this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); + let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length; + let analog = mouseMapTo === 1 ? 0 : 1; + this.updateStick(analog, x, y); + } + handleMouseWheel(data) { + let code = ""; + if (data.vertical < 0) code = "ScrollUp"; + else if (data.vertical > 0) code = "ScrollDown"; + else if (data.horizontal < 0) code = "ScrollLeft"; + else if (data.horizontal > 0) code = "ScrollRight"; + if (!code) return !1; + if (!this.PRESET) return !1; + let key = { + code + }, buttonIndex = this.PRESET.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0); + return this.wheelStoppedTimeoutId = window.setTimeout(() => { + this.prevWheelCode = null, this.pressButton(buttonIndex, !1); + }, 20), !0; + } + async toggle(force) { + if (!this.initialized) return; + if (typeof force !== "undefined") this.enabled = force; + else this.enabled = !this.enabled; + if (this.enabled) try { + await document.body.requestPointerLock({ unadjustedMovement: !0 }); + } catch (e) { + document.body.requestPointerLock(), console.log(e); + } + else document.pointerLockElement && document.exitPointerLock(); + } + refreshPresetData() { + this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads(); + } + waitForMouseData(showPopup) { + this.popup.toggleVisibility(showPopup); + } + onPollingModeChanged = (e) => { + let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; + this.popup.moveOffscreen(move); + }; + onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + onPointerLockChange = () => { + if (document.pointerLockElement) this.start(); + else this.stop(); + }; + onPointerLockError = (e) => { + console.log(e), this.stop(); + }; + onPointerLockRequested = () => { + this.start(); + }; + onPointerLockExited = () => { + this.mouseDataProvider?.stop(); + }; + handleEvent(event) { + switch (event.type) { + case BxEvent.POINTER_LOCK_REQUESTED: + this.onPointerLockRequested(); + break; + case BxEvent.POINTER_LOCK_EXITED: + this.onPointerLockExited(); + break; + } + } + init() { + if (!STATES.browser.capabilities.mkb) { + this.initialized = !1; + return; + } + if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this); + else this.mouseDataProvider = new PointerLockMouseDataProvider(this); + if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError); + if (MkbPopup.getInstance().reset(), AppInterface) { + let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); + if (shortcutKey) { + let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); + Toast.show(msg, t("native-mkb"), { html: !0 }); + } + this.waitForMouseData(!1); + } else this.waitForMouseData(!0); + } + destroy() { + if (!this.initialized) return; + if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); + else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError); + window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); + } + start() { + if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start(); + let virtualGamepad = this.getVirtualGamepad(); + virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { + gamepad: virtualGamepad + }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); + } + stop() { + this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1; + let virtualGamepad = this.getVirtualGamepad(); + if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { + gamepad: virtualGamepad + }), window.navigator.getGamepads = this.nativeGetGamepads; + this.waitForMouseData(!0), this.mouseDataProvider?.stop(); + } + static setupEvents() { + if (BxEventBus.Stream.on("state.playing", () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init(); + else EmulatedMkbHandler.getInstance()?.init(); + }), EmulatedMkbHandler.isAllowed()) + BxEventBus.Stream.on("mkb.setting.updated", () => { + EmulatedMkbHandler.getInstance()?.refreshPresetData(); + }); + } +} +class StreamSettings { + static settings = { + settings: {}, + xCloudPollingMode: "all", + deviceVibrationIntensity: 0, + controllerPollingRate: 4, + controllers: {}, + mkbPreset: null, + keyboardShortcuts: {} + }; + static async refreshControllerSettings() { + let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad?.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data); + controllers[gamepad.id] = { + shortcuts: shortcutsMapping, + customization: customizationData + }; + } + settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); + } + static preCalculateControllerRange(obj, target, values) { + if (values && Array.isArray(values)) { + let [from, to] = values; + if (from > 1 || to < 100) obj[target] = [from / 100, to / 100]; + } + } + static convertControllerCustomization(customization) { + if (!customization) return null; + let converted = { + mapping: {}, + ranges: {}, + vibrationIntensity: 1 + }, gamepadKey; + for (gamepadKey in customization.mapping) { + let gamepadStr = toXcloudGamepadKey(gamepadKey); + if (!gamepadStr) continue; + let mappedKey = customization.mapping[gamepadKey]; + if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey); + else converted.mapping[gamepadStr] = !1; + } + return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted; + } + static async refreshDeviceVibration() { + if (!STATES.browser.capabilities.deviceVibration) return; + let mode = getStreamPref("deviceVibration.mode"), intensity = 0; + if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100; + StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {}); + } + static async refreshMkbSettings() { + let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { + mapping: {}, + mouse: Object.assign({}, orgPresetData.mouse) + }, key; + for (key in orgPresetData.mapping) { + let buttonIndex = parseInt(key); + if (!orgPresetData.mapping[buttonIndex]) continue; + for (let keyName of orgPresetData.mapping[buttonIndex]) + if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; + } + let mouse = converted.mouse; + mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {}); + } + static async refreshKeyboardShortcuts() { + let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId"); + if (presetId === 0) { + settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); + return; + } + let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; + for (action in orgPresetData) { + let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; + converted[key] = action; + } + settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); + } + static async refreshAllSettings() { + window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); + } + static findKeyboardShortcut(targetAction) { + let shortcuts = StreamSettings.settings.keyboardShortcuts; + for (let codeStr in shortcuts) + if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr); + return null; + } + static setup() { + let listener = () => { + StreamSettings.refreshControllerSettings(); + }; + window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); + } +} +class BxNumberStepper extends HTMLInputElement { + intervalId = null; + isHolding; + controlValue; + controlMin; + controlMax; + uiMin; + uiMax; + steps; + options; + onChange; + $text; + $btnInc; + $btnDec; + $range; + onRangeInput; + onClick; + onPointerUp; + onPointerDown; + setValue; + normalizeValue; + static create(key, value, min, max, options = {}, onChange) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnInc, $btnDec, $range, self = CE("div", { + class: "bx-number-stepper", + id: `bx_setting_${escapeCssSelector(key)}` + }, CE("div", !1, $btnDec = CE("button", { + _dataset: { + type: "dec" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span"), $btnInc = CE("button", { + _dataset: { + type: "inc" + }, + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+"))); + if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self; + if ($range = CE("input", { + id: `bx_inp_setting_${key}`, + type: "range", + min: self.uiMin, + max: self.uiMax, + value: options.reverse ? -value : value, + step: self.steps, + tabindex: 0 + }), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) { + let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); + if ($range.setAttribute("list", markersId), options.exactTicks) { + let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks; + if (start === min) start += options.exactTicks; + for (let i = start;i < max; i += options.exactTicks) + $markers.appendChild(CE("option", { + value: options.reverse ? -i : i + })); + } else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + self.appendChild($markers); + } + return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, { + focus: options.hideSlider ? $btnInc : $range + }), Object.defineProperty(self, "value", { + get() { + return self.controlValue; + }, + set(value2) { + BxNumberStepper.setValue.call(self, value2); + } + }), Object.defineProperty(self, "disabled", { + get() { + return $range.disabled; + }, + set(value2) { + $btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2; + } + }), self; + } + static setValue(value) { + if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString(); + BxNumberStepper.updateButtonsVisibility.call(this); + } + static normalizeValue(value) { + return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value; + } + static onRangeInput(e) { + let value = parseInt(e.target.value); + if (this.options.reverse) value *= -1; + if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value); + } + static onClick(e) { + if (e.preventDefault(), this.isHolding) return; + let $btn = e.target.closest("button"); + $btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onPointerDown(e) { + BxNumberStepper.clearIntervalId.call(this); + let $btn = e.target.closest("button"); + if (!$btn) return; + this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => { + BxNumberStepper.buttonPressed.call(this, e2, $btn); + }, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 }); + } + static onPointerUp(e) { + BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; + } + static onContextMenu(e) { + e.preventDefault(); + } + static updateTextValue() { + let value = this.controlValue, textContent = null; + if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax); + if (textContent === null) textContent = value.toString() + this.options.suffix; + return textContent; + } + static buttonPressed(e, $btn) { + BxNumberStepper.change.call(this, $btn.dataset.type); + } + static change(direction) { + let value = this.controlValue; + if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps); + else value = Math.min(this.uiMax, value + this.steps); + value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue); + } + static clearIntervalId() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + static updateButtonsVisibility() { + if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this); + } +} +class SettingElement { + static renderOptions(key, setting, currentValue, onChange) { + let $control = CE("select", { + tabindex: 0 + }), $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + let label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; + !e.ignoreOnChange && onChange(e, value); + }), $control.setValue = (value) => { + $control.value = value; + }, $control; + } + static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { + let $control = CE("select", { + multiple: !0, + tabindex: 0 + }), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions; + $control.setAttribute("size", size.toString()); + for (let value in setting.multipleOptions) { + let label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + let target = e.target; + target.selected = !target.selected; + let $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + let self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), Object.defineProperty($control, "value", { + get() { + return Array.from($control.options).filter((option) => option.selected).map((option) => option.value); + }, + set(value) { + let values = value.split(","); + Array.from($control.options).forEach((option) => { + option.selected = values.includes(option.value); + }); + } + }), $control; + } + static renderCheckbox(key, setting, currentValue, onChange) { + let $control = CE("input", { type: "checkbox", tabindex: 0 }); + return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { + !e.ignoreOnChange && onChange(e, e.target.checked); + }), $control.setValue = (value) => { + $control.checked = !!value; + }, $control; + } + static renderNumberStepper(key, setting, value, onChange, options = {}) { + return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange); + } + static METHOD_MAP = { + options: SettingElement.renderOptions, + "multiple-options": SettingElement.renderMultipleOptions, + "number-stepper": SettingElement.renderNumberStepper, + checkbox: SettingElement.renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, onChange, overrideParams = {}) { + let { definition, storage } = getPrefInfo(key); + if (!definition) return null; + let currentValue = storage.getSetting(key), type; + if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number-stepper"; + else type = "checkbox"; + let params = {}; + if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); + if (params.disabled) currentValue = definition.default; + return SettingElement.render(type, key, definition, currentValue, (e, value) => { + if (isGlobalPref(key)) setGlobalPref(key, value, "ui"); + else { + let id = SettingsManager.getInstance().getTargetGameId(); + setGamePref(id, key, value, "ui"); + } + onChange && onChange(e, value); + }, params); + } +} +class BxSelectElement extends HTMLSelectElement { + isControllerFriendly; + optionsList; + indicatorsList; + $indicators; + visibleIndex; + isMultiple; + $select; + $btnNext; + $btnPrev; + $label; + $checkBox; + static create($select, forceFriendly = !1) { + let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly"); + if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select; + $select.removeAttribute("tabindex"); + let $wrapper = CE("div", { + class: "bx-select", + _dataset: { + controllerFriendly: isControllerFriendly + } + }); + if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width"); + let $content, self = $wrapper; + self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = []; + let $btnPrev, $btnNext; + if (isControllerFriendly) { + $btnPrev = createButton({ + label: "<", + style: 64 + }), $btnNext = createButton({ + label: ">", + style: 64 + }), setNearby($wrapper, { + orientation: "horizontal", + focus: $btnNext + }), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev; + let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); + $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext); + } else $select.addEventListener("change", (e) => { + self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + }); + if (self.isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => { + self.$checkBox.click(); + }), self.$checkBox.addEventListener("input", (e) => { + let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); + return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { + get() { + return $select.value; + }, + set(value) { + self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); + } + }), Object.defineProperty(self, "disabled", { + get() { + return $select.disabled; + }, + set(value) { + $select.disabled = value; + } + }), self.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, self.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, self.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, self.appendChild = function(node) { + return $select.appendChild(node), node; + }, self; + } + static resetIndicators() { + let { + optionsList, + indicatorsList, + $indicators + } = this, targetSize = optionsList.length; + if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize) + indicatorsList.pop()?.remove(); + else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) { + let $indicator = CE("span", {}); + indicatorsList.push($indicator), $indicators.appendChild($indicator); + } + for (let $indicator of indicatorsList) + clearDataSet($indicator); + $indicators.classList.toggle("bx-invisible", targetSize <= 1); + } + static getOptionAtIndex(index) { + return this.optionsList[index]; + } + static render(e) { + let { + $label, + $btnNext, + $btnPrev, + $checkBox, + optionsList, + indicatorsList + } = this; + if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex; + this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex); + let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; + if ($option) { + let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); + if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) { + let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; + $label.innerHTML = ""; + let fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); + } else $label.textContent = content; + } else $label.textContent = content; + if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + let disableButtons = optionsList.length <= 1; + $btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons); + for (let i = 0;i < optionsList.length; i++) { + let $option2 = optionsList[i], $indicator = indicatorsList[i]; + if (!$option2 || !$indicator) continue; + if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; + if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true"; + } + } + static normalizeIndex(index) { + return Math.min(Math.max(index, 0), this.optionsList.length - 1); + } + static onPrevNext(e) { + if (!e.target) return; + let { + $btnNext, + $select, + isMultiple, + visibleIndex: currentIndex + } = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex > this.optionsList.length - 1) newIndex = 0; + else if (newIndex < 0) newIndex = this.optionsList.length - 1; + if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) BxSelectElement.render.call(this); + else BxEvent.dispatch($select, "input"); + } +} +class XboxApi { + static CACHED_TITLES = {}; + static async getProductTitle(xboxTitleId) { + if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; + try { + let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle; + return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle; + } catch (e) {} + return; + } +} +class SettingsManager { + static instance; + static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager); + $streamSettingsSelection; + $tips; + playingGameId = -1; + targetGameId = -1; + SETTINGS = { + "localCoOp.enabled": { + onChange: () => { + BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled")); + } + }, + "deviceVibration.mode": { + onChange: StreamSettings.refreshControllerSettings + }, + "deviceVibration.intensity": { + onChange: StreamSettings.refreshControllerSettings + }, + "controller.pollingRate": { + onChange: StreamSettings.refreshControllerSettings + }, + "controller.settings": { + onChange: StreamSettings.refreshControllerSettings + }, + "nativeMkb.scroll.sensitivityX": { + onChange: () => { + let value = getStreamPref("nativeMkb.scroll.sensitivityX"); + NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); + } + }, + "nativeMkb.scroll.sensitivityY": { + onChange: () => { + let value = getStreamPref("nativeMkb.scroll.sensitivityY"); + NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); + } + }, + "video.player.type": { + onChange: () => { + if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer(); + }, + alwaysTriggerOnChange: !0 + }, + "video.player.powerPreference": { + onChange: () => { + let streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + streamPlayer.reloadPlayer(), updateVideoPlayer(); + } + }, + "video.processing": { + onChange: updateVideoPlayer + }, + "video.processing.sharpness": { + onChange: updateVideoPlayer + }, + "video.maxFps": { + onChange: () => { + let value = getStreamPref("video.maxFps"); + limitVideoPlayerFps(value); + } + }, + "video.ratio": { + onChange: updateVideoPlayer + }, + "video.brightness": { + onChange: updateVideoPlayer + }, + "video.contrast": { + onChange: updateVideoPlayer + }, + "video.saturation": { + onChange: updateVideoPlayer + }, + "video.position": { + onChange: updateVideoPlayer + }, + "audio.volume": { + onChange: () => { + let value = getStreamPref("audio.volume"); + SoundShortcut.setGainNodeVolume(value); + } + }, + "stats.items": { + onChange: StreamStats.refreshStyles + }, + "stats.quickGlance.enabled": { + onChange: () => { + let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance(); + value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); + } + }, + "stats.position": { + onChange: StreamStats.refreshStyles + }, + "stats.textSize": { + onChange: StreamStats.refreshStyles + }, + "stats.opacity.all": { + onChange: StreamStats.refreshStyles + }, + "stats.opacity.background": { + onChange: StreamStats.refreshStyles + }, + "stats.colors": { + onChange: StreamStats.refreshStyles + }, + "mkb.p1.preset.mappingId": { + onChange: StreamSettings.refreshMkbSettings + }, + "mkb.p1.slot": { + onChange: () => { + EmulatedMkbHandler.getInstance()?.resetXcloudGamepads(); + } + }, + "keyboardShortcuts.preset.inGameId": { + onChange: StreamSettings.refreshKeyboardShortcuts + } + }; + constructor() { + BxEventBus.Stream.on("setting.changed", (data) => { + if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey); + }), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => { + this.switchGameSettings(id); + }), this.renderStreamSettingsSelection(); + } + updateStreamElement(key, onChanges) { + let info = this.SETTINGS[key]; + if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange); + else info.onChange(); + let $elm = info.$element; + if (!$elm) return; + let value = getGamePref(this.targetGameId, key, !0); + if ("setValue" in $elm) $elm.setValue(value); + else $elm.value = value.toString(); + this.updateDataset($elm, key); + } + switchGameSettings(id) { + if (setGameIdPref(id), this.targetGameId === id) return; + let onChanges = new Set, oldGameId = this.targetGameId; + this.targetGameId = id; + let key; + for (key in this.SETTINGS) { + if (!isStreamPref(key)) continue; + let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0); + if (oldValue === newValue) continue; + this.updateStreamElement(key, onChanges); + } + onChanges.forEach((onChange) => { + onChange && onChange(); + }), this.$tips.classList.toggle("bx-gone", id < 0); + } + setElement(pref, $elm) { + if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; + this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm; + } + getElement(pref, params) { + if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; + let $elm = this.SETTINGS[pref].$element; + if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm; + return this.updateDataset($elm, pref), $elm; + } + hasElement(pref) { + return !!this.SETTINGS[pref]?.$element; + } + updateDataset($elm, pref) { + if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true"; + else delete $elm.dataset.override; + } + renderStreamSettingsSelection() { + this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`); + let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0); + $select.addEventListener("input", (e) => { + let id = parseInt($select.value); + BxEventBus.Stream.emit("gameSettings.switched", { id }); + }), this.$streamSettingsSelection = CE("div", { + class: "bx-stream-settings-selection bx-gone", + _nearby: { orientation: "vertical" } + }, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => { + this.playingGameId = id, setGameIdPref(id); + let $optGroup = $select.querySelector("optgroup"); + while ($optGroup.childElementCount > 1) + $optGroup.lastElementChild?.remove(); + if (id >= 0) { + let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id); + $optGroup.appendChild(CE("option", { + value: id + }, title)), $select.value = id.toString(); + } else $select.value = "-1"; + BxEventBus.Stream.emit("gameSettings.switched", { id }); + }); + } + getStreamSettingsSelection() { + return this.$streamSettingsSelection; + } + getTargetGameId() { + return this.targetGameId; + } +} +function onChangeVideoPlayerType() { + let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance(); + if (!settingsManager.hasElement("video.processing")) return; + let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; + $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"); +} +function limitVideoPlayerFps(targetFps) { + STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); +} +function updateVideoPlayer() { + let streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + limitVideoPlayerFps(getStreamPref("video.maxFps")); + let options = { + processing: getStreamPref("video.processing"), + sharpness: getStreamPref("video.processing.sharpness"), + saturation: getStreamPref("video.saturation"), + contrast: getStreamPref("video.contrast"), + brightness: getStreamPref("video.brightness") + }; + streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); +} +window.addEventListener("resize", updateVideoPlayer); +class NavigationDialog { + dialogManager; + onMountedCallbacks = []; + constructor() { + this.dialogManager = NavigationDialogManager.getInstance(); + } + isCancellable() { + return !0; + } + isOverlayVisible() { + return !0; + } + show(configs = {}, clearStack = !1) { + if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded(); + } + hide() { + NavigationDialogManager.getInstance().hide(); + } + getFocusedElement() { + let $activeElement = document.activeElement; + if (!$activeElement) return null; + if (this.$container.contains($activeElement)) return $activeElement; + return null; + } + onBeforeMount(configs = {}) {} + onMounted(configs = {}) { + for (let callback of this.onMountedCallbacks) + callback.call(this); + } + onBeforeUnmount() {} + onUnmounted() {} + handleKeyPress(key) { + return !1; + } + handleGamepad(button) { + return !1; + } +} +class NavigationDialogManager { + static instance; + static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager); + LOG_TAG = "NavigationDialogManager"; + static GAMEPAD_POLLING_INTERVAL = 50; + static GAMEPAD_KEYS = [ + 0, + 1, + 2, + 3, + 12, + 15, + 13, + 14, + 4, + 5, + 6, + 7, + 10, + 11, + 8, + 9 + ]; + static GAMEPAD_DIRECTION_MAP = { + 12: 1, + 13: 3, + 14: 4, + 15: 2, + 100: 1, + 101: 3, + 102: 4, + 103: 2 + }; + static SIBLING_PROPERTY_MAP = { + horizontal: { + 4: "previousElementSibling", + 2: "nextElementSibling" + }, + vertical: { + 1: "previousElementSibling", + 3: "nextElementSibling" + } + }; + gamepadPollingIntervalId = null; + gamepadLastStates = []; + gamepadHoldingIntervalId = null; + $overlay; + $container; + dialog = null; + dialogsStack = []; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); + }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + let $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); + } + updateActiveInput(input) { + document.documentElement.dataset.activeInput = input; + } + handleEvent(event) { + switch (event.type) { + case "keydown": + this.updateActiveInput("keyboard"); + let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode); + if (handled) { + event.preventDefault(), event.stopPropagation(); + return; + } + if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3); + else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") { + if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2); + } else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") { + if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); + } else if (keyCode === "Escape") handled = !0, this.hide(); + if (handled) event.preventDefault(), event.stopPropagation(); + break; + } + } + isShowing() { + return this.$container && !this.$container.classList.contains("bx-gone"); + } + pollGamepad = () => { + let gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad || !gamepad.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; + if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; + if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue; + for (let key of NavigationDialogManager.GAMEPAD_KEYS) + if (lastKey === key && !buttons[key].pressed) { + releasedButton = key; + break; + } else if (buttons[key].pressed) { + heldButton = key; + break; + } + if (heldButton === null && releasedButton === null && axes && axes.length >= 2) { + if (lastKey) { + let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101); + if (releasedHorizontal || releasedVertical) releasedButton = lastKey; + else heldButton = lastKey; + } else if (axes[0] < -0.5) heldButton = 102; + else if (axes[0] > 0.5) heldButton = 103; + else if (axes[1] < -0.5) heldButton = 100; + else if (axes[1] > 0.5) heldButton = 101; + } + if (heldButton !== null) { + if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => { + let lastState2 = this.gamepadLastStates[gamepad.index]; + if (lastState2) { + if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { + this.handleGamepad(gamepad, heldButton); + return; + } + } + this.clearGamepadHoldingInterval(); + }, 100); + continue; + } + if (releasedButton === null) { + this.clearGamepadHoldingInterval(); + continue; + } + if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return; + if (releasedButton === 0) { + document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); + return; + } else if (releasedButton === 1) { + this.hide(); + return; + } + } + }; + handleGamepad(gamepad, key) { + let handled = this.dialog?.handleGamepad(key); + if (handled) return !0; + let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key]; + if (!direction) return !1; + if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") { + let $range = document.activeElement; + if (direction === 4 || direction === 2) { + let $numberStepper = $range.closest(".bx-number-stepper"); + if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc"); + else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")); + handled = !0; + } + } + if (!handled) this.focusDirection(direction); + return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0; + } + clearGamepadHoldingInterval() { + this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null; + } + show(dialog, configs = {}, clearStack = !1) { + this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); + } + hide() { + if (this.clearGamepadHoldingInterval(), !this.isShowing()) return; + if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) { + let dialogIndex = this.dialogsStack.indexOf(this.dialog); + if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); + } + if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show(); + } + focus($elm) { + if (!$elm) return !1; + if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus); + else return $elm.nearby.focus(); + return $elm.focus(), $elm === document.activeElement; + } + getOrientation($elm) { + let nearby = $elm.nearby || {}; + if (nearby.selfOrientation) return nearby.selfOrientation; + let orientation, $current = $elm.parentElement; + while ($current !== this.$container) { + let tmp = $current.nearby?.orientation; + if ($current.nearby && tmp) { + orientation = tmp; + break; + } + $current = $current.parentElement; + } + return orientation = orientation || "vertical", setNearby($elm, { + selfOrientation: orientation + }), orientation; + } + findNextTarget($focusing, direction, checkParent = !1, checked = []) { + if (!$focusing || $focusing === this.$container) return null; + if (checked.includes($focusing)) return null; + checked.push($focusing); + let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + if (nearby[1] && direction === 1) return nearby[1]; + else if (nearby[3] && direction === 3) return nearby[3]; + else if (nearby[4] && direction === 4) return nearby[4]; + else if (nearby[2] && direction === 2) return nearby[2]; + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + if (siblingProperty) { + let $sibling = $target; + while ($sibling[siblingProperty]) { + $sibling = $sibling[siblingProperty]; + let $focusable = this.findFocusableElement($sibling, direction); + if ($focusable) return $focusable; + } + } + if (nearby.loop) { + if (nearby.loop(direction)) return null; + } + if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked); + return null; + } + findFocusableElement($elm, direction) { + if (!$elm) return null; + if (!!$elm.disabled) return null; + if (!isElementVisible($elm)) return null; + if ($elm.tabIndex > -1) return $elm; + let focus = $elm.nearby?.focus; + if (focus) { + if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction); + else if (typeof focus === "function") { + if (focus()) return document.activeElement; + } + } + let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical"; + if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse(); + for (let $child of children) { + if (!$child || !($child instanceof HTMLElement)) return null; + let $target = this.findFocusableElement($child, direction); + if ($target) return $target; + } + return null; + } + startGamepadPolling() { + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + } + stopGamepadPolling() { + this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; + } + focusDirection(direction) { + let dialog = this.dialog; + if (!dialog) return; + let $focusing = dialog.getFocusedElement(); + if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; + let $target = this.findNextTarget($focusing, direction, !0); + this.focus($target); + } + unmountCurrentDialog() { + let dialog = this.dialog; + dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + } +} +var LOG_TAG = "TouchController"; +class TouchController { + static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { + data: JSON.stringify({ + content: '{"layoutId":""}', + target: "/streaming/touchcontrols/showlayoutv2", + type: "Message" + }), + origin: "better-xcloud" + }); + static #$style; + static #enabled = !1; + static #dataChannel; + static #customLayouts = {}; + static #baseCustomLayouts = {}; + static #currentLayoutId; + static #customList; + static #xboxTitleId = null; + static setXboxTitleId(xboxTitleId) { + TouchController.#xboxTitleId = xboxTitleId; + } + static getCustomLayouts() { + let xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) return null; + return TouchController.#customLayouts[xboxTitleId]; + } + static enable() { + TouchController.#enabled = !0; + } + static disable() { + TouchController.#enabled = !1; + } + static isEnabled() { + return TouchController.#enabled; + } + static #showDefault() { + TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); + } + static #show() { + document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); + } + static toggleVisibility() { + if (!TouchController.#dataChannel) return !1; + let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement; + if (!$container) return !1; + return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen"); + } + static reset() { + TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = ""); + } + static #dispatchMessage(msg) { + TouchController.#dataChannel && window.setTimeout(() => { + TouchController.#dataChannel.dispatchEvent(msg); + }, 10); + } + static #dispatchLayouts(data) { + TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + } + static async requestCustomLayouts(retries = 1) { + let xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) return; + if (xboxTitleId in TouchController.#customLayouts) { + TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); + return; + } + if (retries = retries || 1, retries > 2) { + TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); + return; + } + try { + let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {}; + json.layouts.forEach(async (layoutName) => { + let baseLayouts = {}; + if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; + else try { + let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`); + baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; + } catch (e) {} + Object.assign(layouts, baseLayouts); + }), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); + } catch (e) { + TouchController.requestCustomLayouts(retries + 1); + } + } + static applyCustomLayout(layoutId, delay = 0) { + if (!window.BX_EXPOSED.touchLayoutManager) { + let listener = (e) => { + if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); + }; + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 }); + return; + } + let xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) { + BxLogger.error(LOG_TAG, "Invalid xboxTitleId"); + return; + } + if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; + if (!layoutId) { + BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); + return; + } + let layoutChanged = TouchController.#currentLayoutId !== layoutId; + TouchController.#currentLayoutId = layoutId; + let layoutData = TouchController.#customLayouts[xboxTitleId]; + if (!xboxTitleId || !layoutId || !layoutData) { + TouchController.#enabled && TouchController.#showDefault(); + return; + } + let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; + if (!layout) return; + let msg, html = !1; + if (layout.author) { + let author = `${escapeHtml(layout.author)}`; + msg = t("touch-control-layout-by", { name: author }), html = !0; + } else msg = t("touch-control-layout"); + layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => { + window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "System", + layoutFile: layout + } + }); + }, delay); + } + static updateCustomList() { + TouchController.#customList = GhPagesUtils.getTouchControlCustomList(); + } + static getCustomList() { + return TouchController.#customList; + } + static setup() { + window.testTouchLayout = (layout) => { + let { touchLayoutManager } = window.BX_EXPOSED; + touchLayoutManager && touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: "" + TouchController.#xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "Custom", + layoutFile: layout + } + }); + }; + let $style = document.createElement("style"); + document.documentElement.appendChild($style), TouchController.#$style = $style; + let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom"); + BxEventBus.Stream.on("dataChannelCreated", (payload) => { + let { dataChannel } = payload; + if (dataChannel?.label !== "message") return; + let filter = ""; + if (TouchController.#enabled) { + if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)"; + else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)"; + } else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)"; + if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; + else $style.textContent = ""; + TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => { + window.setTimeout(TouchController.#show, 1000); + }); + let focused = !1; + dataChannel.addEventListener("message", (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; + if (msg.data.includes("touchcontrols/showtitledefault")) { + if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts(); + else TouchController.#showDefault(); + return; + } + try { + if (msg.data.includes("/titleinfo")) { + let json = JSON.parse(JSON.parse(msg.data).content); + if (focused = json.focused, !json.focused) TouchController.#show(); + TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); + } + } catch (e) { + BxLogger.error(LOG_TAG, "Load custom layout", e); + } + }); + }); + } +} +var controller_customization_default = "var shareButtonPressed=currentGamepad.buttons[17]?.pressed,shareButtonHandled=!1,xCloudGamepad=$xCloudGamepadVar$;if(currentGamepad.id in window.BX_STREAM_SETTINGS.controllers){let controller=window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];if(controller?.customization){let{mapping,ranges}=controller.customization,pressedButtons={},releasedButtons={},isModified=!1;if(ranges.LeftTrigger){let[from,to]=ranges.LeftTrigger;xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTrigger>to?1:xCloudGamepad.LeftTrigger,xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTriggerto?1:xCloudGamepad.RightTrigger,xCloudGamepad.RightTrigger=xCloudGamepad.RightTriggerto?1:range;if(newRange=newRangeto?1:range;if(newRange=newRange=0.1)pressedButtons[targetX]=rangeX,pressedButtons[targetY]=rangeY}releasedButtons[sourceX]=0,releasedButtons[sourceY]=0,isModified=!0}else if(typeof mappedKey===\"string\"){let pressed=!1,value=0;if(key===\"LeftTrigger\"||key===\"RightTrigger\"){let currentRange=xCloudGamepad[key];if(mappedKey===\"LeftTrigger\"||mappedKey===\"RightTrigger\")pressed=currentRange>=0.1,value=currentRange;else pressed=!0,value=currentRange>=0.9?1:0}else if(xCloudGamepad[key])pressed=!0,value=xCloudGamepad[key];if(pressed)pressedButtons[mappedKey]=value,releasedButtons[key]=0,isModified=!0}else if(mappedKey===!1)pressedButtons[key]=0,isModified=!0}isModified&&Object.assign(xCloudGamepad,releasedButtons,pressedButtons)}}if(shareButtonPressed&&!shareButtonHandled)window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n"; +var poll_gamepad_default = "var self=this;if(window.BX_EXPOSED.disableGamepadPolling){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(50):self.pollGamepadssetTimeoutTimerID=window.setTimeout(self.pollGamepads,50);return}var currentGamepad=$gamepadVar$,btnHome=currentGamepad.buttons[16];if(btnHome){if(!self.bxHomeStates)self.bxHomeStates={};let intervalMs=0,hijack=!1;if(btnHome.pressed)if(hijack=!0,intervalMs=16,self.gamepadIsIdle.set(currentGamepad.index,!1),self.bxHomeStates[currentGamepad.index]){let lastTimestamp=self.bxHomeStates[currentGamepad.index].timestamp;if(currentGamepad.timestamp!==lastTimestamp){if(self.bxHomeStates[currentGamepad.index].timestamp=currentGamepad.timestamp,window.BX_EXPOSED.handleControllerShortcut(currentGamepad))self.bxHomeStates[currentGamepad.index].shortcutPressed+=1}}else window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index),self.bxHomeStates[currentGamepad.index]={shortcutPressed:0,timestamp:currentGamepad.timestamp};else if(self.bxHomeStates[currentGamepad.index]){hijack=!0;let info=structuredClone(self.bxHomeStates[currentGamepad.index]);if(self.bxHomeStates[currentGamepad.index]=null,info.shortcutPressed===0){let fakeGamepadMappings=[{GamepadIndex:currentGamepad.index,A:0,B:0,X:0,Y:0,LeftShoulder:0,RightShoulder:0,LeftTrigger:0,RightTrigger:0,View:0,Menu:0,LeftThumb:0,RightThumb:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,Nexus:1,LeftThumbXAxis:0,LeftThumbYAxis:0,RightThumbXAxis:0,RightThumbYAxis:0,PhysicalPhysicality:0,VirtualPhysicality:0,Dirty:!0,Virtual:!1}];intervalMs=currentGamepad.timestamp-info.timestamp>=500?500:100,self.inputSink.onGamepadInput(performance.now()-intervalMs,fakeGamepadMappings)}else intervalMs=window.BX_STREAM_SETTINGS.controllerPollingRate}if(hijack&&intervalMs){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(intervalMs):self.pollGamepadssetTimeoutTimerID=setTimeout(self.pollGamepads,intervalMs);return}}\n"; +var expose_stream_session_default = 'var self=this;window.BX_EXPOSED.streamSession=self;var orgSetMicrophoneState=self.setMicrophoneState.bind(self);self.setMicrophoneState=(state)=>{orgSetMicrophoneState(state),window.BxEventBus.Stream.emit("microphone.state.changed",{state})};window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));var updateDimensionsStr=self.updateDimensions.toString();if(updateDimensionsStr.startsWith("function "))updateDimensionsStr=updateDimensionsStr.substring(9);var renderTargetVar=updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];updateDimensionsStr=updateDimensionsStr.replaceAll(renderTargetVar+".scroll","scroll");updateDimensionsStr=updateDimensionsStr.replace(`if(${renderTargetVar}){`,`\nif (${renderTargetVar}) {\nconst scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\nconst scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);eval(`this.updateDimensions = function ${updateDimensionsStr}`);\n'; +var game_card_icons_default = `var supportedInputIcons=$supportedInputIcons$,{productId}=$param$;supportedInputIcons.shift();if(window.BX_EXPOSED.localCoOpManager.isSupported(productId))supportedInputIcons.push(window.BX_EXPOSED.createReactLocalCoOpIcon);`; +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}}`; +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 = ` +const [logTag, logLevel, logMessage] = Array.from(arguments); +const logFunc = [console.debug, console.log, console.warn, console.error][logLevel]; +logFunc(logTag, '//', logMessage); +`; + return str = str.replaceAll(text, text + newCode), str; + }, + enableConsoleLogging(str) { + let text = "static isConsoleLoggingAllowed(){"; + if (!str.includes(text)) return !1; + return str = str.replaceAll(text, text + "return true;"), str; + }, + playVibration(str) { + let text = "}playVibration(e){"; + if (!str.includes(text)) return !1; + return str = str.replaceAll(text, text + vibration_adjust_default), str; + }, + disableGamepadDisconnectedScreen(str) { + let index = str.indexOf('"GamepadDisconnected_Title",'); + if (index < 0) return !1; + let constIndex = str.indexOf("const", index - 30); + return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str; + }, + patchUpdateInputConfigurationAsync(str) { + let text = "async updateInputConfigurationAsync(e){"; + if (!str.includes(text)) return !1; + let newCode = "e.enableTouchInput = true;"; + return str = str.replace(text, text + newCode), str; + }, + disableStreamGate(str) { + let index = str.indexOf('case"partially-ready":'); + if (index < 0) return !1; + let bracketIndex = str.indexOf("=>{", index - 150) + 3; + return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str; + }, + exposeTouchLayoutManager(str) { + let text = "this._perScopeLayoutsStream=new"; + if (!str.includes(text)) return !1; + let newCode = ` +true; +window.BX_EXPOSED["touchLayoutManager"] = this; +window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}")); +`; + return str = str.replace(text, newCode + text), str; + }, + patchBabylonRendererClass(str) { + let index = str.indexOf(".current.render(),"); + if (index < 0) return !1; + index -= 1; + let newCode = ` +if (window.BX_EXPOSED.stopTakRendering) { + try { + document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen'); + ${str[index]}.current.dispose(); + } catch (e) {} + window.BX_EXPOSED.stopTakRendering = false; + return; +} +`; + return str = str.substring(0, index) + newCode + str.substring(index), str; + }, + supportLocalCoOp(str) { + let text = "this.gamepadMappingsToSend=[],"; + if (!str.includes(text)) return !1; + let newCode = `true; ${local_co_op_enable_default}; true,`; + return str = str.replace(text, text + newCode), str; + }, + forceFortniteConsole(str) { + let text = "sendTouchInputEnabledMessage(e){"; + if (!str.includes(text)) return !1; + let newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);"; + return str = str.replace(text, text + newCode), str; + }, + disableTakRenderer(str) { + let text = "const{TakRenderer:"; + if (!str.includes(text)) return !1; + let autoOffCode = ""; + if (getGlobalPref("touchController.mode") === "off") autoOffCode = "return;"; + else if (getGlobalPref("touchController.autoOff")) autoOffCode = ` +const gamepads = window.navigator.getGamepads(); +let gamepadFound = false; +for (let gamepad of gamepads) { + if (gamepad && gamepad.connected) { + gamepadFound = true; + break; + } +} +if (gamepadFound) { + return; +} +`; + let newCode = ` +${autoOffCode} +const titleInfo = window.BX_EXPOSED.getTitleInfo(); +if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) { + return; +} +`; + return str = str.replace(text, newCode + text), str; + }, + streamCombineSources(str) { + let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str; + }, + patchStreamHud(str) { + let text = "let{onCollapse"; + if (!str.includes(text)) return !1; + let newCode = ` +// Expose onShowStreamMenu +window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; +// Restore the "..." button +e.guideUI = null; +`; + if (getGlobalPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;"; + return str = str.replace(text, newCode + text), str; + }, + broadcastPollingMode(str) { + let text = ".setPollingMode=e=>{"; + if (!str.includes(text)) return !1; + let newCode = ` +window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase(); +BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED); +`; + return str = str.replace(text, text + newCode), str; + }, + patchGamepadPolling(str) { + let index = str.indexOf(".shouldHandleGamepadInput)())return void"); + if (index < 0) return !1; + return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str; + }, + patchXcloudTitleInfo(str) { + let text = "async cloudConnect", index = str.indexOf(text); + if (index < 0) return !1; + let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; + if (!params) return !1; + let titleInfoVar = params.split(",")[0], newCode = ` +${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar}); +BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar}); +`; + return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; + }, + patchRemotePlayMkb(str) { + let text = "async homeConsoleConnect", index = str.indexOf(text); + if (index < 0) return !1; + let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; + if (!params) return !1; + let configsVar = params.split(",")[1], newCode = ` +Object.assign(${configsVar}.inputConfiguration, { + enableMouseInput: false, + enableKeyboardInput: false, + enableAbsoluteMouse: false, +}); +BxLogger.info('patchRemotePlayMkb', ${configsVar}); +`; + return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; + }, + patchAudioMediaStream(str) { + let text = ".srcObject=this.audioMediaStream,"; + if (!str.includes(text)) return !1; + let newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),"; + return str = str.replace(text, text + newCode), str; + }, + patchCombinedAudioVideoMediaStream(str) { + let text = ".srcObject=this.combinedAudioVideoStream"; + if (!str.includes(text)) return !1; + let newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)"; + return str = str.replace(text, text + newCode), str; + }, + patchTouchControlDefaultOpacity(str) { + let text = "opacityMultiplier:1"; + if (!str.includes(text)) return !1; + let newCode = `opacityMultiplier: ${(getGlobalPref("touchController.opacity.default") / 100).toFixed(1)}`; + return str = str.replace(text, newCode), str; + }, + patchShowSensorControls(str) { + let text = ",{shouldShowSensorControls:"; + if (!str.includes(text)) return !1; + let newCode = ",{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||"; + return str = str.replace(text, newCode), str; + }, + exposeStreamSession(str) { + let text = ",this._connectionType="; + if (!str.includes(text)) return !1; + let newCode = `; +${expose_stream_session_default} +true` + text; + return str = str.replace(text, newCode), str; + }, + skipFeedbackDialog(str) { + let text = "shouldTransitionToFeedback(e){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return !1;"), str; + }, + enableNativeMkb(str) { + let index = str.indexOf(".mouseSupported&&"); + if (index < 0) return !1; + let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return true;"), str; + }, + patchMouseAndKeyboardEnabled(str) { + let text = "get mouseAndKeyboardEnabled(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return true;"), str; + }, + exposeInputChannel(str) { + let index = str.indexOf("this.flushData="); + if (index < 0) return !1; + let newCode = "window.BX_EXPOSED.inputChannel = this,"; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + disableNativeRequestPointerLock(str) { + let text = "async requestPointerLock(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return;"), str; + }, + patchRequestInfoCrash(str) { + let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");'; + if (!str.includes(text)) return !1; + return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str; + }, + exposeDialogRoutes(str) { + let text = "return{goBack:function(){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str; + }, + enableTvRoutes(str) { + let index = str.indexOf(".LoginDeviceCode.path,"); + if (index < 0) return !1; + let match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100)); + if (!match) return !1; + let funcName = match[1]; + if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1; + return str = str.substring(0, index) + "|| true" + str.substring(index), str; + }, + ignoreNewsSection(str) { + let index = str.indexOf('Logger("CarouselRow")'); + if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const ", index, 200)), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "return null;"), str; + }, + ignorePlayWithFriendsSection(str) { + let index = str.indexOf('location:"PlayWithFriendsRow",'); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1; + return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str; + }, + ignoreAllGamesSection(str) { + let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer'); + if (index < 0) return !1; + if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "true ? null :"), str; + }, + ignorePlayWithTouchSection(str) { + let index = str.indexOf('("Play_With_Touch"),'); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "return null;"), str; + }, + ignoreSiglSections(str) { + let index = str.indexOf("SiglRow-module__heroCard___"); + if (index < 0) return !1; + if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1; + let PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), siglIds = [], sections = { + "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", + "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" + }; + for (let section of PREF_HIDE_SECTIONS) { + let galleryId = sections[section]; + galleryId && siglIds.push(galleryId); + } + let newCode = ` +if (e && e.id) { + const siglId = e.id; + if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) { + return null; + } +} +`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + overrideStorageGetSettings(str) { + let text = "}getSetting(e){"; + if (!str.includes(text)) return !1; + let newCode = ` +// console.log('setting', this.baseStorageKey, e); +if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { + const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey]; + if (e in settings) { + return settings[e]; + } +} +`; + return str = str.replace(text, text + newCode), str; + }, + alwaysShowStreamHud(str) { + let index = str.indexOf(",{onShowStreamMenu:"); + if (index < 0) return !1; + if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1; + let commaIndex = str.indexOf(",", index - 10); + return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str; + }, + patchSetCurrentFocus(str) { + let index = str.indexOf(".setCurrentFocus=("); + if (index < 0) return !1; + return index = str.indexOf("{", index) + 1, str = PatcherUtils.insertAt(str, index, "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });"), str; + }, + detectProductDetailPage(str) { + let index = str.indexOf('{location:"ProductDetailPage",'); + if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; + return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str; + }, + detectBrowserRouterReady(str) { + let index = str.indexOf("{history:this.history,"); + if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 100)), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str; + }, + guideAchievementsDefaultLocked(str) { + let index = str.indexOf("FilterButton-module__container"); + if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1; + if (str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250)), index < 0) return !1; + return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str; + }, + disableTouchContextMenu(str) { + let index = str.indexOf('"ContextualCardActions-module__container'); + if (index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index)), index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return ", index, 50)), index < 0) return !1; + return str = PatcherUtils.replaceWith(str, index, "return", "return () => {};"), str; + }, + optimizeGameSlugGenerator(str) { + let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "window.BX_EXPOSED.GameSlugRegexes[0]"), str = str.replace("/ {2,}/g", "window.BX_EXPOSED.GameSlugRegexes[1]"), str = str.replace("/ /g", "window.BX_EXPOSED.GameSlugRegexes[2]"), str; + }, + modifyPreloadedState(str) { + let text = "=window.__PRELOADED_STATE__;"; + if (!str.includes(text)) return !1; + return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; + }, + homePageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "home"); + }, + productDetailPageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "product-detail"); + }, + streamPageBeforeLoad(str) { + return PatcherUtils.patchBeforePageLoad(str, "stream"); + }, + disableAbsoluteMouse(str) { + let text = "sendAbsoluteMouseCapableMessage(e){"; + if (!str.includes(text)) return !1; + return str = str.replace(text, text + "return;"), str; + }, + changeNotificationsSubscription(str) { + let text = ";buildSubscriptionQueryParamsForNotifications(", index = str.indexOf(text); + if (index < 0) return !1; + index += text.length; + let subsVar = str[index]; + index = str.indexOf("{", index) + 1; + let blockFeatures = getGlobalPref("block.features"), filters = []; + if (blockFeatures.includes("notifications-invites")) filters.push("GameInvite", "PartyInvite"); + if (blockFeatures.includes("friends")) filters.push("Follower"); + if (blockFeatures.includes("notifications-achievements")) filters.push("AchievementUnlock"); + let newCode = ` +let subs = ${subsVar}; +subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); +${subsVar} = subs; +`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + exposeReactCreateComponent(str) { + let index = str.indexOf(".prototype.isReactComponent={}"); + if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1; + let newCode = "window.BX_EXPOSED.reactCreateElement="; + return str = PatcherUtils.insertAt(str, index - 1, newCode), str; + }, + gameCardCustomIcons(str) { + let initialIndex = str.indexOf("const{supportedInputIcons:"); + if (initialIndex < 0) return !1; + let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge")); + if (returnIndex < 0) return !1; + let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300); + if (arrowIndex < 0) return !1; + let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0)); + if (!paramVar || !supportedInputIconsVar) return !1; + let newCode = renderString(game_card_icons_default, { + param: paramVar, + supportedInputIcons: supportedInputIconsVar + }); + return str = PatcherUtils.insertAt(str, returnIndex, newCode), str; + }, + setImageQuality(str) { + let index = str.indexOf("const{size:{width:"); + if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1; + let paramVar = PatcherUtils.getVariableNameBefore(str, index); + if (!paramVar) return !1; + index = PatcherUtils.indexOf(str, "return", index, 200); + let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + setBackgroundImageQuality(str) { + let index = str.indexOf("}?w=${"); + if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1; + return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str; + } +}, PATCH_ORDERS = PatcherUtils.filterPatches([ + ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [ + "enableNativeMkb", + "disableAbsoluteMouse" + ] : [], + "exposeReactCreateComponent", + "gameCardCustomIcons", + ...getGlobalPref("ui.imageQuality") < 90 ? [ + "setImageQuality" + ] : [], + "modifyPreloadedState", + "optimizeGameSlugGenerator", + "detectBrowserRouterReady", + "patchRequestInfoCrash", + "disableStreamGate", + "broadcastPollingMode", + "patchGamepadPolling", + "exposeStreamSession", + "exposeDialogRoutes", + "homePageBeforeLoad", + "productDetailPageBeforeLoad", + "streamPageBeforeLoad", + "guideAchievementsDefaultLocked", + "enableTvRoutes", + "supportLocalCoOp", + "overrideStorageGetSettings", + getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus", + getGlobalPref("ui.layout") !== "default" && "websiteLayout", + getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole", + ...STATES.userAgent.capabilities.touch ? [ + "disableTouchContextMenu" + ] : [], + ...getGlobalPref("block.tracking") ? [ + "disableAiTrack", + "disableTelemetry", + "blockWebRtcStatsCollector", + "disableIndexDbLogging", + "disableTelemetryProvider" + ] : [], + ...getGlobalPref("xhome.enabled") ? [ + "remotePlayKeepAlive", + "remotePlayDirectConnectUrl", + "remotePlayDisableAchievementToast", + "remotePlayRecentlyUsedTitleIds", + "remotePlayWebTitle", + STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync" + ] : [], + ...BX_FLAGS.EnableXcloudLogging ? [ + "enableConsoleLogging", + "enableXcloudLogger" + ] : [] +]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ + hideSections.includes("news") && "ignoreNewsSection", + hideSections.includes("friends") && "ignorePlayWithFriendsSection", + hideSections.includes("all-games") && "ignoreAllGamesSection", + 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" +]), 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; + 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, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS); + } + if (modified) { + 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(); + } +} +class PatcherCache { + static instance; + static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache); + KEY_CACHE = "BetterXcloud.Patches.Cache"; + KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature"; + CACHE; + constructor() { + this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE); + let pathName = window.location.pathname; + if (pathName.includes("/play/launch/")) Patcher.patchPage("stream"); + else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail"); + else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home"); + PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0)); + } + getSignature() { + let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]'); + if ($link) { + let match = /\/client\.([^\.]+)\.js/.exec($link.href); + match && (webVersion = match[1]); + } else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? ""; + return hashCode(scriptVersion + webVersion + patches); + } + clear() { + window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {}; + } + checkSignature() { + let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature(); + if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear(); + else BxLogger.info(LOG_TAG2, "Signature unchanged"); + } + cleanupPatches(patches) { + return patches.filter((item2) => { + for (let id in this.CACHE) + if (this.CACHE[id].includes(item2)) return !1; + return !0; + }); + } + getPatches(id) { + return this.CACHE[id]; + } + saveToCache(subCache) { + for (let id in subCache) { + let patchNames = subCache[id], data = this.CACHE[id]; + if (!data) this.CACHE[id] = patchNames; + else for (let patchName of patchNames) + if (!data.includes(patchName)) data.push(patchName); + } + window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); + } +} +class FullscreenText { + static instance; + static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); + LOG_TAG = "FullscreenText"; + $text; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", { + class: "bx-fullscreen-text bx-gone" + }), document.documentElement.appendChild(this.$text); + } + show(msg) { + document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg; + } + hide() { + document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone"); + } +} +class BaseProfileManagerDialog extends NavigationDialog { + $container; + title; + presetsDb; + allPresets; + currentPresetId = null; + activatedPresetId = null; + $presets; + $header; + $defaultNote; + $content; + $btnRename; + $btnDelete; + constructor(title, presetsDb) { + super(); + this.title = title, this.presetsDb = presetsDb; + } + async renderSummary(presetId) { + return null; + } + updateButtonStates() { + let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0; + this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset); + } + async renderPresetsList() { + if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0]; + renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 }); + } + promptNewName(action, value = "") { + let newName = ""; + while (!newName) { + if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1; + newName = newName.trim(); + } + return newName ? newName : !1; + } + async renderDialog() { + this.$presets = CE("select", { + class: "bx-full-width", + tabindex: -1 + }); + let $select = BxSelectElement.create(this.$presets); + $select.addEventListener("input", (e) => { + this.switchPreset(parseInt($select.value)); + }); + let $header = CE("div", { + class: "bx-dialog-preset-tools", + _nearby: { + orientation: "horizontal", + focus: $select + } + }, $select, this.$btnRename = createButton({ + title: t("rename"), + icon: BxIcon.CURSOR_TEXT, + style: 64, + onClick: async () => { + let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name); + if (!newName) return; + preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh(); + } + }), this.$btnDelete = createButton({ + icon: BxIcon.TRASH, + title: t("delete"), + style: 4 | 64, + onClick: async (e) => { + if (!confirm(t("confirm-delete-preset"))) return; + await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh(); + } + }), createButton({ + icon: BxIcon.NEW, + title: t("new"), + style: 64 | 1, + onClick: async (e) => { + let newName = this.promptNewName(t("new")); + if (!newName) return; + let newId = await this.presetsDb.newPreset(newName, this.presetsDb.BLANK_PRESET_DATA); + this.currentPresetId = newId, await this.refresh(); + } + }), createButton({ + icon: BxIcon.COPY, + title: t("copy"), + style: 64 | 1, + onClick: async (e) => { + let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`); + if (!newName) return; + let newId = await this.presetsDb.newPreset(newName, preset.data); + this.currentPresetId = newId, await this.refresh(); + } + })); + this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, this.title), createButton({ + icon: BxIcon.CLOSE, + style: 64 | 2048 | 8, + onClick: (e) => this.hide() + })), CE("div", !1, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content)); + } + async refresh() { + await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 }); + } + async onBeforeMount(configs = {}) { + await this.renderPresetsList(); + let valid = !1; + if (typeof configs?.id === "number") { + if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0; + } + if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null; + this.refresh(); + } + getDialog() { + return this; + } + getContent() { + if (!this.$container) this.renderDialog(); + return this.$container; + } + focusIfNeeded() { + this.dialogManager.focus(this.$header); + } +} +var SHORTCUT_ACTIONS = { + [t("better-xcloud")]: { + "bx.settings.show": [t("settings"), t("show")] + }, + ...STATES.browser.capabilities.mkb ? { + [t("mouse-and-keyboard")]: { + "mkb.toggle": [t("toggle")] + } + } : {}, + [t("controller")]: { + "controller.xbox.press": [t("button-xbox"), t("press")] + }, + ...AppInterface ? { + [t("device")]: { + "device.sound.toggle": [t("sound"), t("toggle")], + "device.volume.inc": [t("volume"), t("increase")], + "device.volume.dec": [t("volume"), t("decrease")], + "device.brightness.inc": [t("brightness"), t("increase")], + "device.brightness.dec": [t("brightness"), t("decrease")] + } + } : {}, + [t("stream")]: { + "stream.screenshot.capture": [t("take-screenshot")], + "stream.video.toggle": [t("video"), t("toggle")], + "stream.sound.toggle": [t("sound"), t("toggle")], + ...getGlobalPref("audio.volume.booster.enabled") ? { + "stream.volume.inc": [t("volume"), t("increase")], + "stream.volume.dec": [t("volume"), t("decrease")] + } : {}, + "stream.menu.show": [t("menu"), t("show")], + "stream.stats.toggle": [t("stats"), t("show-hide")], + "stream.microphone.toggle": [t("microphone"), t("toggle")] + }, + [t("other")]: { + "ta.open": [t("true-achievements"), t("show")] + } +}; +class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts"))); + $content; + selectActions = {}; + BUTTONS_ORDER = [ + 3, + 0, + 2, + 1, + 12, + 13, + 14, + 15, + 8, + 9, + 4, + 5, + 6, + 7, + 10, + 11 + ]; + constructor(title) { + super(title, ControllerShortcutsTable.getInstance()); + let $baseSelect = CE("select", { + class: "bx-full-width", + autocomplete: "off" + }, CE("option", { value: "" }, "---")); + for (let groupLabel in SHORTCUT_ACTIONS) { + let items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) continue; + let $optGroup = CE("optgroup", { label: groupLabel }); + for (let action in items) { + let crumbs = items[action]; + if (!crumbs) continue; + let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label); + $optGroup.appendChild($option); + } + $baseSelect.appendChild($optGroup); + } + let $content = CE("div", { + class: "bx-controller-shortcuts-manager-container" + }), onActionChanged = (e) => { + if (!e.ignoreOnChange) this.updatePreset(); + }, fragment = document.createDocumentFragment(); + fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note"))); + for (let button of this.BUTTONS_ORDER) { + let prompt2 = GamepadKeyName[button][1], $row = CE("div", { + class: "bx-shortcut-row", + _nearby: { + orientation: "horizontal" + } + }), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0)); + $select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, { + focus: $select + }), $row.append($label, $select), fragment.appendChild($row); + } + $content.appendChild(fragment), this.$content = $content; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + this.currentPresetId = id; + let isDefaultPreset = id <= 0, actions = preset.data, button; + for (button in this.selectActions) { + let $select = this.selectActions[button]; + $select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { + ignoreOnChange: !0, + manualTrigger: !0 + }); + } + super.updateButtonStates(); + } + updatePreset() { + let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), button; + for (button in this.selectActions) { + let action = this.selectActions[button].value; + if (!action) continue; + newData.mapping[button] = action; + } + let preset = this.allPresets.data[this.currentPresetId]; + preset.data = newData, this.presetsDb.updatePreset(preset); + } + onBeforeUnmount() { + StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); + } +} +class BxDualNumberStepper extends HTMLInputElement { + controlValues; + controlMin; + controlMinDiff; + controlMax; + steps; + options; + onChange; + $text; + $rangeFrom; + $rangeTo; + $activeRange; + onRangeInput; + setValue; + getValue; + normalizeValue; + static create(key, values, options, onChange) { + options.suffix = options.suffix || "", options.disabled = !!options.disabled; + let $text, $rangeFrom, $rangeTo, self = CE("div", { + class: "bx-dual-number-stepper", + id: `bx_setting_${escapeCssSelector(key)}` + }, $text = CE("span")); + if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self; + return $rangeFrom = CE("input", { + type: "range", + min: self.controlMin, + max: self.controlMax, + step: self.steps, + tabindex: 0 + }), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", !1, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, { + focus: $rangeFrom, + orientation: "vertical" + }), Object.defineProperty(self, "value", { + get() { + return self.controlValues; + }, + set(value) { + let from, to; + if (typeof value === "string") { + let tmp = value.split(","); + from = parseInt(tmp[0]), to = parseInt(tmp[1]); + } else if (Array.isArray(value)) [from, to] = value; + if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]); + } + }), self; + } + static setValues(values) { + let from, to; + if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values); + else from = this.controlMin, to = this.controlMax, values = [from, to]; + this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString(); + let ratio = 100 / (this.controlMax - this.controlMin); + this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%"); + } + static getValues() { + return this.controlValues || [this.controlMin, this.controlMax]; + } + static normalizeValues(values) { + let [from, to] = values; + if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from); + else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to); + return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to]; + } + static onRangeInput(e) { + this.$activeRange = e.target; + let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]); + if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values); + } + static onContextMenu(e) { + e.preventDefault(); + } + static updateTextValue() { + let values = this.controlValues, textContent = null; + if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax); + if (textContent === null) { + let [from, to] = values; + if (from === this.controlMin && to === this.controlMax) textContent = t("default"); + else { + let pad = to.toString().length; + textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`; + } + } + return textContent; + } +} +class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization"))); + $vibrationIntensity; + $leftTriggerRange; + $rightTriggerRange; + $leftStickDeadzone; + $rightStickDeadzone; + $btnDetect; + selectsMap = {}; + selectsOrder = []; + isDetectingButton = !1; + detectIntervalId = null; + static BUTTONS_ORDER = [ + 0, + 1, + 2, + 3, + 12, + 15, + 13, + 14, + 4, + 5, + 6, + 7, + 10, + 11, + 104, + 204, + 8, + 9, + 17 + ]; + constructor(title) { + super(title, ControllerCustomizationsTable.getInstance()); + this.render(); + } + render() { + let isControllerFriendly = getGlobalPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => { + if (!e.ignoreOnChange) this.updatePreset(); + }, boundUpdatePreset = this.updatePreset.bind(this); + for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { + if (gamepadKey === 17) continue; + let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0]; + (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", { + value: gamepadKey, + _dataset: { label: GamepadKeyName[gamepadKey][1] } + }, name)); + } + for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { + let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0); + $clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove(); + let $select = BxSelectElement.create($clonedSelect); + $select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey); + let $row = CE("div", { + class: "bx-controller-key-row", + _nearby: { orientation: "horizontal" } + }, CE("label", { title: buttonName }, buttonPrompt), $select); + $rows.append($row); + } + if (getGlobalPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) { + let $select = this.selectsMap[this.selectsOrder[i]], directions = { + 1: i - 2, + 3: i + 2, + 4: i - 1, + 2: i + 1 + }; + for (let dir in directions) { + let idx = directions[dir]; + if (typeof this.selectsOrder[idx] === "undefined") continue; + let $targetSelect = this.selectsMap[this.selectsOrder[idx]]; + setNearby($select, { + [dir]: $targetSelect + }); + } + } + let blankSettings = this.presetsDb.BLANK_PRESET_DATA.settings, params = { + min: 0, + minDiff: 1, + max: 100, + steps: 1 + }; + this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({ + label: t("detect-controller-button"), + classes: ["bx-btn-detect"], + style: 4096 | 64 | 128, + onClick: () => { + this.startDetectingButton(); + } + }), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, { + steps: 10, + suffix: "%", + exactTicks: 20, + customTextValue: (value) => { + return value = parseInt(value), value === 0 ? t("off") : value + "%"; + } + }, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", blankSettings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", blankSettings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", blankSettings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", blankSettings.rightStickDeadzone, params, boundUpdatePreset))); + } + startDetectingButton() { + this.isDetectingButton = !0; + let { $btnDetect } = this; + $btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0; + let count = 4; + $btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => { + if (count -= 1, count === 0) { + this.stopDetectingButton(), $btnDetect.focus(); + return; + } + $btnDetect.textContent = `[${count}] ${t("press-any-button")}`; + }, 1000); + } + stopDetectingButton() { + let { $btnDetect } = this; + $btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null; + } + async onBeforeMount() { + this.stopDetectingButton(), super.onBeforeMount(...arguments); + } + onBeforeUnmount() { + this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); + } + handleGamepad(button) { + if (!this.isDetectingButton) return super.handleGamepad(button); + if (button in ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { + this.stopDetectingButton(); + let $select = this.selectsMap[button], $label = $select.previousElementSibling; + if ($label.addEventListener("animationend", () => { + $label.classList.remove("bx-horizontal-shaking"); + }, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getGlobalPref("ui.controllerFriendly")) + this.dialogManager.focus($select); + } + return !0; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let { + $btnDetect, + $vibrationIntensity, + $leftStickDeadzone, + $rightStickDeadzone, + $leftTriggerRange, + $rightTriggerRange, + selectsMap + } = this, presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id <= 0; + this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset); + let buttonIndex; + for (buttonIndex in selectsMap) { + buttonIndex = buttonIndex; + let $select = selectsMap[buttonIndex]; + if (!$select) continue; + let mappedButton = presetData.mapping[buttonIndex]; + $select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { + ignoreOnChange: !0, + manualTrigger: !0 + }); + } + presetData.settings = Object.assign({}, this.presetsDb.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange); + } + updatePreset() { + let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), gamepadKey; + for (gamepadKey in this.selectsMap) { + let value = this.selectsMap[gamepadKey].value; + if (!value) continue; + let mapTo = value === "false" ? !1 : parseInt(value); + newData.mapping[gamepadKey] = mapTo; + } + Object.assign(newData.settings, { + vibrationIntensity: parseInt(this.$vibrationIntensity.value), + leftStickDeadzone: this.$leftStickDeadzone.getValue(), + rightStickDeadzone: this.$rightStickDeadzone.getValue(), + leftTriggerRange: this.$leftTriggerRange.getValue(), + rightTriggerRange: this.$rightTriggerRange.getValue() + }); + let preset = this.allPresets.data[this.currentPresetId]; + preset.data = newData, this.presetsDb.updatePreset(preset); + } + async renderSummary(presetId) { + let preset = await this.presetsDb.getPreset(presetId); + if (!preset) return null; + let presetData = preset.data, $content, showNote = !1; + if (Object.keys(presetData.mapping).length > 0) { + $content = CE("div", { class: "bx-controller-customization-summary" }); + for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { + if (!(gamepadKey in presetData.mapping)) continue; + let mappedKey = presetData.mapping[gamepadKey]; + $content.append(CE("span", { class: "bx-prompt" }, getGamepadPrompt(gamepadKey) + " > " + (mappedKey === !1 ? "🚫" : getGamepadPrompt(mappedKey)))); + } + showNote = !0; + } + let key; + for (key in presetData.settings) { + if (key === "vibrationIntensity") continue; + let value = presetData.settings[key]; + if (Array.isArray(value) && (value[0] !== 0 || value[1] !== 100)) { + showNote = !0; + break; + } + } + let fragment = document.createDocumentFragment(); + if (showNote) { + let $note = CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("controller-customization-input-latency-note")); + fragment.appendChild($note); + } + if ($content) fragment.appendChild($content); + return fragment.childElementCount ? fragment : null; + } +} +class ControllerExtraSettings extends HTMLElement { + currentControllerId; + controllerIds; + $selectControllers; + $selectShortcuts; + $selectCustomization; + $summaryCustomization; + updateLayout; + switchController; + getCurrentControllerId; + saveSettings; + updateCustomizationSummary; + setValue; + static renderSettings() { + let $container = CE("label", { + class: "bx-settings-row bx-controller-extra-settings" + }); + $container.prefKey = "controller.settings", $container.addEventListener("contextmenu", this.boundOnContextMenu), this.settingsManager.setElement("controller.settings", $container), $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container), $container.setValue = ControllerExtraSettings.setValue.bind($container); + let $selectControllers = BxSelectElement.create(CE("select", { + class: "bx-full-width", + autocomplete: "off", + _on: { + input: (e) => { + $container.switchController($selectControllers.value); + } + } + })), $selectShortcuts = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { input: $container.saveSettings } + })), $selectCustomization = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: async () => { + ControllerExtraSettings.updateCustomizationSummary.call($container), $container.saveSettings(); + } + } + })), $rowCustomization = createSettingRow(t("in-game-controller-customization"), CE("div", { + class: "bx-preset-row", + _nearby: { orientation: "horizontal" } + }, $selectCustomization, createButton({ + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, + onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({ + id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null + }) + })), { multiLines: !0 }); + return $rowCustomization.appendChild($container.$summaryCustomization = CE("div")), $container.append(CE("span", !1, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", { + class: "bx-preset-row", + _nearby: { orientation: "horizontal" } + }, $selectShortcuts, createButton({ + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, + onClick: () => ControllerShortcutsManagerDialog.getInstance().show({ + id: parseInt($container.$selectShortcuts.value) + }) + })), { multiLines: !0 }), $rowCustomization))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => { + $container.updateLayout(); + }), $container; + } + static async updateCustomizationSummary() { + let presetId = parseInt(this.$selectCustomization.value), $summaryContent = await ControllerCustomizationsManagerDialog.getInstance().renderSummary(presetId); + if (removeChildElements(this.$summaryCustomization), $summaryContent) this.$summaryCustomization.appendChild($summaryContent); + } + static async updateLayout() { + if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return; + let $fragment = document.createDocumentFragment(); + removeChildElements(this.$selectControllers); + for (let name of this.controllerIds) { + let $option = CE("option", { value: name }, simplifyGamepadName(name)); + $fragment.appendChild($option); + } + this.$selectControllers.appendChild($fragment); + let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets(); + renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 }); + let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets(); + renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 }); + for (let name of this.controllerIds) { + let $option = CE("option", { value: name }, name); + $fragment.appendChild($option); + } + BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this); + } + static async switchController(id) { + if (this.currentControllerId = id, !this.getCurrentControllerId()) return; + let controllerSetting = STORAGE.Stream.getControllerSetting(this.currentControllerId); + ControllerExtraSettings.updateElements.call(this, controllerSetting); + } + static getCurrentControllerId() { + if (this.currentControllerId) { + if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId; + this.currentControllerId = ""; + } + if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0]; + if (this.currentControllerId) return this.currentControllerId; + return null; + } + static async saveSettings() { + if (!this.getCurrentControllerId()) return; + let controllerSettings = getStreamPref("controller.settings"); + controllerSettings[this.currentControllerId] = { + shortcutPresetId: parseInt(this.$selectShortcuts.value), + customizationPresetId: parseInt(this.$selectCustomization.value) + }, setStreamPref("controller.settings", controllerSettings, "ui"), StreamSettings.refreshControllerSettings(); + } + static setValue(value) { + ControllerExtraSettings.updateElements.call(this, value[this.currentControllerId]); + } + static updateElements(controllerSetting) { + if (!controllerSetting) return; + this.$selectShortcuts.value = controllerSetting.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSetting.customizationPresetId.toString(), ControllerExtraSettings.updateCustomizationSummary.call(this); + } +} +class SuggestionsSetting { + static async renderSuggestions(e) { + let $btnSuggest = e.target.closest("div"); + $btnSuggest.toggleAttribute("bx-open"); + let $content = $btnSuggest.nextElementSibling; + if ($content) { + BxEvent.dispatch($content.querySelector("select"), "input"); + return; + } + let settingTabGroup; + for (settingTabGroup in this.SETTINGS_UI) { + let settingTab = this.SETTINGS_UI[settingTabGroup]; + if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; + for (let settingTabContent of settingTab.items) { + if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue; + for (let setting of settingTabContent.items) { + let prefKey; + if (typeof setting === "string") prefKey = setting; + else if (typeof setting === "object") prefKey = setting.pref; + if (prefKey) this.settingLabels[prefKey] = settingTabContent.label; + } + } + } + let recommendedDevice = ""; + if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { + if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); + } + let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; + if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on"); + else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto"); + else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"); + SuggestionsSetting.generateDefaultSuggestedSettings.call(this); + let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); + $select.addEventListener("input", (e2) => { + let profile = $select.value; + removeChildElements($suggestedSettings); + let fragment = document.createDocumentFragment(), note; + if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); + else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); + note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); + let settings = this.suggestedSettings[profile]; + for (let key in settings) { + let { storage, definition } = getPrefInfo(key), prefKey; + if (storage === STORAGE.Stream) prefKey = key; + else prefKey = key; + let suggestedValue; + if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]); + else suggestedValue = settings[prefKey]; + let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value; + if (isSameValue) $value = currentValueText; + else { + let suggestedValueText = storage.getValueText(prefKey, suggestedValue); + $value = currentValueText + " ➔ " + suggestedValueText; + } + let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`); + if ($child = CE("div", { + class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` + }, $checkbox = CE("input", { + type: "checkbox", + tabindex: 0, + checked: !0, + id + }), CE("label", { + for: id + }, CE("div", { + class: "bx-suggest-label" + }, breadcrumb), CE("div", { + class: "bx-suggest-value" + }, $value))), isSameValue) + $checkbox.disabled = !0, $checkbox.checked = !0; + fragment.appendChild($child); + } + $suggestedSettings.appendChild(fragment); + }), BxEvent.dispatch($select, "input"); + let onClickApply = () => { + let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance(); + for (prefKey in settings) { + let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`); + if (!$checkBox.checked || $checkBox.disabled) continue; + let $control = settingsManager.getElement(prefKey); + if (!$control) { + setPref(prefKey, suggestedValue, "direct"); + continue; + } + let { definition: settingDefinition } = getPrefInfo(prefKey); + if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue); + if ("setValue" in $control) $control.setValue(suggestedValue); + else $control.value = suggestedValue; + BxEvent.dispatch($control, "input", { + manualTrigger: !0 + }); + } + BxEvent.dispatch($select, "input"); + }, $btnApply = createButton({ + label: t("apply"), + style: 128 | 64, + onClick: onClickApply + }); + $content = CE("div", { + class: "bx-sub-content-box bx-suggest-box", + _nearby: { + orientation: "vertical" + } + }, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { + class: "bx-suggest-link bx-focusable", + href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", + target: "_blank", + tabindex: 0 + }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { + class: "bx-suggest-link bx-focusable", + href: "https://github.com/redphx/better-xcloud-devices", + target: "_blank", + tabindex: 0 + }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); + } + static async getRecommendedSettings(androidInfo) { + function normalize(str) { + return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); + } + try { + let { brand, board, model } = androidInfo; + brand = normalize(brand), board = normalize(board), model = normalize(model); + let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; + if (json.schema_version !== 2) return null; + let scriptSettings = json.settings.script; + if (scriptSettings._base) { + let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; + for (let profile of base) + Object.assign(recommended, this.suggestedSettings[profile]); + delete scriptSettings._base; + } + let key; + for (key in scriptSettings) + recommended[key] = scriptSettings[key]; + return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; + } catch (e) {} + return null; + } + static addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + static generateDefaultSuggestedSettings() { + let key; + for (key in this.suggestedSettings) { + if (key === "default") continue; + let prefKey; + for (prefKey in this.suggestedSettings[key]) + if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default; + } + } +} +class BxKeyBindingButton extends HTMLButtonElement { + title; + isPrompt = !1; + allowedFlags; + keyInfo = null; + bindKey; + unbindKey; + static create(options) { + let $btn = CE("button", { + class: "bx-binding-button bx-focusable", + type: "button" + }); + return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn; + } + static onClick(e) { + KeyBindingDialog.getInstance().show({ + $elm: this + }); + } + static onContextMenu = (e) => { + e.preventDefault(); + let $btn = e.target; + if (!$btn.disabled) $btn.unbindKey.apply($btn); + }; + static bindKey(key, force = !1) { + if (!key) return; + if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) { + if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change"); + } + } + static unbindKey(force = !1) { + this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change"); + } + constructor() { + super(); + } +} +class KeyBindingDialog { + static instance; + static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog); + $dialog; + $wait; + $title; + $inputList; + $overlay; + $currentElm; + countdownIntervalId; + constructor() { + this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", !1, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", !1, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", !1, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); + } + show(options) { + this.$currentElm = options.$elm, this.addEventListeners(); + let allowedFlags = this.$currentElm.allowedFlags; + this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown(); + } + startCountdown() { + this.stopCountdown(); + let count = 9; + this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => { + if (count -= 1, count === 0) { + this.stopCountdown(), this.hide(); + return; + } + this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`; + }, 1000); + } + stopCountdown() { + this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null; + } + hide = () => { + this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"); + }; + addEventListeners() { + let allowedFlags = this.$currentElm.allowedFlags; + if (allowedFlags.includes(1)) window.addEventListener("keyup", this); + if (allowedFlags.includes(4)) window.addEventListener("mousedown", this); + if (allowedFlags.includes(8)) window.addEventListener("wheel", this); + } + clearEventListeners() { + window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this); + } + handleEvent(e) { + let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1; + switch (e.type) { + case "wheel": + if (handled = !0, allowedFlags.includes(8)) valid = !0; + break; + case "mousedown": + if (handled = !0, allowedFlags.includes(4)) valid = !0; + break; + case "keyup": + if (handled = !0, allowedFlags.includes(1)) { + let keyboardEvent = e; + if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) { + let key = keyboardEvent.key; + valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid; + } + } + break; + } + if (handled) { + if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown(); + else this.startCountdown(); + window.setTimeout(this.hide, 200); + } + } +} +class MkbMappingManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller"))); + KEYS_PER_BUTTON = 2; + BUTTONS_ORDER = [ + 16, + 12, + 13, + 14, + 15, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 100, + 101, + 102, + 103, + 11, + 200, + 201, + 202, + 203 + ]; + allKeyElements = []; + $mouseMapTo; + $mouseSensitivityX; + $mouseSensitivityY; + $mouseDeadzone; + $unbindNote; + constructor(title) { + super(title, MkbMappingPresetsTable.getInstance()); + this.render(); + } + onBindingKey = (e) => { + if (e.target.disabled) return; + if (e.button !== 0) return; + }; + parseDataset($btn) { + let dataset = $btn.dataset; + return { + keySlot: parseInt(dataset.keySlot), + buttonIndex: parseInt(dataset.buttonIndex) + }; + } + onKeyChanged = (e) => { + let $current = e.target, keyInfo = $current.keyInfo; + if (keyInfo) { + for (let $elm of this.allKeyElements) + if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0); + } + this.savePreset(); + }; + render() { + let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))); + for (let buttonIndex of this.BUTTONS_ORDER) { + let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment(); + for (let i = 0;i < this.KEYS_PER_BUTTON; i++) + $elm = BxKeyBindingButton.create({ + title: buttonPrompt, + isPrompt: !0, + allowedFlags: [1, 4, 8], + onChanged: this.onKeyChanged + }), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm); + let $keyRow = CE("div", { + class: "bx-mkb-key-row", + _nearby: { orientation: "horizontal" } + }, CE("label", { title: buttonName }, buttonPrompt), $fragment); + $rows.appendChild($keyRow); + } + let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, { + suffix: "%", + exactTicks: 50 + }, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, { + suffix: "%", + exactTicks: 50 + }, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, { + suffix: "%", + exactTicks: 10 + }, savePreset))); + this.$content = CE("div", !1, $rows, $extraSettings); + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id <= 0; + this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset); + for (let $elm of this.allKeyElements) { + let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({ + code: buttonKeys[keySlot] + }, !0); + else $elm.unbindKey(!0); + $elm.disabled = isDefaultPreset; + } + let mouse = presetData.mouse; + this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString(); + } + savePreset() { + let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA); + for (let $elm of this.allKeyElements) { + let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping; + if (!mapping[buttonIndex]) mapping[buttonIndex] = []; + if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot]; + else mapping[buttonIndex][keySlot] = $elm.keyInfo.code; + } + let mouse = presetData.mouse; + mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value); + let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData + }; + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; + } + onBeforeUnmount() { + StreamSettings.refreshMkbSettings(), super.onBeforeUnmount(); + } +} +class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { + static instance; + static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts"))); + $content; + $unbindNote; + allKeyElements = []; + constructor(title) { + super(title, KeyboardShortcutsTable.getInstance()); + let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" }); + for (let groupLabel in SHORTCUT_ACTIONS) { + let items = SHORTCUT_ACTIONS[groupLabel]; + if (!items) continue; + let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel)); + for (let action in items) { + let crumbs = items[action]; + if (!crumbs) continue; + let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({ + title: label, + isPrompt: !1, + onChanged: this.onKeyChanged, + allowedFlags: [1, 2] + }); + $btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn); + let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn)); + $fieldSet.appendChild($row); + } + if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet); + } + this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows); + } + onKeyChanged = (e) => { + let $current = e.target, keyInfo = $current.keyInfo; + if (keyInfo) for (let $elm of this.allKeyElements) { + if ($elm === $current) continue; + if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0); + } + this.savePreset(); + }; + parseDataset($btn) { + return { + action: $btn.dataset.action + }; + } + switchPreset(id) { + let preset = this.allPresets.data[id]; + if (!preset) { + this.currentPresetId = 0; + return; + } + let presetData = preset.data; + this.currentPresetId = id; + let isDefaultPreset = id <= 0; + this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset); + for (let $elm of this.allKeyElements) { + let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action]; + if (keyInfo) $elm.bindKey(keyInfo, !0); + else $elm.unbindKey(!0); + $elm.disabled = isDefaultPreset; + } + } + savePreset() { + let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA); + for (let $elm of this.allKeyElements) { + let { action } = this.parseDataset($elm), mapping = presetData.mapping; + if ($elm.keyInfo) mapping[action] = $elm.keyInfo; + } + let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { + id: this.currentPresetId, + name: oldPreset.name, + data: presetData + }; + this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; + } + onBeforeUnmount() { + StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount(); + } +} +class MkbExtraSettings extends HTMLElement { + $mappingPresets; + $shortcutsPresets; + updateLayout; + saveMkbSettings; + saveShortcutsSettings; + static renderSettings() { + let $container = document.createDocumentFragment(); + $container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container); + let $mappingPresets = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: $container.saveMkbSettings + } + })), $shortcutsPresets = BxSelectElement.create(CE("select", { + autocomplete: "off", + _on: { + input: $container.saveShortcutsSettings + } + })); + return $container.append(...getGlobalPref("mkb.enabled") ? [ + createSettingRow(t("virtual-controller"), CE("div", { + class: "bx-preset-row", + _nearby: { + orientation: "horizontal" + } + }, $mappingPresets, createButton({ + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, + onClick: () => MkbMappingManagerDialog.getInstance().show({ + id: parseInt($container.$mappingPresets.value) + }) + })), { + multiLines: !0, + onContextMenu: this.boundOnContextMenu, + pref: "mkb.p1.preset.mappingId" + }), + createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), { + onContextMenu: this.boundOnContextMenu, + pref: "mkb.p1.slot" + }) + ] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", { + class: "bx-preset-row", + _nearby: { + orientation: "horizontal" + } + }, $shortcutsPresets, createButton({ + title: t("manage"), + icon: BxIcon.MANAGE, + style: 64 | 1 | 512, + onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({ + id: parseInt($container.$shortcutsPresets.value) + }) + })), { + multiLines: !0, + onContextMenu: this.boundOnContextMenu, + pref: "keyboardShortcuts.preset.inGameId" + })), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => { + $container.updateLayout(); + }), $container; + } + static async updateLayout() { + let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets(); + renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId")); + let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets(); + renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 }); + } + static async saveMkbSettings() { + let presetId = parseInt(this.$mappingPresets.value); + setStreamPref("mkb.p1.preset.mappingId", presetId, "ui"); + } + static async saveShortcutsSettings() { + let presetId = parseInt(this.$shortcutsPresets.value); + setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui"); + } +} +class SettingsDialog extends NavigationDialog { + static instance; + static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog); + LOG_TAG = "SettingsNavigationDialog"; + $container; + $tabs; + $tabContents; + $btnReload; + $btnGlobalReload; + $noteGlobalReload; + $btnSuggestion; + $streamSettingsSelection; + renderFullSettings; + boundOnContextMenu; + suggestedSettings = { + recommended: {}, + default: {}, + lowest: {}, + highest: {} + }; + settingLabels = {}; + settingsManager; + TAB_GLOBAL_ITEMS = [{ + group: "general", + label: t("better-xcloud"), + helpUrl: "https://better-xcloud.github.io/features/", + items: [ + ($parent) => { + let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { + let opts = { + label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), + style: 1 | 64 | 128 + }; + if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); + else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; + topButtons.push(createButton(opts)); + } + if (AppInterface) topButtons.push(createButton({ + label: t("app-settings"), + icon: BxIcon.STREAM_SETTINGS, + style: 128 | 64, + onClick: (e) => { + AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); + } + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ + label: "🔥 " + t("install-android"), + style: 128 | 64, + url: "https://better-xcloud.github.io/android" + })); + this.$btnGlobalReload = createButton({ + label: t("settings-reload"), + classes: ["bx-settings-reload-button", "bx-gone"], + style: 64 | 128, + onClick: (e) => { + this.reloadPage(); + } + }), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", { + class: "bx-settings-reload-note" + }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", { + class: "bx-suggest-toggler bx-focusable", + tabindex: 0 + }, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + let $div = CE("div", { + class: "bx-top-buttons", + _nearby: { + orientation: "vertical" + } + }, ...topButtons); + $parent.appendChild($div); + }, + { + pref: "bx.locale", + multiLines: !0 + }, + "server.bypassRestriction", + "ui.controllerFriendly", + "xhome.enabled" + ] + }, { + group: "server", + label: t("server"), + items: [ + { + pref: "server.region", + multiLines: !0 + }, + { + pref: "stream.locale", + multiLines: !0 + }, + "server.ipv6.prefer" + ] + }, { + group: "stream", + label: t("stream"), + items: [ + "stream.video.resolution", + "stream.video.codecProfile", + "stream.video.maxBitrate", + "audio.volume.booster.enabled", + "screenshot.applyFilters", + "audio.mic.onPlaying", + "game.fortnite.forceConsole", + "stream.video.combineAudio" + ] + }, { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + items: [ + "nativeMkb.mode", + { + pref: "nativeMkb.forcedGames", + multiLines: !0, + note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list")) + }, + "mkb.enabled", + "mkb.cursor.hideIdle" + ], + ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { + unsupported: !0, + unsupportedNote: CE("a", { + href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", + target: "_blank" + }, "⚠️ " + t("browser-unsupported-feature")) + } : {} + }, { + requiredVariants: "full", + group: "touch-control", + label: t("touch-controller"), + items: [ + { + pref: "touchController.mode", + note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list")) + }, + "touchController.autoOff", + "touchController.opacity.default", + "touchController.style.standard", + "touchController.style.custom" + ], + ...!STATES.userAgent.capabilities.touch ? { + unsupported: !0, + unsupportedNote: "⚠️ " + t("device-unsupported-touch") + } : {} + }, { + group: "ui", + label: t("ui"), + items: [ + "ui.layout", + "ui.imageQuality", + "ui.gameCard.waitTime.show", + "ui.controllerStatus.show", + "ui.streamMenu.simplify", + "ui.splashVideo.skip", + !AppInterface && "ui.hideScrollbar", + "ui.systemMenu.hideHandle", + "ui.feedbackDialog.disabled", + "ui.reduceAnimations", + { + pref: "ui.hideSections", + multiLines: !0 + }, + { + pref: "block.features", + multiLines: !0 + } + ] + }, { + requiredVariants: "full", + group: "game-bar", + label: t("game-bar"), + items: [ + "gameBar.position" + ] + }, { + group: "loading-screen", + label: t("loading-screen"), + items: [ + "loadingScreen.gameArt.show", + "loadingScreen.waitTime.show", + "loadingScreen.rocket" + ] + }, { + group: "other", + label: t("other"), + items: [ + "block.tracking" + ] + }, { + group: "advanced", + label: t("advanced"), + items: [ + { + pref: "userAgent.profile", + multiLines: !0, + onCreated: (setting, $control) => { + let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { + type: "text", + placeholder: defaultUserAgent, + autocomplete: "off", + class: "bx-settings-custom-user-agent", + tabindex: 0 + }); + $inpCustomUserAgent.addEventListener("input", (e) => { + let profile = $control.value, custom = e.target.value.trim(); + UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e); + }), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, { + orientation: "vertical" + }); + } + } + ] + }, { + group: "footer", + items: [ + ($parent) => { + try { + let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); + $parent.appendChild(CE("div", { + class: "bx-settings-app-version" + }, `xCloud website version ${appVersion} (${appDate})`)); + } catch (e) {} + }, + ($parent) => { + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + $parent.appendChild(createButton({ + label: t("clear-data"), + style: 8 | 128 | 64, + onClick: (e) => { + if (confirm(t("clear-data-confirm"))) clearAllData(); + } + })); + }, + ($parent) => { + $parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 8 | 128 | 64, + onClick: (e) => { + let $button = e.target.closest("button"); + if (!$button) return; + let $pre = $button.nextElementSibling; + if (!$pre) { + let debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + _on: { + click: async (e2) => { + await copyToClipboard(e2.target.innerText); + } + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre); + } + $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); + } + }))); + } + ] + }]; + TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", + group: "audio", + label: t("audio"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: "audio.volume", + params: { + disabled: !getGlobalPref("audio.volume.booster.enabled") + }, + onCreated: (setting, $elm) => { + let $range = $elm.querySelector("input[type=range"); + BxEventBus.Stream.on("setting.changed", (payload) => { + let { settingKey } = payload; + if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 }); + }); + } + }] + }, { + group: "video", + label: t("video"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#video", + items: [ + "video.player.type", + "video.maxFps", + "video.player.powerPreference", + "video.processing", + "video.ratio", + "video.position", + "video.processing.sharpness", + "video.saturation", + "video.contrast", + "video.brightness" + ] + }]; + TAB_CONTROLLER_ITEMS = [ + { + group: "controller", + label: t("controller"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", + items: [ + "localCoOp.enabled", + "controller.pollingRate", + ($parent) => { + $parent.appendChild(ControllerExtraSettings.renderSettings.apply(this)); + } + ] + }, + STATES.userAgent.capabilities.touch && { + group: "touch-control", + label: t("touch-controller"), + items: [{ + label: t("layout"), + content: CE("select", { + disabled: !0 + }, CE("option", !1, t("default"))), + onCreated: (setting, $elm) => { + $elm.addEventListener("input", (e) => { + TouchController.applyCustomLayout($elm.value, 1000); + }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { + let customLayouts = TouchController.getCustomLayouts(); + while ($elm.firstChild) + $elm.removeChild($elm.firstChild); + if ($elm.disabled = !customLayouts, !customLayouts) { + $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input")); + return; + } + let $fragment = document.createDocumentFragment(); + for (let key in customLayouts.layouts) { + let layout = customLayouts.layouts[key], name; + if (layout.author) name = `${layout.name} (${layout.author})`; + else name = layout.name; + let $option = CE("option", { value: key }, name); + $fragment.appendChild($option); + } + $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; + }); + } + }] + }, + STATES.browser.capabilities.deviceVibration && { + group: "device", + label: t("device"), + items: [{ + pref: "deviceVibration.mode", + multiLines: !0, + unsupported: !STATES.browser.capabilities.deviceVibration + }, { + pref: "deviceVibration.intensity", + unsupported: !STATES.browser.capabilities.deviceVibration + }] + } + ]; + TAB_MKB_ITEMS = [ + { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", + items: [ + ($parent) => { + $parent.appendChild(MkbExtraSettings.renderSettings.apply(this)); + } + ] + }, + NativeMkbHandler.isAllowed() && { + requiredVariants: "full", + group: "native-mkb", + label: t("native-mkb"), + items: [ + "nativeMkb.scroll.sensitivityY", + "nativeMkb.scroll.sensitivityX" + ] + } + ]; + TAB_STATS_ITEMS = [{ + group: "stats", + label: t("stream-stats"), + helpUrl: "https://better-xcloud.github.io/stream-stats/", + items: [ + "stats.showWhenPlaying", + "stats.quickGlance.enabled", + "stats.items", + "stats.position", + "stats.textSize", + "stats.opacity.all", + "stats.opacity.background", + "stats.colors" + ] + }]; + SETTINGS_UI = { + global: { + group: "global", + icon: BxIcon.HOME, + items: this.TAB_GLOBAL_ITEMS + }, + stream: { + group: "stream", + icon: BxIcon.DISPLAY, + items: this.TAB_DISPLAY_ITEMS + }, + controller: { + group: "controller", + icon: BxIcon.CONTROLLER, + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + mkb: { + group: "mkb", + icon: BxIcon.NATIVE_MKB, + items: this.TAB_MKB_ITEMS, + requiredVariants: "full" + }, + stats: { + group: "stats", + icon: BxIcon.STREAM_STATS, + items: this.TAB_STATS_ITEMS + } + }; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => { + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + }), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => { + this.$tabContents.dataset.gameId = id.toString(); + }); + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + onMounted() { + super.onMounted(); + } + isOverlayVisible() { + return !STATES.isPlaying; + } + reloadPage() { + this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); + } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } + onTabClicked = (e) => { + let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children); + for ($child of children) + if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child); + else if ($child.dataset.tabGroup) $child.classList.add("bx-gone"); + this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global"); + for (let $child2 of Array.from(this.$tabs.children)) + $child2.classList.remove("bx-active"); + $svg.classList.add("bx-active"); + }; + renderTab(settingTab) { + let $svg = createSvgIcon(settingTab.icon); + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg; + } + onGlobalSettingChanged = (e) => { + PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); + }; + onContextMenu(e) { + e.preventDefault(); + let $elm = e.target; + $elm instanceof HTMLElement && this.resetHighlightedSetting($elm); + } + renderServerSetting(setting) { + let selectedValue = getGlobalPref("server.region"), continents = { + "america-north": { + label: t("continent-north-america") + }, + "america-south": { + label: t("continent-south-america") + }, + asia: { + label: t("continent-asia") + }, + australia: { + label: t("continent-australia") + }, + europe: { + label: t("continent-europe") + }, + other: { + label: t("other") + } + }, $control = CE("select", { + id: `bx_setting_${escapeCssSelector(setting.pref)}`, + tabindex: 0 + }); + $control.name = $control.id, $control.addEventListener("input", (e) => { + setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e); + }), setting.options = {}; + for (let regionName in STATES.serverRegions) { + let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`; + if (region.isDefault) { + if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; + } + setting.options[value] = label; + let $option = CE("option", { value }, label), continent = continents[region.contintent]; + if (!continent.children) continent.children = []; + continent.children.push($option); + } + let fragment = document.createDocumentFragment(), key; + for (key in continents) { + let continent = continents[key]; + if (!continent.children) continue; + fragment.appendChild(CE("optgroup", { + label: continent.label + }, ...continent.children)); + } + return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; + } + renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { + if (typeof setting === "string") setting = { + pref: setting + }; + let pref = setting.pref, $control; + if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this); + else $control = setting.content; + else if (!setting.unsupported) { + if (pref === "server.region") $control = this.renderServerSetting(setting); + else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => { + let newLocale = e.target.value; + if (getGlobalPref("ui.controllerFriendly")) { + let timeoutId = e.target.timeoutId; + timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { + Translations.refreshLocale(newLocale), Translations.updateTranslations(); + }, 500); + } else Translations.refreshLocale(newLocale), Translations.updateTranslations(); + this.onGlobalSettingChanged(e); + }); + else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => { + let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value); + UserAgent.updateStorage(value); + let $inp = $control.nextElementSibling; + $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); + }); + else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged); + if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control); + } + let prefDefinition = null; + if (pref) prefDefinition = getPrefInfo(pref).definition; + if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; + let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; + if (typeof note === "function") note = note(); + if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote(); + if (settingTabContent.label && setting.pref) { + if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest); + } + if (experimental) if (label = "🧪 " + label, !note) note = t("experimental"); + else note = `${t("experimental")}: ${note}`; + let $note; + if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote); + else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note); + let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, { + $note, + multiLines: setting.multiLines, + icon: prefDefinition?.labelIcon, + onContextMenu: this.boundOnContextMenu, + pref + }); + if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`; + $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + } + renderSettingsSection(settingTab, sections) { + let $tabContent = CE("div", { + class: "bx-gone", + _dataset: { + tabGroup: settingTab.group + } + }); + for (let section of sections) { + if (!section) continue; + if (section instanceof HTMLElement) { + $tabContent.appendChild(section); + continue; + } + if (!this.isSupportedVariant(section.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue; + let label = section.label; + if (label === t("better-xcloud")) { + if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; + label = createButton({ + label, + url: "https://github.com/redphx/better-xcloud/releases", + style: 4096 | 16 | 64 + }); + } + if (label) { + let $title = CE("h2", { + _nearby: { + orientation: "horizontal" + } + }, CE("span", !1, label), section.helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 8 | 64, + url: section.helpUrl, + title: t("help") + })); + $tabContent.appendChild($title); + } + if (section.unsupportedNote) { + let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote); + $tabContent.appendChild($note); + } + if (section.unsupported) continue; + if (section.content) { + $tabContent.appendChild(section.content); + continue; + } + section.items = section.items || []; + for (let setting of section.items) { + if (setting === !1) continue; + if (typeof setting === "function") { + setting.apply(this, [$tabContent]); + continue; + } + this.renderSettingRow(settingTab, $tabContent, section, setting); + } + } + return $tabContent; + } + setupDialog() { + let $tabs, $tabContents, $container = CE("div", { + class: "bx-settings-dialog", + _nearby: { + orientation: "horizontal" + } + }, CE("div", { + class: "bx-settings-tabs-container", + _nearby: { + orientation: "vertical", + focus: () => { + return this.dialogManager.focus($tabs); + }, + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + }, $tabs = CE("div", { + class: "bx-settings-tabs bx-hide-scroll-bar", + _nearby: { + focus: () => this.focusActiveTab() + } + }), CE("div", !1, this.$btnReload = createButton({ + icon: BxIcon.REFRESH, + style: 64 | 32, + onClick: (e) => { + this.reloadPage(); + } + }), createButton({ + icon: BxIcon.CLOSE, + style: 64 | 32, + onClick: (e) => { + this.dialogManager.hide(); + } + }))), CE("div", { + class: "bx-settings-tab-contents", + _nearby: { + orientation: "vertical", + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + }, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", { + class: "bx-settings-tab-content", + _nearby: { + orientation: "vertical", + focus: () => this.jumpToSettingGroup("next") + } + }))); + this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => { + if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + }); + let settingTabGroup; + for (settingTabGroup in this.SETTINGS_UI) { + let settingTab = this.SETTINGS_UI[settingTabGroup]; + if (!settingTab) continue; + if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; + if (settingTab.group !== "global" && !this.renderFullSettings) continue; + let $svg = this.renderTab(settingTab); + $tabs.appendChild($svg); + let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items); + $tabContents.appendChild($tabContent); + } + $tabs.firstElementChild.dispatchEvent(new Event("click")); + } + focusTab(tabId) { + let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); + $tab && $tab.dispatchEvent(new Event("click")); + } + focusIfNeeded() { + this.jumpToSettingGroup("next"); + } + focusActiveTab() { + let $currentTab = this.$tabs.querySelector(".bx-active"); + return $currentTab && $currentTab.focus(), !0; + } + focusVisibleSetting(type = "first") { + let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *")); + if (!controls.length) return !1; + if (type === "last") controls.reverse(); + for (let $control of controls) { + if (!($control instanceof HTMLElement)) continue; + let $focusable = this.dialogManager.findFocusableElement($control); + if ($focusable) { + if (this.dialogManager.focus($focusable)) return !0; + } + } + return !1; + } + focusVisibleTab(type = "first") { + let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)")); + if (!tabs.length) return !1; + if (type === "last") tabs.reverse(); + for (let $tab of tabs) + if (this.dialogManager.focus($tab)) return !0; + return !1; + } + jumpToSettingGroup(direction) { + let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)"); + if (!$tabContent) return !1; + let $header, $focusing = document.activeElement; + if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + else { + let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0; + while (!0) { + if (!$tmp) break; + if ($tmp.tagName === "H2") { + if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) { + if (++times, direction === "next" || times >= 2) break; + } + } + $tmp = $tmp[siblingProperty]; + } + } + let $target; + if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1); + if ($target) return this.dialogManager.focus($target); + return !1; + } + resetHighlightedSetting($elm) { + let targetGameId = SettingsManager.getInstance().getTargetGameId(); + if (targetGameId < 0) return; + if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0; + let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row"); + if (!$row) return; + let pref = $row.prefKey; + if (!pref) alert("Pref not found: " + $row.id); + if (!isStreamPref(pref)) return; + let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref); + if (deleted) BxEventBus.Stream.emit("setting.changed", { + storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`, + settingKey: pref + }); + return deleted; + } + handleKeyPress(key) { + let handled = !0; + switch (key) { + case "Tab": + this.focusActiveTab(); + break; + case "Home": + this.focusVisibleSetting("first"); + break; + case "End": + this.focusVisibleSetting("last"); + break; + case "PageUp": + this.jumpToSettingGroup("previous"); + break; + case "PageDown": + this.jumpToSettingGroup("next"); + break; + case "KeyQ": + this.resetHighlightedSetting(); + break; + default: + handled = !1; + break; + } + return handled; + } + handleGamepad(button) { + let handled = !0; + switch (button) { + case 1: + let $focusing = document.activeElement; + if ($focusing && this.$tabs.contains($focusing)) this.hide(); + else this.focusActiveTab(); + break; + case 4: + case 5: + this.focusActiveTab(); + break; + case 6: + this.jumpToSettingGroup("previous"); + break; + case 7: + this.jumpToSettingGroup("next"); + break; + case 2: + this.resetHighlightedSetting(); + break; + default: + handled = !1; + break; + } + return handled; + } +} +class ScreenshotManager { + static instance; + static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager); + LOG_TAG = "ScreenshotManager"; + $download; + $canvas; + canvasContext; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", { + alpha: !1, + willReadFrequently: !1 + }); + } + updateCanvasSize(width, height) { + this.$canvas.width = width, this.$canvas.height = height; + } + updateCanvasFilters(filters) { + this.canvasContext.filter = filters; + } + onAnimationEnd(e) { + e.target.classList.remove("bx-taking-screenshot"); + } + takeScreenshot(callback) { + let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas; + if (!streamPlayer || !$canvas) return; + let $player; + if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement(); + else $player = streamPlayer.getPlayerElement("default"); + if (!$player || !$player.isConnected) return; + let $gameStream = $player.closest("#game-stream"); + if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot"); + let canvasContext = this.canvasContext; + if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame(); + if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { + let data = $canvas.toDataURL("image/png").split(";base64,")[1]; + AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + return; + } + $canvas.toBlob((blob) => { + if (!blob) return; + let now = +new Date, $download = this.$download; + $download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + }, "image/png"); + } +} +class RendererShortcut { + static toggleVisibility() { + let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); + if (!$mediaContainer) { + BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 }); + return; + } + $mediaContainer.classList.toggle("bx-gone"); + let isVisible = !$mediaContainer.classList.contains("bx-gone"); + limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible }); + } +} +class TrueAchievements { + static instance; + static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements); + LOG_TAG = "TrueAchievements"; + $link; + $button; + $hiddenLink; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({ + label: t("true-achievements"), + url: "#", + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 64 | 8 | 128 | 8192, + onClick: this.onClick + }), this.$button = createButton({ + label: t("true-achievements"), + title: t("true-achievements"), + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 64, + onClick: this.onClick + }), this.$hiddenLink = CE("a", { + target: "_blank" + }); + } + onClick = (e) => { + e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll(); + let dataset = this.$link.dataset; + this.open(!0, dataset.xboxTitleId, dataset.id); + }; + updateIds(xboxTitleId, id) { + let $link = this.$link, $button = this.$button; + if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; + if (id) $link.dataset.id = id, $button.dataset.id = id; + } + injectAchievementsProgress($elm) { + if (SCRIPT_VARIANT !== "full") return; + let $parent = $elm.parentElement, $div = CE("div", { + class: "bx-guide-home-achievements-progress" + }, $elm), xboxTitleId; + try { + let $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); + if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; + } catch (e) {} + if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId(); + if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); + if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link); + else $div.appendChild(this.$button); + $parent.appendChild($div); + } + injectAchievementDetailPage($parent) { + if (SCRIPT_VARIANT !== "full") return; + let props = getReactProps($parent); + if (!props) return; + try { + let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId; + for (let achiev of achievementList) + if (achiev.name === achievementName) { + id = achiev.id, xboxTitleId = achiev.title.id; + break; + } + if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link); + } catch (e) {} + } + getStreamXboxTitleId() { + return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; + } + open(override, xboxTitleId, id) { + if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId(); + if (AppInterface && AppInterface.openTrueAchievementsLink) { + AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString()); + return; + } + let url = "https://www.trueachievements.com"; + if (xboxTitleId) { + if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`; + } + this.$hiddenLink.href = url, this.$hiddenLink.click(); + } +} +class VirtualControllerShortcut { + static pressXboxButton() { + let streamSession = window.BX_EXPOSED.streamSession; + if (!streamSession) return; + let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, { + Nexus: 1, + VirtualPhysicality: 1024 + }); + streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => { + streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]); + }, 100); + } +} +class ShortcutHandler { + static runAction(action) { + switch (action) { + case "bx.settings.show": + SettingsDialog.getInstance().show(); + break; + case "stream.screenshot.capture": + ScreenshotManager.getInstance().takeScreenshot(); + break; + case "stream.video.toggle": + RendererShortcut.toggleVisibility(); + break; + case "stream.stats.toggle": + StreamStats.getInstance().toggle(); + break; + case "stream.microphone.toggle": + MicrophoneShortcut.toggle(); + break; + case "stream.menu.show": + StreamUiShortcut.showHideStreamMenu(); + break; + case "stream.sound.toggle": + SoundShortcut.muteUnmute(); + break; + case "stream.volume.inc": + SoundShortcut.adjustGainNodeVolume(10); + break; + case "stream.volume.dec": + SoundShortcut.adjustGainNodeVolume(-10); + break; + case "device.brightness.inc": + case "device.brightness.dec": + case "device.sound.toggle": + case "device.volume.inc": + case "device.volume.dec": + AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action); + break; + case "mkb.toggle": + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle(); + else EmulatedMkbHandler.getInstance()?.toggle(); + break; + case "ta.open": + TrueAchievements.getInstance().open(!1); + break; + case "controller.xbox.press": + VirtualControllerShortcut.pressXboxButton(); + break; + } + } +} +class ControllerShortcut { + static buttonsCache = {}; + static buttonsStatus = {}; + static reset(index) { + ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; + } + static handle(gamepad) { + let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; + if (!controllerSettings) return !1; + let actions = controllerSettings.shortcuts; + if (!actions) return !1; + let gamepadIndex = gamepad.index; + ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; + let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button; + for ([index, button] of entries) + if (button.pressed && index !== 16) { + if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) { + let idx = index; + setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0); + } + } + return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; + } +} +var FeatureGates = { + PwaPrompt: !1, + EnableWifiWarnings: !1, + EnableUpdateRequiredPage: !1, + ShowForcedUpdateScreen: !1, + EnableTakControlResizing: !0 +}, nativeMkbMode = getGlobalPref("nativeMkb.mode"); +if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; +var blockFeatures = getGlobalPref("block.features"); +if (blockFeatures.includes("chat")) FeatureGates.EnableGuideChatTab = !1; +if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers = !1; +if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; +if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); +class LocalCoOpManager { + static instance; + static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager); + supportedIds; + constructor() { + BxEventBus.Script.once("list.localCoOp.updated", (e) => { + this.supportedIds = e.ids; + }), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds); + } + isSupported(productId) { + return this.supportedIds.has(productId); + } +} +var BxExposed = { + getTitleInfo: () => STATES.currentStream.titleInfo, + modifyPreloadedState: (state) => { + let LOG_TAG3 = "PreloadState"; + try { + state.appContext.requestInfo.userAgent = window.navigator.userAgent; + } catch (e) { + BxLogger.error(LOG_TAG3, e); + } + try { + for (let exp in FeatureGates) + state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp]; + } catch (e) { + BxLogger.error(LOG_TAG3, e); + } + try { + let sigls = state.xcloud.sigls; + if (STATES.userAgent.capabilities.touch) { + let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470"; + if (siglId in sigls) { + let allGames = sigls[siglId].data.products; + customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); + } else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId); + } + } catch (e) { + BxLogger.error(LOG_TAG3, e); + } + try { + let sigls = state.xcloud.sigls; + if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles); + } catch (e) { + BxLogger.error(LOG_TAG3, e); + } + try { + let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud; + if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play"; + } catch (e) { + BxLogger.error(LOG_TAG3, e); + } + return state; + }, + modifyTitleInfo: function(titleInfo) { + titleInfo = deepClone(titleInfo); + let supportedInputTypes = titleInfo.details.supportedInputTypes; + if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); + if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); + if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { + let touchControllerAvailability = getGlobalPref("touchController.mode"); + if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) { + let gamepads = window.navigator.getGamepads(), gamepadFound = !1; + for (let gamepad of gamepads) + if (gamepad && gamepad.connected) { + gamepadFound = !0; + break; + } + gamepadFound && (touchControllerAvailability = "off"); + } + if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = []; + if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch"); + } + return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo; + }, + setupGainNode: ($media, audioStream) => { + if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => { + $media.muted = !0, $media.pause(); + }); + else $media.muted = !0, $media.addEventListener("playing", (e) => { + $media.muted = !0; + }); + try { + let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain(); + source.connect(gainNode).connect(audioCtx.destination); + } catch (e) { + BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null; + } + }, + handleControllerShortcut: ControllerShortcut.handle, + resetControllerShortcut: ControllerShortcut.reset, + overrideSettings: { + Tv_settings: { + hasCompletedOnboarding: !0 + } + }, + disableGamepadPolling: !1, + backButtonPressed: () => { + let navigationDialogManager = NavigationDialogManager.getInstance(); + if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; + let dict = { + bubbles: !0, + cancelable: !0, + key: "XF86Back", + code: "XF86Back", + keyCode: 4, + which: 4 + }; + return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1; + }, + GameSlugRegexes: [ + /[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, + / {2,}/g, + / /g + ], + toggleLocalCoOp(enable) {}, + beforePageLoad: (page) => { + BxLogger.info("beforePageLoad", page), Patcher.patchPage(page); + }, + localCoOpManager: LocalCoOpManager.getInstance(), + reactCreateElement: function(...args) {}, + createReactLocalCoOpIcon: (attrs) => { + let reactCE = window.BX_EXPOSED.reactCreateElement; + return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" }))); + } +}; +function localRedirect(path) { + let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); + if (!$pageContent) return; + let $anchor = CE("a", { + href: url, + class: "bx-hidden bx-offscreen" + }, ""); + $anchor.addEventListener("click", (e) => { + window.setTimeout(() => { + $pageContent.removeChild($anchor); + }, 1000); + }), $pageContent.appendChild($anchor), $anchor.click(); +} +window.localRedirect = localRedirect; +function getPreferredServerRegion(shortName = !1) { + let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions; + if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; + else return preferredRegion; + for (let regionName in serverRegions) { + let region = serverRegions[regionName]; + if (!region.isDefault) continue; + if (shortName && region.shortName) return region.shortName; + else return regionName; + } + return null; +} +class HeaderSection { + static instance; + static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection); + LOG_TAG = "HeaderSection"; + $btnRemotePlay; + $btnSettings; + $buttonsWrapper; + observer; + timeoutId; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({ + classes: ["bx-header-remote-play-button", "bx-gone"], + icon: BxIcon.REMOTE_PLAY, + title: t("remote-play"), + style: 8 | 64 | 2048, + onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() + }), this.$btnSettings = createButton({ + classes: ["bx-header-settings-button"], + label: "???", + style: 16 | 32 | 64 | 256, + onClick: (e) => SettingsDialog.getInstance().show() + }), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings); + } + injectSettingsButton($parent) { + if (!$parent) return; + let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings; + if (isElementVisible(this.$buttonsWrapper)) return; + if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); + $parent.appendChild(this.$buttonsWrapper); + } + checkHeader = () => { + let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); + if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); + $target && this.injectSettingsButton($target); + }; + watchHeader() { + let $root = document.querySelector("#PageContent header") || document.querySelector("#root"); + if (!$root) return; + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => { + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000); + }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader(); + } + showRemotePlayButton() { + this.$btnRemotePlay?.classList.remove("bx-gone"); + } + static watchHeader() { + HeaderSection.getInstance().watchHeader(); + } +} +class RemotePlayDialog extends NavigationDialog { + static instance; + static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog); + LOG_TAG = "RemotePlayNavigationDialog"; + STATE_LABELS = { + On: t("powered-on"), + Off: t("powered-off"), + ConnectedStandby: t("standby"), + Unknown: t("unknown") + }; + $container; + constructor() { + super(); + BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog(); + } + setupDialog() { + let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); + $resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => { + let value = e.target.value; + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui"); + }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { + manualTrigger: !0 + }); + let $qualitySettings = CE("div", { + class: "bx-remote-play-settings" + }, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions)); + $fragment.appendChild($qualitySettings); + let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); + for (let con of consoles) { + let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({ + classes: ["bx-remote-play-connect-button"], + label: t("console-connect"), + style: 1 | 64, + onClick: (e) => manager.play(con.serverId) + })); + $fragment.appendChild($child); + } + $fragment.appendChild(CE("div", { + class: "bx-remote-play-buttons", + _nearby: { + orientation: "horizontal" + } + }, createButton({ + icon: BxIcon.QUESTION, + style: 8 | 64, + url: "https://better-xcloud.github.io/remote-play", + label: t("help") + }), createButton({ + style: 8 | 64, + label: t("close"), + onClick: (e) => this.hide() + }))), this.$container = $fragment; + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + focusIfNeeded() { + let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); + $btnConnect && $btnConnect.focus(); + } +} +class RemotePlayManager { + static instance; + static getInstance() { + if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager; + else RemotePlayManager.instance = null; + return RemotePlayManager.instance; + } + LOG_TAG = "RemotePlayManager"; + isInitialized = !1; + XCLOUD_TOKEN; + XHOME_TOKEN; + consoles; + regions = []; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"); + } + initialize() { + if (this.isInitialized) return; + this.isInitialized = !0, this.requestXhomeToken(() => { + this.getConsolesList(() => { + BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); + }); + }); + } + getXcloudToken() { + return this.XCLOUD_TOKEN; + } + setXcloudToken(token) { + this.XCLOUD_TOKEN = token; + } + getXhomeToken() { + return this.XHOME_TOKEN; + } + getConsoles() { + return this.consoles; + } + requestXhomeToken(callback) { + if (this.XHOME_TOKEN) { + callback(); + return; + } + let GSSV_TOKEN; + try { + GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token; + } catch (e) { + for (let i = 0;i < localStorage.length; i++) { + let key = localStorage.key(i); + if (!key.startsWith("Auth.User.")) continue; + let json = JSON.parse(localStorage.getItem(key)); + for (let token of json.tokens) { + if (!token.relyingParty.includes("gssv.xboxlive.com")) continue; + GSSV_TOKEN = token.tokenData.token; + break; + } + break; + } + } + let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { + method: "POST", + body: JSON.stringify({ + offeringId: "xhome", + token: GSSV_TOKEN + }), + headers: { + "Content-Type": "application/json; charset=utf-8" + } + }); + fetch(request).then((resp) => resp.json()).then((json) => { + this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback(); + }); + } + async getConsolesList(callback) { + if (this.consoles) { + callback(); + return; + } + let options = { + method: "GET", + headers: { + Authorization: `Bearer ${this.XHOME_TOKEN}` + } + }; + for (let region of this.regions) + try { + let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json(); + if (json.results.length === 0) continue; + this.consoles = json.results, STATES.remotePlay.server = region.baseUri; + break; + } catch (e) {} + if (!STATES.remotePlay.server) this.consoles = []; + callback(); + } + play(serverId, resolution) { + if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui"); + STATES.remotePlay.config = { + serverId + }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); + } + togglePopup(force = null) { + if (!this.isReady()) { + Toast.show(t("getting-consoles-list")); + return; + } + if (this.consoles.length === 0) { + Toast.show(t("no-consoles-found"), "", { instant: !0 }); + return; + } + RemotePlayDialog.getInstance().show(); + } + static detect() { + if (!getGlobalPref("xhome.enabled")) return; + if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play"); + else window.BX_REMOTE_PLAY_CONFIG = null; + } + isReady() { + return this.consoles !== null; + } +} +class XhomeInterceptor { + static consoleAddrs = {}; + static async handleLogin(request) { + try { + let obj = await request.clone().json(); + obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { + method: "POST", + body: JSON.stringify(obj), + headers: { + "Content-Type": "application/json" + } + }); + } catch (e) { + alert(e), console.log(e); + } + return NATIVE_FETCH(request); + } + static async handleConfiguration(request) { + BxEventBus.Stream.emit("state.starting", {}); + let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [ + ["ipAddress", "port"], + ["ipV4Address", "ipV4Port"], + ["ipV6Address", "ipV6Port"] + ]; + XhomeInterceptor.consoleAddrs = {}; + for (let pair of pairs) { + let [keyAddr, keyPort] = pair; + if (keyAddr && keyPort && serverDetails[keyAddr]) { + let port = serverDetails[keyPort], ports = new Set; + port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports); + } + } + return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async handleInputConfigs(request, opts) { + let response = await NATIVE_FETCH(request); + if (getGlobalPref("touchController.mode") !== "all") return response; + let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; + TouchController.setXboxTitleId(xboxTitleId); + let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0; + if (!hasTouchSupport) { + let supportedInputTypes = inputConfigs.supportedInputTypes; + hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay"); + } + if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { + data: null + }); + else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId); + return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async handleTitles(request) { + let clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`; + let index = request.url.indexOf(".xboxlive.com"); + return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), { + method: clone.method, + body: await clone.text(), + headers + }), NATIVE_FETCH(request); + } + static async handlePlay(request) { + BxEventBus.Stream.emit("state.loading", {}); + let body = await request.clone().json(), newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async handle(request) { + TouchController.disable(); + let clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`; + let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution")); + headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)); + let opts = { + method: clone.method, + headers + }; + if (clone.method === "POST") opts.body = await clone.text(); + let url = request.url; + if (!url.includes("/servers/home")) { + let parsed = new URL(url); + url = STATES.remotePlay.server + parsed.pathname; + } + if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request); + else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request); + else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts); + else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request); + else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request); + else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs); + return await NATIVE_FETCH(request); + } +} +class LoadingScreen { + static $bgStyle; + static $waitTimeBox; + static waitTimeInterval = null; + static orgWebTitle; + static secondsToString(seconds) { + let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0"); + return mDisplay + sDisplay; + } + static setup() { + let titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) return; + if (!LoadingScreen.$bgStyle) { + let $bgStyle = CE("style"); + document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; + } + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket(); + } + static hideRocket() { + let $bgStyle = LoadingScreen.$bgStyle; + $bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}"; + } + static setBackground(imageUrl) { + let $bgStyle = LoadingScreen.$bgStyle; + imageUrl = imageUrl + "?w=1920"; + let imageQuality = getGlobalPref("ui.imageQuality"); + if (imageQuality !== 90) imageUrl += "&q=" + imageQuality; + $bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`; + let bg = new Image; + bg.onload = (e) => { + $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; + }, bg.src = imageUrl; + } + static setupWaitTime(waitTime) { + if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket(); + let secondsLeft = waitTime, $countDown, $estimated; + LoadingScreen.orgWebTitle = document.title; + let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; + endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds); + let endDateStr = endDate.toISOString().slice(0, 19); + endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`; + let $waitTimeBox = LoadingScreen.$waitTimeBox; + if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox; + else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown"); + $estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => { + if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null; + }, 1000); + } + static hide() { + if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) { + let $rocketBg = document.querySelector('#game-stream rect[width="800"]'); + $rocketBg && $rocketBg.addEventListener("transitionend", (e) => { + LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}"; + }), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}'; + } + setTimeout(LoadingScreen.reset, 2000); + } + static reset() { + LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null; + } +} +class GuideMenu { + static instance; + static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu); + $renderedButtons; + closeGuideMenu() { + if (window.BX_EXPOSED.dialogRoutes) { + window.BX_EXPOSED.dialogRoutes.closeAll(); + return; + } + let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); + $btnClose && $btnClose.click(); + } + renderButtons() { + if (this.$renderedButtons) return this.$renderedButtons; + let buttons = { + scriptSettings: createButton({ + label: t("better-xcloud"), + icon: BxIcon.BETTER_XCLOUD, + style: 128 | 64 | 1, + onClick: () => { + BxEventBus.Script.once("dialog.dismissed", () => { + setTimeout(() => SettingsDialog.getInstance().show(), 50); + }), this.closeGuideMenu(); + } + }), + closeApp: AppInterface && createButton({ + icon: BxIcon.POWER, + label: t("close-app"), + title: t("close-app"), + style: 128 | 64 | 4, + onClick: (e) => { + AppInterface.closeApp(); + }, + attributes: { + "data-state": "normal" + } + }), + reloadPage: createButton({ + icon: BxIcon.REFRESH, + label: t("reload-page"), + title: t("reload-page"), + style: 128 | 64, + onClick: () => { + if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); + else window.location.reload(); + } + }), + backToHome: createButton({ + icon: BxIcon.HOME, + label: t("back-to-home"), + title: t("back-to-home"), + style: 128 | 64, + onClick: () => { + this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); + }, + attributes: { + "data-state": "playing" + } + }) + }, buttonsLayout = [ + buttons.scriptSettings, + [ + buttons.backToHome, + buttons.reloadPage, + buttons.closeApp + ] + ], $div = CE("div", { + class: "bx-guide-home-buttons" + }); + if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv"; + for (let $button of buttonsLayout) { + if (!$button) continue; + if ($button instanceof HTMLElement) $div.appendChild($button); + else if (Array.isArray($button)) { + let $wrapper = CE("div", {}); + for (let $child of $button) + $child && $wrapper.appendChild($child); + $div.appendChild($wrapper); + } + } + return this.$renderedButtons = $div, $div; + } + injectHome($root, isPlaying = !1) { + { + let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); + if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress); + } + let $target = null; + if (isPlaying) { + $target = $root.querySelector("a[class*=QuitGameButton]"); + let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); + $btnXcloudHome && ($btnXcloudHome.style.display = "none"); + } else { + let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); + if ($dividers) $target = $dividers[$dividers.length - 1]; + } + if (!$target) return !1; + let $buttons = this.renderButtons(); + $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + } + onShown = async (e) => { + if (e.where === "home") { + let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && this.injectHome($root, STATES.isPlaying); + } + }; + addEventListeners() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown); + } + observe($addedElm) { + let className = $addedElm.className; + if (!className) className = $addedElm.firstElementChild?.className ?? ""; + if (!className || className.startsWith("bx-")) return; + if (className.includes("AchievementsButton-module__progressBarContainer")) { + TrueAchievements.getInstance().injectAchievementsProgress($addedElm); + return; + } + if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; + { + let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); + if ($achievDetailPage) { + TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage); + return; + } + } + let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true"); + if ($selectedTab) { + let $elm = $selectedTab, index; + for (index = 0;$elm = $elm?.previousElementSibling; index++) + ; + if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" }); + } + } +} +class StreamBadges { + static instance; + static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges); + LOG_TAG = "StreamBadges"; + serverInfo = {}; + badges = { + playtime: { + name: t("playtime"), + icon: BxIcon.PLAYTIME, + color: "#ff004d" + }, + battery: { + name: t("battery"), + icon: BxIcon.BATTERY, + color: "#00b543" + }, + download: { + name: t("download"), + icon: BxIcon.DOWNLOAD, + color: "#29adff" + }, + upload: { + name: t("upload"), + icon: BxIcon.UPLOAD, + color: "#ff77a8" + }, + server: { + name: t("server"), + icon: BxIcon.SERVER, + color: "#ff6c24" + }, + video: { + name: t("video"), + icon: BxIcon.DISPLAY, + color: "#742f29" + }, + audio: { + name: t("audio"), + icon: BxIcon.AUDIO, + color: "#5f574f" + } + }; + $container; + intervalId; + REFRESH_INTERVAL = 3000; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"); + } + setRegion(region) { + this.serverInfo.server = { + region + }; + } + renderBadge(name, value) { + let badgeInfo = this.badges[name], $badge; + if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge; + if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery"); + return this.badges[name].$element = $badge, $badge; + } + updateBadges = async (forceUpdate = !1) => { + if (!this.$container || !forceUpdate && !this.$container.isConnected) { + this.stop(); + return; + } + let statsCollector = StreamStatsCollector.getInstance(); + await statsCollector.collect(); + let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = { + download: dl.toString(), + upload: ul.toString(), + playtime: play.toString(), + battery: batt.toString() + }, name; + for (name in badges) { + let value = badges[name]; + if (value === null) continue; + let $elm = this.badges[name].$element; + if (!$elm) continue; + if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone"); + else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone"); + } + }; + async start() { + await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL); + } + stop() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + destroy() { + this.serverInfo = {}, delete this.$container; + } + async render() { + if (this.$container) return this.start(), this.$container; + await this.getServerStats(); + let batteryLevel = ""; + if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; + let BADGES = [ + ["playtime", "1m"], + ["battery", batteryLevel], + ["download", humanFileSize(0)], + ["upload", humanFileSize(0)], + this.badges.server.$element ?? ["server", "?"], + this.serverInfo.video ? this.badges.video.$element : ["video", "?"], + this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"] + ], $container = CE("div", { class: "bx-badges" }); + for (let item2 of BADGES) { + if (!item2) continue; + let $badge; + if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2); + else $badge = item2; + $container.appendChild($badge); + } + return this.$container = $container, await this.start(), $container; + } + async getServerStats() { + let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId; + if (stats.forEach((stat) => { + if (stat.type === "codec") { + let mimeType = stat.mimeType.split("/")[0]; + if (mimeType === "video") allVideoCodecs[stat.id] = stat; + else if (mimeType === "audio") allAudioCodecs[stat.id] = stat; + } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) { + if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight; + else if (stat.kind === "audio") audioCodecId = stat.codecId; + } else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId; + else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId; + else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address; + }), videoCodecId) { + let videoStat = allVideoCodecs[videoCodecId], video = { + width: videoWidth, + height: videoHeight, + codec: videoStat.mimeType.substring(6) + }; + if (video.codec === "H264") { + let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); + match && (video.profile = match[1]); + } + let text = videoHeight + "p"; + if (text && (text += "/"), text += video.codec, video.profile) { + let profile = video.profile, quality = profile; + if (profile.startsWith("4d")) quality = t("visual-quality-high"); + else if (profile.startsWith("42e")) quality = t("visual-quality-normal"); + else if (profile.startsWith("420")) quality = t("visual-quality-low"); + text += ` (${quality})`; + } + this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video; + } + if (audioCodecId) { + let audioStat = allAudioCodecs[audioCodecId], audio = { + codec: audioStat.mimeType.substring(6), + bitrate: audioStat.clockRate + }, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`; + this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio; + } + if (candidatePairId) { + BxLogger.info("candidate", candidatePairId, allCandidatePairs); + let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server; + if (server && server.region) text += server.region; + text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); + } + } + static setupEvents() {} +} +class XcloudInterceptor { + static SERVER_EXTRA_INFO = { + EastUS: ["🇺🇸", "america-north"], + EastUS2: ["🇺🇸", "america-north"], + NorthCentralUs: ["🇺🇸", "america-north"], + SouthCentralUS: ["🇺🇸", "america-north"], + WestUS: ["🇺🇸", "america-north"], + WestUS2: ["🇺🇸", "america-north"], + MexicoCentral: ["🇲🇽", "america-north"], + BrazilSouth: ["🇧🇷", "america-south"], + JapanEast: ["🇯🇵", "asia"], + KoreaCentral: ["🇰🇷", "asia"], + AustraliaEast: ["🇦🇺", "australia"], + AustraliaSouthEast: ["🇦🇺", "australia"], + SwedenCentral: ["🇸🇪", "europe"], + UKSouth: ["🇬🇧", "europe"], + WestEurope: ["🇪🇺", "europe"] + }; + static async handleLogin(request, init) { + let bypassServer = getGlobalPref("server.bypassRestriction"); + if (bypassServer !== "off") { + let ip = BypassServerIps[bypassServer]; + ip && request.headers.set("X-Forwarded-For", ip); + } + let response = await NATIVE_FETCH(request, init); + if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response; + let obj = await response.clone().json(); + RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); + let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region; + for (region of obj.offeringSettings.regions) { + let { name: regionName, name: shortName } = region; + if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); + let match = serverRegex.exec(region.baseUri); + if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1]; + else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName); + region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); + } + BxEventBus.Script.emit("xcloud.server.ready", {}); + let preferredRegion = getPreferredServerRegion(); + if (preferredRegion && preferredRegion in STATES.serverRegions) { + let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); + tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; + } + return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; + } + static async handlePlay(request, init) { + BxEventBus.Stream.emit("state.loading", {}); + let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; + for (let regionName in STATES.serverRegions) { + let region = STATES.serverRegions[regionName]; + if (region && parsedUrl.origin === region.baseUri) { + badgeRegion = regionName; + break; + } + } + StreamBadges.getInstance().setRegion(badgeRegion); + let clone = request.clone(), body = await clone.json(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { + let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION); + headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName; + } + if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; + let newRequest = new Request(request, { + body: JSON.stringify(body), + headers + }); + return NATIVE_FETCH(newRequest); + } + static async handleWaitTime(request, init) { + let response = await NATIVE_FETCH(request, init); + if (getGlobalPref("loadingScreen.waitTime.show")) { + let json = await response.clone().json(); + if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); + } + return response; + } + static async handleConfiguration(request, init) { + if (request.method !== "GET") return NATIVE_FETCH(request, init); + if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); + else TouchController.enable(); + let response = await NATIVE_FETCH(request, init), text = await response.clone().text(); + if (!text.length) return response; + BxEventBus.Stream.emit("state.starting", {}); + let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; + overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; + let overrideMkb = null; + if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1; + if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { + enableMouseInput: overrideMkb, + enableKeyboardInput: overrideMkb + }); + if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10; + if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; + return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async handle(request, init) { + let url = typeof request === "string" ? request : request.url; + if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init); + else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init); + else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init); + else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init); + else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request); + return NATIVE_FETCH(request, init); + } +} +function clearApplicationInsightsBuffers() { + window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer"); +} +function clearDbLogs(dbName, table) { + let request = window.indexedDB.open(dbName); + request.onsuccess = (e) => { + let db = e.target.result; + try { + let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); + objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`); + } catch (ex) {} + }; +} +function clearAllLogs() { + clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); +} +function updateIceCandidates(candidates, options) { + let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; + for (let item2 of candidates) { + if (item2.candidate == "a=end-of-candidates") continue; + let groups = pattern.exec(item2.candidate).groups; + lst.push(groups); + } + if (options.preferIpv6Server) lst.sort((a, b) => { + let firstIp = a.ip, secondIp = b.ip; + return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; + }); + let newCandidates = [], foundation = 1, newCandidate = (candidate) => { + return { + candidate, + messageType: "iceCandidate", + sdpMLineIndex: "0", + sdpMid: "0" + }; + }; + if (lst.forEach((item2) => { + item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation; + }), options.consoleAddrs) + for (let ip in options.consoleAddrs) + for (let port of options.consoleAddrs[ip]) + newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`)); + return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates; +} +async function patchIceCandidates(request, consoleAddrs) { + let response = await NATIVE_FETCH(request), text = await response.clone().text(); + if (!text.length) return response; + let options = { + preferIpv6Server: getGlobalPref("server.ipv6.prefer"), + consoleAddrs + }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse); + return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; +} +function interceptHttpRequests() { + let BLOCKED_URLS = []; + if (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net"); + let blockFeatures2 = getGlobalPref("block.features"); + if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox"); + if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations"); + if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/"); + let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send; + xhrPrototype.open = function(method, url) { + return this._url = url, nativeXhrOpen.apply(this, arguments); + }, xhrPrototype.send = function(...arg) { + for (let url of BLOCKED_URLS) + if (this._url.startsWith(url)) { + if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); + return BxLogger.warning("Blocked URL", url), !1; + } + return nativeXhrSend.apply(this, arguments); + }; + let gamepassAllGames = [], IGNORED_DOMAINS = [ + "accounts.xboxlive.com", + "chat.xboxlive.com", + "notificationinbox.xboxlive.com", + "peoplehub.xboxlive.com", + "peoplehub-public.xboxlive.com", + "rta.xboxlive.com", + "userpresence.xboxlive.com", + "xblmessaging.xboxlive.com", + "consent.config.office.com", + "arc.msn.com", + "browser.events.data.microsoft.com", + "dc.services.visualstudio.com", + "2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" + ]; + window.BX_FETCH = window.fetch = async (request, init) => { + let url = typeof request === "string" ? request : request.url; + for (let blocked of BLOCKED_URLS) + if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', { + status: 200, + statusText: "200 OK" + }); + try { + let domain = new URL(url).hostname; + if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init); + } catch (e) { + return NATIVE_FETCH(request, init); + } + if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { + let response = await NATIVE_FETCH(request, init), json = await response.json(); + if (json && json.exp && json.exp.treatments) for (let key in FeatureGates) + json.exp.treatments[key] = FeatureGates[key]; + return response.json = () => Promise.resolve(json), response; + } catch (e) { + return console.log(e), NATIVE_FETCH(request, init); + } + if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { + let response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++) + gamepassAllGames.push(obj[i].id); + else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try { + let customList = TouchController.getCustomList(); + customList = customList.filter((id) => gamepassAllGames.includes(id)); + let newCustomList = customList.map((item2) => ({ id: item2 })); + obj.push(...newCustomList); + } catch (e) { + console.log(e); + } + return response.json = () => Promise.resolve(obj), response; + } + if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) { + let response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + try { + let newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 })); + obj.push(...newCustomList); + } catch (e) { + console.log(e); + } + return response.json = () => Promise.resolve(obj), response; + } + let requestType; + if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome"; + else requestType = "xcloud"; + if (requestType === "xhome") return XhomeInterceptor.handle(request); + return XcloudInterceptor.handle(request, init); + }; +} +function generateMsDeviceInfo(osName) { + return { + appInfo: { + env: { + clientAppId: window.location.host, + clientAppType: "browser", + clientAppVersion: "26.1.97", + clientSdkVersion: "10.3.7", + httpEnvironment: "prod", + sdkInstallId: "" + } + }, + dev: { + os: { name: osName, ver: "22631.2715", platform: "desktop" }, + hw: { make: "Microsoft", model: "unknown", sdktype: "web" }, + browser: { browserName: "chrome", browserVersion: "130.0" }, + displayInfo: { + dimensions: { widthInPixels: 1920, heightInPixels: 1080 }, + pixelDensity: { dpiX: 1, dpiY: 1 } + } + } + }; +} +function getOsNameFromResolution(resolution) { + let osName; + switch (resolution) { + case "1080p-hq": + osName = "tizen"; + break; + case "1080p": + osName = "windows"; + break; + default: + osName = "android"; + break; + } + return osName; +} +function addCss() { + let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = []; + if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); + if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); + if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); + if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); + if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; + if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; + if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; + else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}"; + if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + let $style = CE("style", !1, css); + document.documentElement.appendChild($style); +} +function preloadFonts() { + let $link = CE("link", { + rel: "preload", + href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", + as: "font", + type: "font/otf", + crossorigin: "" + }); + document.querySelector("head")?.appendChild($link); +} +class MouseCursorHider { + static instance; + static getInstance() { + if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider; + else MouseCursorHider.instance = null; + return MouseCursorHider.instance; + } + timeoutId; + isCursorVisible = !0; + show() { + document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0; + } + hide() { + document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1; + } + onMouseMove = (e) => { + !this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000); + }; + start() { + this.show(), document.addEventListener("mousemove", this.onMouseMove); + } + stop() { + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show(); + } +} +function patchHistoryMethod(type) { + let orig = window.history[type]; + return function(...args) { + return BxEvent.dispatch(window, BxEvent.POPSTATE, { + arguments: args + }), orig.apply(this, arguments); + }; +} +function onHistoryChanged(e) { + if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; + window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {}); +} +function setCodecPreferences(sdp, preferredCodec) { + let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; + for (let match of matches) { + let id = match[1]; + if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); + } + if (!preferredCodecIds.length) return sdp; + let lines = sdp.split(`\r +`); + for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { + let line = lines[lineIndex]; + if (!line.startsWith("m=video")) continue; + let tmp = line.trim().split(" "), ids = tmp.slice(3); + ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); + break; + } + return lines.join(`\r +`); +} +function patchSdpBitrate(sdp, video, audio) { + let lines = sdp.split(`\r +`), mediaSet = new Set; + !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); + let bitrate = { + video, + audio + }; + for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) { + let media = "", line = lines[lineNumber]; + if (!line.startsWith("m=")) continue; + for (let m of mediaSet) + if (line.startsWith(`m=${m}`)) { + media = m, mediaSet.delete(media); + break; + } + if (!media) continue; + let bLine = `b=AS:${bitrate[media]}`; + while (lineNumber++, lineNumber < lines.length) { + if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue; + if (line.startsWith("b=AS:")) { + lines[lineNumber] = bLine; + break; + } + if (line.startsWith("m=")) { + lines.splice(lineNumber, 0, bLine); + break; + } + } + } + return lines.join(`\r +`); +} +var clarity_boost_default = `#version 300 es +in vec4 position; +void main() { +gl_Position = position; +}`; +var clarity_boost_default2 = `#version 300 es +precision mediump float; +uniform sampler2D data; +uniform vec2 iResolution; +const int FILTER_UNSHARP_MASKING = 1; +const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0; +const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722); +uniform int filterId; +uniform float sharpenFactor; +uniform float brightness; +uniform float contrast; +uniform float saturation; +out vec4 fragColor; +vec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) { +vec2 texelSize = 1.0 / iResolution.xy; +vec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb; +vec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb; +vec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb; +vec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb; +vec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb; +vec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb; +vec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb; +vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb; +if (filterId == FILTER_UNSHARP_MASKING) { +vec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0; +gaussianBlur /= 16.0; +return e + (e - gaussianBlur) * sharpenFactor / 3.0; +} +vec3 minRgb = min(min(min(d, e), min(f, b)), h); +minRgb += min(min(a, c), min(g, i)); +vec3 maxRgb = max(max(max(d, e), max(f, b)), h); +maxRgb += max(max(a, c), max(g, i)); +vec3 reciprocalMaxRgb = 1.0 / maxRgb; +vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0); +amplifyRgb = inversesqrt(amplifyRgb); +vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK)); +vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0); +vec3 window = b + d + f + h; +vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0); +return mix(e, outColor, sharpenFactor / 2.0); +} +void main() { +vec2 uv = gl_FragCoord.xy / iResolution.xy; +vec3 color = texture(data, uv).rgb; +color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color; +color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color; +color = contrast * (color - 0.5) + 0.5; +color = brightness * color; +fragColor = vec4(color, 1.0); +}`; +class WebGL2Player { + LOG_TAG = "WebGL2Player"; + $video; + $canvas; + gl = null; + resources = []; + program = null; + stopped = !1; + options = { + filterId: 1, + sharpenFactor: 0, + brightness: 0, + contrast: 0, + saturation: 0 + }; + targetFps = 60; + frameInterval = 0; + lastFrameTime = 0; + animFrameId = null; + constructor($video) { + BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; + let $canvas = document.createElement("canvas"); + $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas); + } + setFilter(filterId, update = !0) { + this.options.filterId = filterId, update && this.updateCanvas(); + } + setSharpness(sharpness, update = !0) { + this.options.sharpenFactor = sharpness, update && this.updateCanvas(); + } + setBrightness(brightness, update = !0) { + this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas(); + } + setContrast(contrast, update = !0) { + this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas(); + } + setSaturation(saturation, update = !0) { + this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas(); + } + setTargetFps(target) { + this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0; + } + getCanvas() { + return this.$canvas; + } + updateCanvas() { + let gl = this.gl, program = this.program; + gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation); + } + forceDrawFrame() { + let gl = this.gl; + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); + } + setupRendering() { + let frameCallback; + if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { + let $video = this.$video; + frameCallback = $video.requestVideoFrameCallback.bind($video); + } else frameCallback = requestAnimationFrame; + let animate = () => { + if (this.stopped) return; + this.animFrameId = frameCallback(animate); + let draw = !0; + if (this.targetFps === 0) draw = !1; + else if (this.targetFps < 60) { + let currentTime = performance.now(); + if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1; + else this.lastFrameTime = currentTime; + } + if (draw) { + let gl = this.gl; + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); + } + }; + this.animFrameId = frameCallback(animate); + } + setupShaders() { + BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference")); + let gl = this.$canvas.getContext("webgl2", { + isBx: !0, + antialias: !0, + alpha: !1, + powerPreference: getStreamPref("video.player.powerPreference") + }); + this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); + let vShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); + let fShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); + let program = gl.createProgram(); + if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); + this.updateCanvas(); + let buffer = gl.createBuffer(); + this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); + let texture = gl.createTexture(); + this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); + } + resume() { + this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering(); + } + stop() { + if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) { + if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId); + else cancelAnimationFrame(this.animFrameId); + this.animFrameId = null; + } + } + destroy() { + BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); + let gl = this.gl; + if (gl) { + gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null); + for (let resource of this.resources) + if (resource instanceof WebGLProgram) gl.deleteProgram(resource); + else if (resource instanceof WebGLShader) gl.deleteShader(resource); + else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); + else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); + this.gl = null; + } + if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas); + this.$canvas.width = 1, this.$canvas.height = 1; + } +} +class StreamPlayer { + $video; + playerType = "default"; + options = {}; + webGL2Player = null; + $videoCss = null; + $usmMatrix = null; + constructor($video, type, options) { + this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type); + } + setupVideoElements() { + if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return; + let $fragment = document.createDocumentFragment(); + this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); + let $svg = CE("svg", { + id: "bx-video-filters", + xmlns: "http://www.w3.org/2000/svg", + class: "bx-gone" + }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { + id: "bx-filter-usm", + xmlns: "http://www.w3.org/2000/svg" + }, this.$usmMatrix = CE("feConvolveMatrix", { + id: "bx-filter-usm-matrix", + order: "3", + xmlns: "http://www.w3.org/2000/svg" + })))); + $fragment.appendChild($svg), document.documentElement.appendChild($fragment); + } + getVideoPlayerFilterStyle() { + let filters = [], sharpness = this.options.sharpness || 0; + if (this.options.processing === "usm" && sharpness != 0) { + let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; + this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); + } + let saturation = this.options.saturation || 100; + if (saturation != 100) filters.push(`saturate(${saturation}%)`); + let contrast = this.options.contrast || 100; + if (contrast != 100) filters.push(`contrast(${contrast}%)`); + let brightness = this.options.brightness || 100; + if (brightness != 100) filters.push(`brightness(${brightness}%)`); + return filters.join(" "); + } + resizePlayer() { + let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; + if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); + let targetWidth, targetHeight, targetObjectFit; + if (PREF_RATIO.includes(":")) { + let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect(); + if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio; + else width = parentRect.width, height = width / videoRatio; + width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(); + let $parent = $video.parentElement, position = getStreamPref("video.position"); + if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") { + let padding = Math.floor((window.innerHeight - height) / 4); + if (padding > 0) { + if (position === "bottom-half") padding *= 3; + $parent.style.paddingTop = padding + "px"; + } + } + targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; + } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); + if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize")); + if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); + } + setPlayerType(type, refreshPlayer = !1) { + if (this.playerType !== type) { + let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone"; + if (type === "webgl2") { + if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); + else this.webGL2Player.resume(); + this.$videoCss.textContent = "", this.$video.classList.add(videoClass); + } else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass); + } + this.playerType = type, refreshPlayer && this.refreshPlayer(); + } + setOptions(options, refreshPlayer = !1) { + this.options = options, refreshPlayer && this.refreshPlayer(); + } + updateOptions(options, refreshPlayer = !1) { + this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer(); + } + getPlayerElement(playerType) { + if (typeof playerType === "undefined") playerType = this.playerType; + if (playerType === "webgl2") return this.webGL2Player?.getCanvas(); + return this.$video; + } + getWebGL2Player() { + return this.webGL2Player; + } + refreshPlayer() { + if (this.playerType === "webgl2") { + let options = this.options, webGL2Player = this.webGL2Player; + if (options.processing === "usm") webGL2Player.setFilter(1); + else webGL2Player.setFilter(2); + ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100); + } else { + let filters = this.getVideoPlayerFilterStyle(), videoCss = ""; + if (filters) videoCss += `filter: ${filters} !important;`; + if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); + let css = ""; + if (videoCss) css = `#game-stream video { ${videoCss} }`; + this.$videoCss.textContent = css; + } + this.resizePlayer(); + } + reloadPlayer() { + this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1); + } + cleanUpWebGL2Player() { + this.webGL2Player?.destroy(), this.webGL2Player = null; + } + destroy() { + this.cleanUpWebGL2Player(); + } +} +function patchVideoApi() { + let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() { + if (this.style.visibility = "visible", !this.videoWidth) return; + let playerOptions = { + processing: getStreamPref("video.processing"), + sharpness: getStreamPref("video.processing.sharpness"), + saturation: getStreamPref("video.saturation"), + contrast: getStreamPref("video.contrast"), + brightness: getStreamPref("video.brightness") + }; + STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", { + $video: this + }); + }, nativePlay = HTMLMediaElement.prototype.play; + HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() { + if (this.className && this.className.startsWith("XboxSplashVideo")) { + if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {}); + return nativePlay.apply(this); + } + let $parent = this.parentElement; + if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); + return nativePlay.apply(this); + }; +} +function patchRtcCodecs() { + if (getGlobalPref("stream.video.codecProfile") === "default") return; + if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; +} +function patchRtcPeerConnection() { + let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; + RTCPeerConnection.prototype.createDataChannel = function() { + let dataChannel = nativeCreateDataChannel.apply(this, arguments); + return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel; + }; + let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile"); + if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { + let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; + RTCPeerConnection.prototype.setLocalDescription = function(description) { + if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); + try { + if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + } catch (e) { + BxLogger.error("setLocalDescription", e); + } + return nativeSetLocalDescription.apply(this, arguments); + }; + } + let OrgRTCPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function() { + let conn = new OrgRTCPeerConnection; + return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { + BxLogger.info("connectionstatechange", conn.connectionState); + }), conn; + }; +} +function patchAudioContext() { + let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; + window.AudioContext = function(options) { + if (options && options.latencyHint) options.latencyHint = 0; + let ctx = new OrgAudioContext(options); + return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { + let gainNode = nativeCreateGain.apply(this); + return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + }, STATES.currentStream.audioContext = ctx, ctx; + }; +} +function patchMeControl() { + let overrideConfigs = { + enableAADTelemetry: !1, + enableTelemetry: !1, + telEvs: "", + oneDSUrl: "" + }, MSA = { + MeControl: { + API: { + setDisplayMode: () => {}, + setMobileState: () => {}, + addEventListener: () => {}, + removeEventListener: () => {} + } + } + }, MeControl = {}, MsaHandler = { + get(target, prop, receiver) { + return target[prop]; + }, + set(obj, prop, value) { + if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs); + return obj[prop] = value, !0; + } + }, MeControlHandler = { + get(target, prop, receiver) { + return target[prop]; + }, + set(obj, prop, value) { + if (prop === "Config") value = Object.assign(value, overrideConfigs); + return obj[prop] = value, !0; + } + }; + window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler); +} +function disableAdobeAudienceManager() { + Object.defineProperty(window, "adobe", { + get() { + return Object.freeze({}); + } + }); +} +function patchCanvasContext() { + let nativeGetContext = HTMLCanvasElement.prototype.getContext; + HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) { + if (contextType.includes("webgl")) { + if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) { + if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power"; + } + } + return nativeGetContext.apply(this, [contextType, contextAttributes]); + }; +} +function patchPointerLockApi() { + Object.defineProperty(document, "fullscreenElement", { + configurable: !0, + get() { + return document.documentElement; + } + }), HTMLElement.prototype.requestFullscreen = function(options) { + return Promise.resolve(); + }; + let pointerLockElement = null; + Object.defineProperty(document, "pointerLockElement", { + configurable: !0, + get() { + return pointerLockElement; + } + }), HTMLElement.prototype.requestPointerLock = function() { + pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED)); + }, Document.prototype.exitPointerLock = function() { + pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED)); + }; +} +class BaseGameBarAction { + constructor() {} + reset() {} + onClick(e) { + BxEventBus.Stream.emit("gameBar.activated", {}); + } + render() { + return this.$content; + } +} +class ScreenshotAction extends BaseGameBarAction { + $content; + constructor() { + super(); + this.$content = createButton({ + style: 8, + icon: BxIcon.SCREENSHOT, + title: t("take-screenshot"), + onClick: this.onClick + }); + } + onClick = (e) => { + super.onClick(e), ScreenshotManager.getInstance().takeScreenshot(); + }; +} +class TouchControlAction extends BaseGameBarAction { + $content; + constructor() { + super(); + let $btnEnable = createButton({ + style: 8, + icon: BxIcon.TOUCH_CONTROL_ENABLE, + title: t("show-touch-controller"), + onClick: this.onClick + }), $btnDisable = createButton({ + style: 8, + icon: BxIcon.TOUCH_CONTROL_DISABLE, + title: t("hide-touch-controller"), + onClick: this.onClick, + classes: ["bx-activated"] + }); + this.$content = CE("div", !1, $btnEnable, $btnDisable); + } + onClick = (e) => { + super.onClick(e); + let isVisible = TouchController.toggleVisibility(); + this.$content.dataset.activated = (!isVisible).toString(); + }; + reset() { + this.$content.dataset.activated = "false"; + } +} +class MicrophoneAction extends BaseGameBarAction { + $content; + constructor() { + super(); + let $btnDefault = createButton({ + style: 8, + icon: BxIcon.MICROPHONE, + onClick: this.onClick, + classes: ["bx-activated"] + }), $btnMuted = createButton({ + style: 8, + icon: BxIcon.MICROPHONE_MUTED, + onClick: this.onClick + }); + this.$content = CE("div", !1, $btnMuted, $btnDefault), BxEventBus.Stream.on("microphone.state.changed", (payload) => { + let enabled = payload.state === "Enabled"; + this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); + }); + } + onClick = (e) => { + super.onClick(e); + let enabled = MicrophoneShortcut.toggle(!1); + this.$content.dataset.activated = enabled.toString(); + }; + reset() { + this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false"; + } +} +class TrueAchievementsAction extends BaseGameBarAction { + $content; + constructor() { + super(); + this.$content = createButton({ + style: 8, + icon: BxIcon.TRUE_ACHIEVEMENTS, + onClick: this.onClick + }); + } + onClick = (e) => { + super.onClick(e), TrueAchievements.getInstance().open(!1); + }; +} +class SpeakerAction extends BaseGameBarAction { + $content; + constructor() { + super(); + let $btnEnable = createButton({ + style: 8, + icon: BxIcon.AUDIO, + onClick: this.onClick + }), $btnMuted = createButton({ + style: 8, + icon: BxIcon.SPEAKER_MUTED, + onClick: this.onClick, + classes: ["bx-activated"] + }); + this.$content = CE("div", !1, $btnEnable, $btnMuted), BxEventBus.Stream.on("speaker.state.changed", (payload) => { + let enabled = payload.state === 0; + this.$content.dataset.activated = (!enabled).toString(); + }); + } + onClick = (e) => { + super.onClick(e), SoundShortcut.muteUnmute(); + }; + reset() { + this.$content.dataset.activated = "false"; + } +} +class RendererAction extends BaseGameBarAction { + $content; + constructor() { + super(); + let $btnDefault = createButton({ + style: 8, + icon: BxIcon.EYE, + onClick: this.onClick + }), $btnActivated = createButton({ + style: 8, + icon: BxIcon.EYE_SLASH, + onClick: this.onClick, + classes: ["bx-activated"] + }); + this.$content = CE("div", !1, $btnDefault, $btnActivated), BxEventBus.Stream.on("video.visibility.changed", (payload) => { + this.$content.dataset.activated = (!payload.isVisible).toString(); + }); + } + onClick = (e) => { + super.onClick(e), RendererShortcut.toggleVisibility(); + }; + reset() { + this.$content.dataset.activated = "false"; + } +} +class GameBar { + static instance; + static getInstance() { + if (typeof GameBar.instance === "undefined") if (getGlobalPref("gameBar.position") !== "off") GameBar.instance = new GameBar; + else GameBar.instance = null; + return GameBar.instance; + } + LOG_TAG = "GameBar"; + static VISIBLE_DURATION = 2000; + $gameBar; + $container; + timeoutId = null; + actions = []; + constructor() { + BxLogger.info(this.LOG_TAG, "constructor()"); + let $container, position = getGlobalPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); + if (this.actions = [ + new ScreenshotAction, + ...STATES.userAgent.capabilities.touch && getGlobalPref("touchController.mode") !== "off" ? [new TouchControlAction] : [], + new SpeakerAction, + new RendererAction, + new MicrophoneAction, + new TrueAchievementsAction + ], position === "bottom-right") + this.actions.reverse(); + for (let action of this.actions) + $container.appendChild(action.render()); + $gameBar.addEventListener("click", (e) => { + if (e.target !== $gameBar) return; + $container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); + }), BxEventBus.Stream.on("gameBar.activated", this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => { + $container.classList.replace("bx-hide", "bx-offscreen"); + }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { + if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable(); + }).bind(this)); + } + beginHideTimeout = () => { + this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => { + this.timeoutId = null, this.hideBar(); + }, GameBar.VISIBLE_DURATION); + }; + clearHideTimeout = () => { + this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null; + }; + enable() { + this.$gameBar.classList.remove("bx-gone"); + } + disable() { + this.hideBar(), this.$gameBar.classList.add("bx-gone"); + } + showBar() { + this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout(); + } + hideBar = () => { + this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide"); + }; + reset() { + for (let action of this.actions) + action.reset(); + } +} +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 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; + } +} +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); +if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") { + window.stop(); + let css = ""; + css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}'; + let isSafari = UserAgent.isSafari(), $secondaryAction; + if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading")); + else $secondaryAction = CE("a", { + href: "https://better-xcloud.github.io/troubleshooting", + target: "_blank" + }, "🤓 " + t("how-to-fix")); + let $fragment = document.createDocumentFragment(); + throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", { + class: "bx-reload-overlay" + }, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari"); +} +window.addEventListener("load", (e) => { + window.setTimeout(() => { + if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0); + }, 3000); +}); +document.addEventListener("readystatechange", (e) => { + if (document.readyState !== "interactive") return; + if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); + else window.setTimeout(HeaderSection.watchHeader, 2000); + if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) { + let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); + $parent && ($parent.style.display = "none"); + } + preloadFonts(); +}); +window.BX_EXPOSED = BxExposed; +window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); +window.addEventListener("popstate", onHistoryChanged); +window.history.pushState = patchHistoryMethod("pushState"); +window.history.replaceState = patchHistoryMethod("replaceState"); +BxEventBus.Script.once("xcloud.server.unavailable", () => { + if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); +}); +BxEventBus.Script.on("xcloud.server.ready", () => { + STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); +}); +BxEventBus.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.error", () => { + 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.streamPlayer?.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(); +}); +function main() { + if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") { + let customList = getGlobalPref("nativeMkb.forcedGames"); + BX_FLAGS.ForceNativeMkbTitles.push(...customList); + } + if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect(); + if (getGlobalPref("touchController.mode") === "all") TouchController.setup(); + if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); + if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); +} +main(); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 3b089ac..768f05b 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -13,4817 +13,127 @@ // @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js // ==/UserScript== "use strict"; -class BxLogger { -static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args); -static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args); -static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args); -static log(color, tag, ...args) { -console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args); -} -} +class BxLogger {static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args);static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args);static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);static log(color, tag, ...args) {console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);}} window.BxLogger = BxLogger; /* ADDITIONAL CODE */ -var DEFAULT_FLAGS = { -Debug: !1, -CheckForUpdate: !0, -EnableXcloudLogging: !1, -SafariWorkaround: !0, -ForceNativeMkbTitles: [], -FeatureGates: null, -DeviceInfo: { -deviceType: "unknown" -} -}, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); -try { -delete window.BX_FLAGS; -} catch (e) {} +var DEFAULT_FLAGS = {Debug: !1,CheckForUpdate: !0,EnableXcloudLogging: !1,SafariWorkaround: !0,ForceNativeMkbTitles: [],FeatureGates: null,DeviceInfo: {deviceType: "unknown"}}, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {}); +try {delete window.BX_FLAGS;} catch (e) {} if (!BX_FLAGS.DeviceInfo.userAgent) BX_FLAGS.DeviceInfo.userAgent = window.navigator.userAgent; BxLogger.info("BxFlags", BX_FLAGS); var NATIVE_FETCH = window.fetch; -var ALL_PREFS = { -global: [ -"audio.mic.onPlaying", -"audio.volume.booster.enabled", -"block.features", -"block.tracking", -"gameBar.position", -"game.fortnite.forceConsole", -"loadingScreen.gameArt.show", -"loadingScreen.rocket", -"loadingScreen.waitTime.show", -"mkb.enabled", -"mkb.cursor.hideIdle", -"nativeMkb.forcedGames", -"nativeMkb.mode", -"xhome.enabled", -"xhome.video.resolution", -"screenshot.applyFilters", -"server.bypassRestriction", -"server.ipv6.prefer", -"server.region", -"stream.video.codecProfile", -"stream.video.combineAudio", -"stream.video.maxBitrate", -"stream.locale", -"stream.video.resolution", -"touchController.autoOff", -"touchController.opacity.default", -"touchController.mode", -"touchController.style.custom", -"touchController.style.standard", -"ui.controllerFriendly", -"ui.controllerStatus.show", -"ui.feedbackDialog.disabled", -"ui.gameCard.waitTime.show", -"ui.hideSections", -"ui.systemMenu.hideHandle", -"ui.imageQuality", -"ui.layout", -"ui.reduceAnimations", -"ui.hideScrollbar", -"ui.streamMenu.simplify", -"ui.splashVideo.skip", -"version.current", -"version.lastCheck", -"version.latest", -"bx.locale", -"userAgent.profile" -], -stream: [ -"audio.volume", -"controller.pollingRate", -"controller.settings", -"deviceVibration.intensity", -"deviceVibration.mode", -"keyboardShortcuts.preset.inGameId", -"localCoOp.enabled", -"mkb.p1.preset.mappingId", -"mkb.p1.slot", -"mkb.p2.preset.mappingId", -"mkb.p2.slot", -"nativeMkb.scroll.sensitivityX", -"nativeMkb.scroll.sensitivityY", -"stats.colors", -"stats.items", -"stats.opacity.all", -"stats.opacity.background", -"stats.position", -"stats.quickGlance.enabled", -"stats.showWhenPlaying", -"stats.textSize", -"video.brightness", -"video.contrast", -"video.maxFps", -"video.player.type", -"video.position", -"video.player.powerPreference", -"video.processing", -"video.ratio", -"video.saturation", -"video.processing.sharpness" -] -}; +var ALL_PREFS = {global: ["audio.mic.onPlaying","audio.volume.booster.enabled","block.features","block.tracking","gameBar.position","game.fortnite.forceConsole","loadingScreen.gameArt.show","loadingScreen.rocket","loadingScreen.waitTime.show","mkb.enabled","mkb.cursor.hideIdle","nativeMkb.forcedGames","nativeMkb.mode","xhome.enabled","xhome.video.resolution","screenshot.applyFilters","server.bypassRestriction","server.ipv6.prefer","server.region","stream.video.codecProfile","stream.video.combineAudio","stream.video.maxBitrate","stream.locale","stream.video.resolution","touchController.autoOff","touchController.opacity.default","touchController.mode","touchController.style.custom","touchController.style.standard","ui.controllerFriendly","ui.controllerStatus.show","ui.feedbackDialog.disabled","ui.gameCard.waitTime.show","ui.hideSections","ui.systemMenu.hideHandle","ui.imageQuality","ui.layout","ui.reduceAnimations","ui.hideScrollbar","ui.streamMenu.simplify","ui.splashVideo.skip","version.current","version.lastCheck","version.latest","bx.locale","userAgent.profile"],stream: ["audio.volume","controller.pollingRate","controller.settings","deviceVibration.intensity","deviceVibration.mode","keyboardShortcuts.preset.inGameId","localCoOp.enabled","mkb.p1.preset.mappingId","mkb.p1.slot","mkb.p2.preset.mappingId","mkb.p2.slot","nativeMkb.scroll.sensitivityX","nativeMkb.scroll.sensitivityY","stats.colors","stats.items","stats.opacity.all","stats.opacity.background","stats.position","stats.quickGlance.enabled","stats.showWhenPlaying","stats.textSize","video.brightness","video.contrast","video.maxFps","video.player.type","video.position","video.player.powerPreference","video.processing","video.ratio","video.saturation","video.processing.sharpness"]}; var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0"; -if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { -let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); -if (match) CHROMIUM_VERSION = match[1]; -} -class UserAgent { -static STORAGE_KEY = "BetterXcloud.UserAgent"; -static #config; -static #isMobile = null; -static #isSafari = null; -static #isSafariMobile = null; -static #USER_AGENTS = { -"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`, -"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1", -"smarttv-generic": `${window.navigator.userAgent} Smart-TV`, -"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`, -"vr-oculus": window.navigator.userAgent + " OculusBrowser VR" -}; -static init() { -if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default"; -if (!UserAgent.#config.custom) UserAgent.#config.custom = ""; -UserAgent.spoof(); -} -static updateStorage(profile, custom) { -let config = UserAgent.#config; -if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom; -window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config)); -} -static getDefault() { -return window.navigator.orgUserAgent || window.navigator.userAgent; -} -static get(profile) { -let defaultUserAgent = window.navigator.userAgent; -switch (profile) { -case "default": -return defaultUserAgent; -case "custom": -return UserAgent.#config.custom || defaultUserAgent; -default: -return UserAgent.#USER_AGENTS[profile] || defaultUserAgent; -} -} -static isSafari() { -if (this.#isSafari !== null) return this.#isSafari; -let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom"); -return this.#isSafari = result, result; -} -static isSafariMobile() { -if (this.#isSafariMobile !== null) return this.#isSafariMobile; -let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); -return this.#isSafariMobile = result, result; -} -static isMobile() { -if (this.#isMobile !== null) return this.#isMobile; -let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); -return this.#isMobile = result, result; -} -static spoof() { -let profile = UserAgent.#config.profile; -if (profile === "default") return; -let newUserAgent = UserAgent.get(profile); -if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {}); -window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", { -value: newUserAgent -}); -} -} +if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);if (match) CHROMIUM_VERSION = match[1];} +class UserAgent {static STORAGE_KEY = "BetterXcloud.UserAgent";static #config;static #isMobile = null;static #isSafari = null;static #isSafariMobile = null;static #USER_AGENTS = {"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1","smarttv-generic": `${window.navigator.userAgent} Smart-TV`,"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"};static init() {if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";if (!UserAgent.#config.custom) UserAgent.#config.custom = "";UserAgent.spoof();}static updateStorage(profile, custom) {let config = UserAgent.#config;if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));}static getDefault() {return window.navigator.orgUserAgent || window.navigator.userAgent;}static get(profile) {let defaultUserAgent = window.navigator.userAgent;switch (profile) {case "default":return defaultUserAgent;case "custom":return UserAgent.#config.custom || defaultUserAgent;default:return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;}}static isSafari() {if (this.#isSafari !== null) return this.#isSafari;let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");return this.#isSafari = result, result;}static isSafariMobile() {if (this.#isSafariMobile !== null) return this.#isSafariMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");return this.#isSafariMobile = result, result;}static isMobile() {if (this.#isMobile !== null) return this.#isMobile;let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);return this.#isMobile = result, result;}static spoof() {let profile = UserAgent.#config.profile;if (profile === "default") return;let newUserAgent = UserAgent.get(profile);if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {value: newUserAgent});}} var SCRIPT_VERSION = "6.3.0-beta-1", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; UserAgent.init(); -var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = { -supportedRegion: !0, -serverRegions: {}, -selectedRegion: {}, -gsToken: "", -isSignedIn: !1, -isPlaying: !1, -browser: { -capabilities: { -touch: browserHasTouchSupport, -batteryApi: "getBattery" in window.navigator, -deviceVibration: !!window.navigator.vibrate, -mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/), -emulatedNativeMkb: !!AppInterface -} -}, -userAgent: { -isTv, -capabilities: { -touch: userAgentHasTouchSupport, -mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/) -} -}, -currentStream: {}, -remotePlay: {}, -pointerServerPort: 9269 -}; -function deepClone(obj) { -if (!obj) return {}; -if ("structuredClone" in window) return structuredClone(obj); -return JSON.parse(JSON.stringify(obj)); -} +var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {supportedRegion: !0,serverRegions: {},selectedRegion: {},gsToken: "",isSignedIn: !1,isPlaying: !1,browser: {capabilities: {touch: browserHasTouchSupport,batteryApi: "getBattery" in window.navigator,deviceVibration: !!window.navigator.vibrate,mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),emulatedNativeMkb: !!AppInterface}},userAgent: {isTv,capabilities: {touch: userAgentHasTouchSupport,mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)}},currentStream: {},remotePlay: {},pointerServerPort: 9269}; +function deepClone(obj) {if (!obj) return {};if ("structuredClone" in window) return structuredClone(obj);return JSON.parse(JSON.stringify(obj));} var BxEvent; -((BxEvent) => { -BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready"; -function dispatch(target, eventName, data) { -if (!target) return; -if (!eventName) { -alert("BxEvent.dispatch(): eventName is null"); -return; -} -let event = new Event(eventName); -if (data) for (let key in data) -event[key] = data[key]; -target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data); -} -BxEvent.dispatch = dispatch; -})(BxEvent ||= {}); +((BxEvent) => {BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready";function dispatch(target, eventName, data) {if (!target) return;if (!eventName) {alert("BxEvent.dispatch(): eventName is null");return;}let event = new Event(eventName);if (data) for (let key in data)event[key] = data[key];target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data);}BxEvent.dispatch = dispatch;})(BxEvent ||= {}); window.BxEvent = BxEvent; -var GamepadKeyName = { -0: ["A", "⇓"], -1: ["B", "⇒"], -2: ["X", "⇐"], -3: ["Y", "⇑"], -4: ["LB", "↘"], -5: ["RB", "↙"], -6: ["LT", "↖"], -7: ["RT", "↗"], -8: ["Select", "⇺"], -9: ["Start", "⇻"], -16: ["Home", ""], -12: ["D-Pad Up", "≻"], -13: ["D-Pad Down", "≽"], -14: ["D-Pad Left", "≺"], -15: ["D-Pad Right", "≼"], -10: ["L3", "↺"], -100: ["Left Stick Up", "↾"], -101: ["Left Stick Down", "⇂"], -102: ["Left Stick Left", "↼"], -103: ["Left Stick Right", "⇀"], -104: ["Left Stick", "⇱"], -11: ["R3", "↻"], -200: ["Right Stick Up", "↿"], -201: ["Right Stick Down", "⇃"], -202: ["Right Stick Left", "↽"], -203: ["Right Stick Right", "⇁"], -204: ["Right Stick", "⇲"], -17: ["Screenshot", "⇧"] -}; -class BxEventBus { -listeners = new Map; -group; -appJsInterfaces; -static Script = new BxEventBus("script", { -"dialog.shown": "onDialogShown", -"dialog.dismissed": "onDialogDismissed" -}); -static Stream = new BxEventBus("stream", { -"state.loading": "onStreamPlaying", -"state.playing": "onStreamPlaying", -"state.stopped": "onStreamStopped" -}); -constructor(group, appJsInterfaces) { -this.group = group, this.appJsInterfaces = appJsInterfaces; -} -on(event, callback) { -if (!this.listeners.has(event)) this.listeners.set(event, new Set); -this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback); -} -once(event, callback) { -let wrapper = (...args) => { -callback(...args), this.off(event, wrapper); -}; -this.on(event, wrapper); -} -off(event, callback) { -if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) { -this.listeners.delete(event); -return; -} -let callbacks = this.listeners.get(event); -if (!callbacks) return; -if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event); -} -offAll() { -this.listeners.clear(); -} -emit(event, payload) { -let callbacks = this.listeners.get(event) || []; -for (let callback of callbacks) -callback(payload); -if (AppInterface) try { -if (event in this.appJsInterfaces) { -let method = this.appJsInterfaces[event]; -AppInterface[method] && AppInterface[method](); -} else AppInterface.onEventBus(this.group + "." + event); -} catch (e) { -console.log(e); -} -BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload); -} -} +var GamepadKeyName = {0: ["A", "⇓"],1: ["B", "⇒"],2: ["X", "⇐"],3: ["Y", "⇑"],4: ["LB", "↘"],5: ["RB", "↙"],6: ["LT", "↖"],7: ["RT", "↗"],8: ["Select", "⇺"],9: ["Start", "⇻"],16: ["Home", ""],12: ["D-Pad Up", "≻"],13: ["D-Pad Down", "≽"],14: ["D-Pad Left", "≺"],15: ["D-Pad Right", "≼"],10: ["L3", "↺"],100: ["Left Stick Up", "↾"],101: ["Left Stick Down", "⇂"],102: ["Left Stick Left", "↼"],103: ["Left Stick Right", "⇀"],104: ["Left Stick", "⇱"],11: ["R3", "↻"],200: ["Right Stick Up", "↿"],201: ["Right Stick Down", "⇃"],202: ["Right Stick Left", "↽"],203: ["Right Stick Right", "⇁"],204: ["Right Stick", "⇲"],17: ["Screenshot", "⇧"]}; +class BxEventBus {listeners = new Map;group;appJsInterfaces;static Script = new BxEventBus("script", {"dialog.shown": "onDialogShown","dialog.dismissed": "onDialogDismissed"});static Stream = new BxEventBus("stream", {"state.loading": "onStreamPlaying","state.playing": "onStreamPlaying","state.stopped": "onStreamStopped"});constructor(group, appJsInterfaces) {this.group = group, this.appJsInterfaces = appJsInterfaces;}on(event, callback) {if (!this.listeners.has(event)) this.listeners.set(event, new Set);this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);}once(event, callback) {let wrapper = (...args) => {callback(...args), this.off(event, wrapper);};this.on(event, wrapper);}off(event, callback) {if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {this.listeners.delete(event);return;}let callbacks = this.listeners.get(event);if (!callbacks) return;if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);}offAll() {this.listeners.clear();}emit(event, payload) {let callbacks = this.listeners.get(event) || [];for (let callback of callbacks)callback(payload);if (AppInterface) try {if (event in this.appJsInterfaces) {let method = this.appJsInterfaces[event];AppInterface[method] && AppInterface[method]();} else AppInterface.onEventBus(this.group + "." + event);} catch (e) {console.log(e);}BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);}} window.BxEventBus = BxEventBus; -class GhPagesUtils { -static fetchLatestCommit() { -NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", { -method: "GET", -headers: { -Accept: "application/vnd.github.v3+json" -} -}).then((response) => response.json()).then((data) => { -let latestCommitHash = data.commit.sha; -window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash); -}).catch((error) => { -BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error); -}); -} -static getUrl(path) { -if (path[0] === "/") alert('`path` must not starts with "/"'); -let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash"); -if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`; -else return `${prefix}/refs/heads/gh-pages/${path}`; -} -static getNativeMkbCustomList(update = !1) { -let key = "BetterXcloud.GhPages.ForceNativeMkb"; -update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => { -if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", { -data: json -}); -else window.localStorage.removeItem(key); -}); -let info = JSON.parse(window.localStorage.getItem(key) || "{}"); -if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {}; -return info.data; -} -static getTouchControlCustomList() { -let key = "BetterXcloud.GhPages.CustomTouchLayouts"; -return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => { -if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json)); -}), JSON.parse(window.localStorage.getItem(key) || "[]"); -} -static getLocalCoOpList() { -let key = "BetterXcloud.GhPages.LocalCoOp"; -NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => { -if (json.$schemaVersion === 1) { -window.localStorage.setItem(key, JSON.stringify(json)); -let ids = new Set(Object.keys(json.data)); -BxEventBus.Script.emit("list.localCoOp.updated", { ids }); -} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set }); -}); -let info = JSON.parse(window.localStorage.getItem(key) || "{}"); -if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set; -return new Set(Object.keys(info.data || {})); -} -} -var SUPPORTED_LANGUAGES = { -"en-US": "English (US)", -"ca-CA": "Català", -"da-DK": "dansk", -"de-DE": "Deutsch", -"en-ID": "Bahasa Indonesia", -"es-ES": "español (España)", -"fr-FR": "français", -"it-IT": "italiano", -"ja-JP": "日本語", -"ko-KR": "한국어", -"pl-PL": "polski", -"pt-BR": "português (Brasil)", -"ru-RU": "русский", -"th-TH": "ภาษาไทย", -"tr-TR": "Türkçe", -"uk-UA": "українська", -"vi-VN": "Tiếng Việt", -"zh-CN": "中文(简体)", -"zh-TW": "中文(繁體)" -}, Texts = { -achievements: "Achievements", -activate: "Activate", -activated: "Activated", -active: "Active", -advanced: "Advanced", -"all-games": "All games", -"always-off": "Always off", -"always-on": "Always on", -"amd-fidelity-cas": "AMD FidelityFX CAS", -"app-settings": "App settings", -apply: "Apply", -"aspect-ratio": "Aspect ratio", -"aspect-ratio-note": "Don't use with native touch games", -audio: "Audio", -auto: "Auto", -availability: "Availability", -"back-to-home": "Back to home", -"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", -"background-opacity": "Background opacity", -battery: "Battery", -"battery-saving": "Battery saving", -"better-xcloud": "Better xCloud", -"bitrate-audio-maximum": "Maximum audio bitrate", -"bitrate-video-maximum": "Maximum video bitrate", -bottom: "Bottom", -"bottom-half": "Bottom half", -"bottom-left": "Bottom-left", -"bottom-right": "Bottom-right", -brazil: "Brazil", -brightness: "Brightness", -"browser-unsupported-feature": "Your browser doesn't support this feature", -"button-xbox": "Xbox button", -"bypass-region-restriction": "Bypass region restriction", -"can-stream-xbox-360-games": "Can stream Xbox 360 games", -cancel: "Cancel", -"cant-stream-xbox-360-games": "Can't stream Xbox 360 games", -center: "Center", -chat: "Chat", -"clarity-boost": "Clarity boost", -"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", -clear: "Clear", -"clear-data": "Clear data", -"clear-data-confirm": "Do you want to clear all Better xCloud settings and data?", -"clear-data-success": "Data cleared! Refresh the page to apply the changes.", -clock: "Clock", -close: "Close", -"close-app": "Close app", -"combine-audio-video-streams": "Combine audio & video streams", -"combine-audio-video-streams-summary": "May fix the laggy audio problem", -"conditional-formatting": "Conditional formatting text color", -"confirm-delete-preset": "Do you want to delete this preset?", -"confirm-reload-stream": "Do you want to refresh the stream?", -connected: "Connected", -"console-connect": "Connect", -"continent-asia": "Asia", -"continent-australia": "Australia", -"continent-europe": "Europe", -"continent-north-america": "North America", -"continent-south-america": "South America", -contrast: "Contrast", -controller: "Controller", -"controller-customization": "Controller customization", -"controller-customization-input-latency-note": "May slightly increase input latency", -"controller-friendly-ui": "Controller-friendly UI", -"controller-shortcuts": "Controller shortcuts", -"controller-shortcuts-connect-note": "Connect a controller to use this feature", -"controller-shortcuts-xbox-note": "Button to open the Guide menu", -"controller-vibration": "Controller vibration", -copy: "Copy", -"create-shortcut": "Shortcut", -custom: "Custom", -"deadzone-counterweight": "Deadzone counterweight", -decrease: "Decrease", -default: "Default", -"default-opacity": "Default opacity", -"default-preset-note": "You can't modify default presets. Create a new one to customize it.", -delete: "Delete", -"detect-controller-button": "Detect controller button", -device: "Device", -"device-unsupported-touch": "Your device doesn't have touch support", -"device-vibration": "Device vibration", -"device-vibration-not-using-gamepad": "On when not using gamepad", -disable: "Disable", -"disable-features": "Disable features", -"disable-home-context-menu": "Disable context menu in Home page", -"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog", -"disable-social-features": "Disable social features", -"disable-xcloud-analytics": "Disable xCloud analytics", -disabled: "Disabled", -disconnected: "Disconnected", -download: "Download", -downloaded: "Downloaded", -edit: "Edit", -"enable-controller-shortcuts": "Enable controller shortcuts", -"enable-local-co-op-support": "Enable local co-op support", -"enable-local-co-op-support-note": "Only works with some games", -"enable-mic-on-startup": "Enable microphone on game launch", -"enable-mkb": "Emulate controller with Mouse & Keyboard", -"enable-quick-glance-mode": 'Enable "Quick Glance" mode', -"enable-remote-play-feature": 'Enable the "Remote Play" feature', -"enable-volume-control": "Enable volume control feature", -enabled: "Enabled", -experimental: "Experimental", -export: "Export", -fast: "Fast", -"force-native-mkb-games": "Force native Mouse & Keyboard for these games", -"fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile', -"fortnite-force-console-version": "Fortnite: force console version", -"friends-followers": "Friends and followers", -"game-bar": "Game Bar", -"getting-consoles-list": "Getting the list of consoles...", -guide: "Guide", -help: "Help", -hide: "Hide", -"hide-idle-cursor": "Hide mouse cursor on idle", -"hide-scrollbar": "Hide web page's scrollbar", -"hide-sections": "Hide sections", -"hide-system-menu-icon": "Hide System menu's icon", -"hide-touch-controller": "Hide touch controller", -"high-performance": "High performance", -"highest-quality": "Highest quality", -"highest-quality-note": "Your device may not be powerful enough to use these settings", -"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity", -"horizontal-sensitivity": "Horizontal sensitivity", -"how-to-fix": "How to fix", -"how-to-improve-app-performance": "How to improve app's performance", -ignore: "Ignore", -"image-quality": "Website's image quality", -import: "Import", -"in-game-controller-customization": "In-game controller customization", -"in-game-controller-shortcuts": "In-game controller shortcuts", -"in-game-keyboard-shortcuts": "In-game keyboard shortcuts", -"in-game-shortcuts": "In-game shortcuts", -increase: "Increase", -"install-android": "Better xCloud app for Android", -invites: "Invites", -japan: "Japan", -jitter: "Jitter", -"keyboard-key": "Keyboard key", -"keyboard-shortcuts": "Keyboard shortcuts", -korea: "Korea", -language: "Language", -large: "Large", -layout: "Layout", -"left-stick": "Left stick", -"left-stick-deadzone": "Left stick deadzone", -"left-trigger-range": "Left trigger range", -"limit-fps": "Limit FPS", -"load-failed-message": "Failed to run Better xCloud", -"loading-screen": "Loading screen", -"local-co-op": "Local co-op", -"lowest-quality": "Lowest quality", -manage: "Manage", -"map-mouse-to": "Map mouse to", -"may-not-work-properly": "May not work properly!", -menu: "Menu", -microphone: "Microphone", -"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings", -"mkb-click-to-activate": "Click to activate", -"mkb-disclaimer": "This could be viewed as cheating when playing online", -"modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.", -"mouse-and-keyboard": "Mouse & Keyboard", -"mouse-click": "Mouse click", -"mouse-wheel": "Mouse wheel", -muted: "Muted", -name: "Name", -"native-mkb": "Native Mouse & Keyboard", -new: "New", -"new-version-available": [ -e => `Version ${e.version} available`, -e => `Versió ${e.version} disponible`, -, -e => `Version ${e.version} verfügbar`, -e => `Versi ${e.version} tersedia`, -e => `Versión ${e.version} disponible`, -e => `Version ${e.version} disponible`, -e => `Disponibile la versione ${e.version}`, -e => `Ver ${e.version} が利用可能です`, -e => `${e.version} 버전 사용가능`, -e => `Dostępna jest nowa wersja ${e.version}`, -e => `Versão ${e.version} disponível`, -e => `Версия ${e.version} доступна`, -e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`, -e => `${e.version} sayılı yeni sürüm mevcut`, -e => `Доступна версія ${e.version}`, -e => `Đã có phiên bản ${e.version}`, -e => `版本 ${e.version} 可供更新`, -e => `已可更新為 ${e.version} 版` -], -"no-consoles-found": "No consoles found", -"no-controllers-connected": "No controllers connected", -normal: "Normal", -notifications: "Notifications", -off: "Off", -official: "Official", -on: "On", -"only-supports-some-games": "Only supports some games", -opacity: "Opacity", -other: "Other", -playing: "Playing", -playtime: "Playtime", -poland: "Poland", -"polling-rate": "Polling rate", -position: "Position", -"powered-off": "Powered off", -"powered-on": "Powered on", -"prefer-ipv6-server": "Prefer IPv6 server", -"preferred-game-language": "Preferred game's language", -preset: "Preset", -press: "Press", -"press-any-button": "Press any button...", -"press-esc-to-cancel": "Press Esc to cancel", -"press-key-to-toggle-mkb": [ -e => `Press ${e.key} to toggle this feature`, -e => `Premeu ${e.key} per alternar aquesta funció`, -e => `Tryk på ${e.key} for at slå denne funktion til`, -e => `${e.key}: Funktion an-/ausschalten`, -e => `Tekan ${e.key} untuk mengaktifkan fitur ini`, -e => `Pulsa ${e.key} para alternar esta función`, -e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`, -e => `Premi ${e.key} per attivare questa funzionalità`, -e => `${e.key} でこの機能を切替`, -e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`, -e => `Naciśnij ${e.key} aby przełączyć tę funkcję`, -e => `Pressione ${e.key} para alternar este recurso`, -e => `Нажмите ${e.key} для переключения этой функции`, -e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`, -e => `Etkinleştirmek için ${e.key} tuşuna basın`, -e => `Натисніть ${e.key} щоб перемкнути цю функцію`, -e => `Nhấn ${e.key} để bật/tắt tính năng này`, -e => `按下 ${e.key} 来切换此功能`, -e => `按下 ${e.key} 來啟用此功能` -], -"press-to-bind": "Press a key or do a mouse click to bind...", -"prompt-preset-name": "Preset's name:", -recommended: "Recommended", -"recommended-settings-for-device": [ -e => `Recommended settings for ${e.device}`, -e => `Configuració recomanada per a ${e.device}`, -, -e => `Empfohlene Einstellungen für ${e.device}`, -e => `Rekomendasi pengaturan untuk ${e.device}`, -e => `Ajustes recomendados para ${e.device}`, -e => `Paramètres recommandés pour ${e.device}`, -e => `Configurazioni consigliate per ${e.device}`, -e => `${e.device} の推奨設定`, -e => `다음 기기에서 권장되는 설정: ${e.device}`, -e => `Zalecane ustawienia dla ${e.device}`, -e => `Configurações recomendadas para ${e.device}`, -e => `Рекомендуемые настройки для ${e.device}`, -e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`, -e => `${e.device} için önerilen ayarlar`, -e => `Рекомендовані налаштування для ${e.device}`, -e => `Cấu hình được đề xuất cho ${e.device}`, -e => `${e.device} 的推荐设置`, -e => `${e.device} 推薦的設定` -], -"reduce-animations": "Reduce UI animations", -region: "Region", -"reload-page": "Reload page", -"remote-play": "Remote Play", -rename: "Rename", -renderer: "Renderer", -"renderer-configuration": "Renderer configuration", -"reset-highlighted-setting": "Reset highlighted setting", -"right-click-to-unbind": "Right-click on a key to unbind it", -"right-stick": "Right stick", -"right-stick-deadzone": "Right stick deadzone", -"right-trigger-range": "Right trigger range", -"rocket-always-hide": "Always hide", -"rocket-always-show": "Always show", -"rocket-animation": "Rocket animation", -"rocket-hide-queue": "Hide when queuing", -saturation: "Saturation", -save: "Save", -screen: "Screen", -"screenshot-apply-filters": "Apply video filters to screenshots", -"section-all-games": "All games", -"section-most-popular": "Most popular", -"section-native-mkb": "Play with mouse & keyboard", -"section-news": "News", -"section-play-with-friends": "Play with friends", -"section-touch": "Play with touch", -"separate-touch-controller": "Separate Touch controller & Controller #1", -"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2", -server: "Server", -"server-locations": "Server locations", -settings: "Settings", -"settings-for": "Settings for", -"settings-reload": "Reload page to reflect changes", -"settings-reload-note": "Settings in this tab only go into effect on the next page load", -"settings-reloading": "Reloading...", -sharpness: "Sharpness", -"shortcut-keys": "Shortcut keys", -show: "Show", -"show-controller-connection-status": "Show controller connection status", -"show-game-art": "Show game art", -"show-hide": "Show/hide", -"show-stats-on-startup": "Show stats when starting the game", -"show-touch-controller": "Show touch controller", -"show-wait-time": "Show the estimated wait time", -"show-wait-time-in-game-card": "Show wait time in game card", -"simplify-stream-menu": "Simplify Stream's menu", -"skip-splash-video": "Skip Xbox splash video", -slow: "Slow", -small: "Small", -"smart-tv": "Smart TV", -sound: "Sound", -standard: "Standard", -standby: "Standby", -"stat-bitrate": "Bitrate", -"stat-decode-time": "Decode time", -"stat-fps": "FPS", -"stat-frames-lost": "Frames lost", -"stat-packets-lost": "Packets lost", -"stat-ping": "Ping", -stats: "Stats", -"stick-decay-minimum": "Stick decay minimum", -"stick-decay-strength": "Stick decay strength", -stream: "Stream", -"stream-settings": "Stream settings", -"stream-stats": "Stream stats", -"stream-your-own-game": "Stream your own game", -stretch: "Stretch", -"suggest-settings": "Suggest settings", -"suggest-settings-link": "Suggest recommended settings for this device", -"support-better-xcloud": "Support Better xCloud", -"swap-buttons": "Swap buttons", -"take-screenshot": "Take screenshot", -"target-resolution": "Target resolution", -"tc-all-white": "All white", -"tc-auto-off": "Off when controller found", -"tc-custom-layout-style": "Custom layout's button style", -"tc-muted-colors": "Muted colors", -"tc-standard-layout-style": "Standard layout's button style", -"text-size": "Text size", -toggle: "Toggle", -top: "Top", -"top-center": "Top-center", -"top-half": "Top half", -"top-left": "Top-left", -"top-right": "Top-right", -"touch-control-layout": "Touch control layout", -"touch-control-layout-by": [ -e => `Touch control layout by ${e.name}`, -e => `Format del control tàctil per ${e.name}`, -e => `Touch-kontrol layout af ${e.name}`, -e => `Touch-Steuerungslayout von ${e.name}`, -e => `Tata letak Sentuhan layar oleh ${e.name}`, -e => `Disposición del control táctil por ${e.nombre}`, -e => `Disposition du contrôleur tactile par ${e.name}`, -e => `Configurazione dei comandi su schermo creata da ${e.name}`, -e => `タッチ操作レイアウト作成者: ${e.name}`, -e => `${e.name} 제작, 터치 컨트롤 레이아웃`, -e => `Układ sterowania dotykowego stworzony przez ${e.name}`, -e => `Disposição de controle por toque feito por ${e.name}`, -e => `Сенсорная раскладка по ${e.name}`, -e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`, -e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`, -e => `Розташування сенсорного керування від ${e.name}`, -e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`, -e => `由 ${e.name} 提供的虚拟按键样式`, -e => `觸控遊玩佈局由 ${e.name} 提供` -], -"touch-controller": "Touch controller", -"true-achievements": "TrueAchievements", -ui: "UI", -"unexpected-behavior": "May cause unexpected behavior", -"united-states": "United States", -unknown: "Unknown", -unlimited: "Unlimited", -unmuted: "Unmuted", -unofficial: "Unofficial", -"unofficial-game-list": "Unofficial game list", -"unsharp-masking": "Unsharp masking", -upload: "Upload", -uploaded: "Uploaded", -"use-mouse-absolute-position": "Use mouse's absolute position", -"use-this-at-your-own-risk": "Use this at your own risk", -"user-agent-profile": "User-Agent profile", -"vertical-scroll-sensitivity": "Vertical scroll sensitivity", -"vertical-sensitivity": "Vertical sensitivity", -"vibration-intensity": "Vibration intensity", -"vibration-status": "Vibration", -video: "Video", -"virtual-controller": "Virtual controller", -"virtual-controller-slot": "Virtual controller slot", -"visual-quality": "Visual quality", -"visual-quality-high": "High", -"visual-quality-low": "Low", -"visual-quality-normal": "Normal", -volume: "Volume", -"wait-time-countdown": "Countdown", -"wait-time-estimated": "Estimated finish time", -"waiting-for-input": "Waiting for input...", -wallpaper: "Wallpaper", -webgl2: "WebGL2" -}; -class Translations { -static EN_US = "en-US"; -static KEY_LOCALE = "BetterXcloud.Locale"; -static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations"; -static selectedLocaleIndex = -1; -static selectedLocale = "en-US"; -static supportedLocales = Object.keys(SUPPORTED_LANGUAGES); -static foreignTranslations = {}; -static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US); -static async init() { -Translations.refreshLocale(), await Translations.loadTranslations(); -} -static refreshLocale(newLocale) { -let locale; -if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale; -else locale = localStorage.getItem(Translations.KEY_LOCALE); -let supportedLocales = Translations.supportedLocales; -if (!locale) { -if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US; -localStorage.setItem(Translations.KEY_LOCALE, locale); -} -Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale); -} -static get(key, values) { -let text = null; -if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key]; -if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`); -let translation; -if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values); -return translation = text, translation; -} -static async loadTranslations() { -if (Translations.selectedLocale === Translations.EN_US) return; -try { -Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS)); -} catch (e) {} -if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale); -} -static async updateTranslations(async = !1) { -if (Translations.selectedLocale === Translations.EN_US) { -localStorage.removeItem(Translations.KEY_TRANSLATIONS); -return; -} -if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale); -else await Translations.downloadTranslations(Translations.selectedLocale); -} -static async downloadTranslations(locale) { -try { -let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json(); -if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; -return !0; -} catch (e) { -debugger; -} -return !1; -} -static downloadTranslationsAsync(locale) { -NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => { -window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations; -}); -} -static switchLocale(locale) { -localStorage.setItem(Translations.KEY_LOCALE, locale); -} -} +class GhPagesUtils {static fetchLatestCommit() {NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {method: "GET",headers: {Accept: "application/vnd.github.v3+json"}}).then((response) => response.json()).then((data) => {let latestCommitHash = data.commit.sha;window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);}).catch((error) => {BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);});}static getUrl(path) {if (path[0] === "/") alert('`path` must not starts with "/"');let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;else return `${prefix}/refs/heads/gh-pages/${path}`;}static getNativeMkbCustomList(update = !1) {let key = "BetterXcloud.GhPages.ForceNativeMkb";update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {data: json});else window.localStorage.removeItem(key);});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};return info.data;}static getTouchControlCustomList() {let key = "BetterXcloud.GhPages.CustomTouchLayouts";return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));}), JSON.parse(window.localStorage.getItem(key) || "[]");}static getLocalCoOpList() {let key = "BetterXcloud.GhPages.LocalCoOp";NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {if (json.$schemaVersion === 1) {window.localStorage.setItem(key, JSON.stringify(json));let ids = new Set(Object.keys(json.data));BxEventBus.Script.emit("list.localCoOp.updated", { ids });} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });});let info = JSON.parse(window.localStorage.getItem(key) || "{}");if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;return new Set(Object.keys(info.data || {}));}} +var SUPPORTED_LANGUAGES = {"en-US": "English (US)","ca-CA": "Català","da-DK": "dansk","de-DE": "Deutsch","en-ID": "Bahasa Indonesia","es-ES": "español (España)","fr-FR": "français","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","pl-PL": "polski","pt-BR": "português (Brasil)","ru-RU": "русский","th-TH": "ภาษาไทย","tr-TR": "Türkçe","uk-UA": "українська","vi-VN": "Tiếng Việt","zh-CN": "中文(简体)","zh-TW": "中文(繁體)"}, Texts = {achievements: "Achievements",activate: "Activate",activated: "Activated",active: "Active",advanced: "Advanced","all-games": "All games","always-off": "Always off","always-on": "Always on","amd-fidelity-cas": "AMD FidelityFX CAS","app-settings": "App settings",apply: "Apply","aspect-ratio": "Aspect ratio","aspect-ratio-note": "Don't use with native touch games",audio: "Audio",auto: "Auto",availability: "Availability","back-to-home": "Back to home","back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?","background-opacity": "Background opacity",battery: "Battery","battery-saving": "Battery saving","better-xcloud": "Better xCloud","bitrate-audio-maximum": "Maximum audio bitrate","bitrate-video-maximum": "Maximum video bitrate",bottom: "Bottom","bottom-half": "Bottom half","bottom-left": "Bottom-left","bottom-right": "Bottom-right",brazil: "Brazil",brightness: "Brightness","browser-unsupported-feature": "Your browser doesn't support this feature","button-xbox": "Xbox button","bypass-region-restriction": "Bypass region restriction","can-stream-xbox-360-games": "Can stream Xbox 360 games",cancel: "Cancel","cant-stream-xbox-360-games": "Can't stream Xbox 360 games",center: "Center",chat: "Chat","clarity-boost": "Clarity boost","clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",clear: "Clear","clear-data": "Clear data","clear-data-confirm": "Do you want to clear all Better xCloud settings and data?","clear-data-success": "Data cleared! Refresh the page to apply the changes.",clock: "Clock",close: "Close","close-app": "Close app","combine-audio-video-streams": "Combine audio & video streams","combine-audio-video-streams-summary": "May fix the laggy audio problem","conditional-formatting": "Conditional formatting text color","confirm-delete-preset": "Do you want to delete this preset?","confirm-reload-stream": "Do you want to refresh the stream?",connected: "Connected","console-connect": "Connect","continent-asia": "Asia","continent-australia": "Australia","continent-europe": "Europe","continent-north-america": "North America","continent-south-america": "South America",contrast: "Contrast",controller: "Controller","controller-customization": "Controller customization","controller-customization-input-latency-note": "May slightly increase input latency","controller-friendly-ui": "Controller-friendly UI","controller-shortcuts": "Controller shortcuts","controller-shortcuts-connect-note": "Connect a controller to use this feature","controller-shortcuts-xbox-note": "Button to open the Guide menu","controller-vibration": "Controller vibration",copy: "Copy","create-shortcut": "Shortcut",custom: "Custom","deadzone-counterweight": "Deadzone counterweight",decrease: "Decrease",default: "Default","default-opacity": "Default opacity","default-preset-note": "You can't modify default presets. Create a new one to customize it.",delete: "Delete","detect-controller-button": "Detect controller button",device: "Device","device-unsupported-touch": "Your device doesn't have touch support","device-vibration": "Device vibration","device-vibration-not-using-gamepad": "On when not using gamepad",disable: "Disable","disable-features": "Disable features","disable-home-context-menu": "Disable context menu in Home page","disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog","disable-social-features": "Disable social features","disable-xcloud-analytics": "Disable xCloud analytics",disabled: "Disabled",disconnected: "Disconnected",download: "Download",downloaded: "Downloaded",edit: "Edit","enable-controller-shortcuts": "Enable controller shortcuts","enable-local-co-op-support": "Enable local co-op support","enable-local-co-op-support-note": "Only works with some games","enable-mic-on-startup": "Enable microphone on game launch","enable-mkb": "Emulate controller with Mouse & Keyboard","enable-quick-glance-mode": 'Enable "Quick Glance" mode',"enable-remote-play-feature": 'Enable the "Remote Play" feature',"enable-volume-control": "Enable volume control feature",enabled: "Enabled",experimental: "Experimental",export: "Export",fast: "Fast","force-native-mkb-games": "Force native Mouse & Keyboard for these games","fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',"fortnite-force-console-version": "Fortnite: force console version","friends-followers": "Friends and followers","game-bar": "Game Bar","getting-consoles-list": "Getting the list of consoles...",guide: "Guide",help: "Help",hide: "Hide","hide-idle-cursor": "Hide mouse cursor on idle","hide-scrollbar": "Hide web page's scrollbar","hide-sections": "Hide sections","hide-system-menu-icon": "Hide System menu's icon","hide-touch-controller": "Hide touch controller","high-performance": "High performance","highest-quality": "Highest quality","highest-quality-note": "Your device may not be powerful enough to use these settings","horizontal-scroll-sensitivity": "Horizontal scroll sensitivity","horizontal-sensitivity": "Horizontal sensitivity","how-to-fix": "How to fix","how-to-improve-app-performance": "How to improve app's performance",ignore: "Ignore","image-quality": "Website's image quality",import: "Import","in-game-controller-customization": "In-game controller customization","in-game-controller-shortcuts": "In-game controller shortcuts","in-game-keyboard-shortcuts": "In-game keyboard shortcuts","in-game-shortcuts": "In-game shortcuts",increase: "Increase","install-android": "Better xCloud app for Android",invites: "Invites",japan: "Japan",jitter: "Jitter","keyboard-key": "Keyboard key","keyboard-shortcuts": "Keyboard shortcuts",korea: "Korea",language: "Language",large: "Large",layout: "Layout","left-stick": "Left stick","left-stick-deadzone": "Left stick deadzone","left-trigger-range": "Left trigger range","limit-fps": "Limit FPS","load-failed-message": "Failed to run Better xCloud","loading-screen": "Loading screen","local-co-op": "Local co-op","lowest-quality": "Lowest quality",manage: "Manage","map-mouse-to": "Map mouse to","may-not-work-properly": "May not work properly!",menu: "Menu",microphone: "Microphone","mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings","mkb-click-to-activate": "Click to activate","mkb-disclaimer": "This could be viewed as cheating when playing online","modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.","mouse-and-keyboard": "Mouse & Keyboard","mouse-click": "Mouse click","mouse-wheel": "Mouse wheel",muted: "Muted",name: "Name","native-mkb": "Native Mouse & Keyboard",new: "New","new-version-available": [e => `Version ${e.version} available`,e => `Versió ${e.version} disponible`,,e => `Version ${e.version} verfügbar`,e => `Versi ${e.version} tersedia`,e => `Versión ${e.version} disponible`,e => `Version ${e.version} disponible`,e => `Disponibile la versione ${e.version}`,e => `Ver ${e.version} が利用可能です`,e => `${e.version} 버전 사용가능`,e => `Dostępna jest nowa wersja ${e.version}`,e => `Versão ${e.version} disponível`,e => `Версия ${e.version} доступна`,e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,e => `${e.version} sayılı yeni sürüm mevcut`,e => `Доступна версія ${e.version}`,e => `Đã có phiên bản ${e.version}`,e => `版本 ${e.version} 可供更新`,e => `已可更新為 ${e.version} 版`],"no-consoles-found": "No consoles found","no-controllers-connected": "No controllers connected",normal: "Normal",notifications: "Notifications",off: "Off",official: "Official",on: "On","only-supports-some-games": "Only supports some games",opacity: "Opacity",other: "Other",playing: "Playing",playtime: "Playtime",poland: "Poland","polling-rate": "Polling rate",position: "Position","powered-off": "Powered off","powered-on": "Powered on","prefer-ipv6-server": "Prefer IPv6 server","preferred-game-language": "Preferred game's language",preset: "Preset",press: "Press","press-any-button": "Press any button...","press-esc-to-cancel": "Press Esc to cancel","press-key-to-toggle-mkb": [e => `Press ${e.key} to toggle this feature`,e => `Premeu ${e.key} per alternar aquesta funció`,e => `Tryk på ${e.key} for at slå denne funktion til`,e => `${e.key}: Funktion an-/ausschalten`,e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,e => `Pulsa ${e.key} para alternar esta función`,e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,e => `Premi ${e.key} per attivare questa funzionalità`,e => `${e.key} でこの機能を切替`,e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,e => `Pressione ${e.key} para alternar este recurso`,e => `Нажмите ${e.key} для переключения этой функции`,e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,e => `Etkinleştirmek için ${e.key} tuşuna basın`,e => `Натисніть ${e.key} щоб перемкнути цю функцію`,e => `Nhấn ${e.key} để bật/tắt tính năng này`,e => `按下 ${e.key} 来切换此功能`,e => `按下 ${e.key} 來啟用此功能`],"press-to-bind": "Press a key or do a mouse click to bind...","prompt-preset-name": "Preset's name:",recommended: "Recommended","recommended-settings-for-device": [e => `Recommended settings for ${e.device}`,e => `Configuració recomanada per a ${e.device}`,,e => `Empfohlene Einstellungen für ${e.device}`,e => `Rekomendasi pengaturan untuk ${e.device}`,e => `Ajustes recomendados para ${e.device}`,e => `Paramètres recommandés pour ${e.device}`,e => `Configurazioni consigliate per ${e.device}`,e => `${e.device} の推奨設定`,e => `다음 기기에서 권장되는 설정: ${e.device}`,e => `Zalecane ustawienia dla ${e.device}`,e => `Configurações recomendadas para ${e.device}`,e => `Рекомендуемые настройки для ${e.device}`,e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,e => `${e.device} için önerilen ayarlar`,e => `Рекомендовані налаштування для ${e.device}`,e => `Cấu hình được đề xuất cho ${e.device}`,e => `${e.device} 的推荐设置`,e => `${e.device} 推薦的設定`],"reduce-animations": "Reduce UI animations",region: "Region","reload-page": "Reload page","remote-play": "Remote Play",rename: "Rename",renderer: "Renderer","renderer-configuration": "Renderer configuration","reset-highlighted-setting": "Reset highlighted setting","right-click-to-unbind": "Right-click on a key to unbind it","right-stick": "Right stick","right-stick-deadzone": "Right stick deadzone","right-trigger-range": "Right trigger range","rocket-always-hide": "Always hide","rocket-always-show": "Always show","rocket-animation": "Rocket animation","rocket-hide-queue": "Hide when queuing",saturation: "Saturation",save: "Save",screen: "Screen","screenshot-apply-filters": "Apply video filters to screenshots","section-all-games": "All games","section-most-popular": "Most popular","section-native-mkb": "Play with mouse & keyboard","section-news": "News","section-play-with-friends": "Play with friends","section-touch": "Play with touch","separate-touch-controller": "Separate Touch controller & Controller #1","separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",server: "Server","server-locations": "Server locations",settings: "Settings","settings-for": "Settings for","settings-reload": "Reload page to reflect changes","settings-reload-note": "Settings in this tab only go into effect on the next page load","settings-reloading": "Reloading...",sharpness: "Sharpness","shortcut-keys": "Shortcut keys",show: "Show","show-controller-connection-status": "Show controller connection status","show-game-art": "Show game art","show-hide": "Show/hide","show-stats-on-startup": "Show stats when starting the game","show-touch-controller": "Show touch controller","show-wait-time": "Show the estimated wait time","show-wait-time-in-game-card": "Show wait time in game card","simplify-stream-menu": "Simplify Stream's menu","skip-splash-video": "Skip Xbox splash video",slow: "Slow",small: "Small","smart-tv": "Smart TV",sound: "Sound",standard: "Standard",standby: "Standby","stat-bitrate": "Bitrate","stat-decode-time": "Decode time","stat-fps": "FPS","stat-frames-lost": "Frames lost","stat-packets-lost": "Packets lost","stat-ping": "Ping",stats: "Stats","stick-decay-minimum": "Stick decay minimum","stick-decay-strength": "Stick decay strength",stream: "Stream","stream-settings": "Stream settings","stream-stats": "Stream stats","stream-your-own-game": "Stream your own game",stretch: "Stretch","suggest-settings": "Suggest settings","suggest-settings-link": "Suggest recommended settings for this device","support-better-xcloud": "Support Better xCloud","swap-buttons": "Swap buttons","take-screenshot": "Take screenshot","target-resolution": "Target resolution","tc-all-white": "All white","tc-auto-off": "Off when controller found","tc-custom-layout-style": "Custom layout's button style","tc-muted-colors": "Muted colors","tc-standard-layout-style": "Standard layout's button style","text-size": "Text size",toggle: "Toggle",top: "Top","top-center": "Top-center","top-half": "Top half","top-left": "Top-left","top-right": "Top-right","touch-control-layout": "Touch control layout","touch-control-layout-by": [e => `Touch control layout by ${e.name}`,e => `Format del control tàctil per ${e.name}`,e => `Touch-kontrol layout af ${e.name}`,e => `Touch-Steuerungslayout von ${e.name}`,e => `Tata letak Sentuhan layar oleh ${e.name}`,e => `Disposición del control táctil por ${e.nombre}`,e => `Disposition du contrôleur tactile par ${e.name}`,e => `Configurazione dei comandi su schermo creata da ${e.name}`,e => `タッチ操作レイアウト作成者: ${e.name}`,e => `${e.name} 제작, 터치 컨트롤 레이아웃`,e => `Układ sterowania dotykowego stworzony przez ${e.name}`,e => `Disposição de controle por toque feito por ${e.name}`,e => `Сенсорная раскладка по ${e.name}`,e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,e => `Розташування сенсорного керування від ${e.name}`,e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,e => `由 ${e.name} 提供的虚拟按键样式`,e => `觸控遊玩佈局由 ${e.name} 提供`],"touch-controller": "Touch controller","true-achievements": "TrueAchievements",ui: "UI","unexpected-behavior": "May cause unexpected behavior","united-states": "United States",unknown: "Unknown",unlimited: "Unlimited",unmuted: "Unmuted",unofficial: "Unofficial","unofficial-game-list": "Unofficial game list","unsharp-masking": "Unsharp masking",upload: "Upload",uploaded: "Uploaded","use-mouse-absolute-position": "Use mouse's absolute position","use-this-at-your-own-risk": "Use this at your own risk","user-agent-profile": "User-Agent profile","vertical-scroll-sensitivity": "Vertical scroll sensitivity","vertical-sensitivity": "Vertical sensitivity","vibration-intensity": "Vibration intensity","vibration-status": "Vibration",video: "Video","virtual-controller": "Virtual controller","virtual-controller-slot": "Virtual controller slot","visual-quality": "Visual quality","visual-quality-high": "High","visual-quality-low": "Low","visual-quality-normal": "Normal",volume: "Volume","wait-time-countdown": "Countdown","wait-time-estimated": "Estimated finish time","waiting-for-input": "Waiting for input...",wallpaper: "Wallpaper",webgl2: "WebGL2"}; +class Translations {static EN_US = "en-US";static KEY_LOCALE = "BetterXcloud.Locale";static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";static selectedLocaleIndex = -1;static selectedLocale = "en-US";static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);static foreignTranslations = {};static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);static async init() {Translations.refreshLocale(), await Translations.loadTranslations();}static refreshLocale(newLocale) {let locale;if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;else locale = localStorage.getItem(Translations.KEY_LOCALE);let supportedLocales = Translations.supportedLocales;if (!locale) {if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;localStorage.setItem(Translations.KEY_LOCALE, locale);}Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);}static get(key, values) {let text = null;if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);let translation;if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);return translation = text, translation;}static async loadTranslations() {if (Translations.selectedLocale === Translations.EN_US) return;try {Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));} catch (e) {}if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);}static async updateTranslations(async = !1) {if (Translations.selectedLocale === Translations.EN_US) {localStorage.removeItem(Translations.KEY_TRANSLATIONS);return;}if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);else await Translations.downloadTranslations(Translations.selectedLocale);}static async downloadTranslations(locale) {try {let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;return !0;} catch (e) {debugger;}return !1;}static downloadTranslationsAsync(locale) {NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;});}static switchLocale(locale) {localStorage.setItem(Translations.KEY_LOCALE, locale);}} var t = Translations.get; Translations.init(); -class NavigationUtils { -static setNearby($elm, nearby) { -$elm.nearby = $elm.nearby || {}; -let key; -for (key in nearby) -$elm.nearby[key] = nearby[key]; -} -} +class NavigationUtils {static setNearby($elm, nearby) {$elm.nearby = $elm.nearby || {};let key;for (key in nearby)$elm.nearby[key] = nearby[key];}} var setNearby = NavigationUtils.setNearby; -var ButtonStyleClass = { -1: "bx-primary", -2: "bx-warning", -4: "bx-danger", -8: "bx-ghost", -16: "bx-frosted", -32: "bx-drop-shadow", -64: "bx-focusable", -128: "bx-full-width", -256: "bx-full-height", -512: "bx-auto-height", -1024: "bx-tall", -2048: "bx-circular", -4096: "bx-normal-case", -8192: "bx-normal-link" -}; -function createElement(elmName, props, ..._) { -let $elm, hasNs = props && "xmlns" in props; -if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; -else $elm = document.createElement(elmName); -if (props) { -if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; -if (props._on) { -for (let name in props._on) -$elm.addEventListener(name, props._on[name]); -delete props._on; -} -if (props._dataset) { -for (let name in props._dataset) -$elm.dataset[name] = props._dataset[name]; -delete props._dataset; -} -for (let key in props) { -if ($elm.hasOwnProperty(key)) continue; -let value = props[key]; -if (hasNs) $elm.setAttributeNS(null, key, value); -else $elm.setAttribute(key, value); -} -} -for (let i = 2, size = arguments.length;i < size; i++) { -let arg = arguments[i]; -if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg); -} -return $elm; -} +var ButtonStyleClass = {1: "bx-primary",2: "bx-warning",4: "bx-danger",8: "bx-ghost",16: "bx-frosted",32: "bx-drop-shadow",64: "bx-focusable",128: "bx-full-width",256: "bx-full-height",512: "bx-auto-height",1024: "bx-tall",2048: "bx-circular",4096: "bx-normal-case",8192: "bx-normal-link"}; +function createElement(elmName, props, ..._) {let $elm, hasNs = props && "xmlns" in props;if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;else $elm = document.createElement(elmName);if (props) {if (props._nearby) setNearby($elm, props._nearby), delete props._nearby;if (props._on) {for (let name in props._on)$elm.addEventListener(name, props._on[name]);delete props._on;}if (props._dataset) {for (let name in props._dataset)$elm.dataset[name] = props._dataset[name];delete props._dataset;}for (let key in props) {if ($elm.hasOwnProperty(key)) continue;let value = props[key];if (hasNs) $elm.setAttributeNS(null, key, value);else $elm.setAttribute(key, value);}}for (let i = 2, size = arguments.length;i < size; i++) {let arg = arguments[i];if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg);}return $elm;} var domParser = new DOMParser; -function createSvgIcon(icon) { -return domParser.parseFromString(icon, "image/svg+xml").documentElement; -} +function createSvgIcon(icon) {return domParser.parseFromString(icon, "image/svg+xml").documentElement;} var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)); -function createButton(options) { -let $btn; -if (options.url) $btn = CE("a", { -class: "bx-button", -href: options.url, -target: "_blank" -}); -else $btn = CE("button", { -class: "bx-button", -type: "button" -}), options.disabled && ($btn.disabled = !0); -let style = options.style || 0; -if (style) { -let index; -for (index of ButtonStyleIndices) -style & index && $btn.classList.add(ButtonStyleClass[index]); -} -if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText)); -for (let key in options.attributes) -if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); -return $btn; -} -function createSettingRow(label, $control, options = {}) { -let $label, $row = CE("label", { -class: "bx-settings-row" -}, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control); -if (options.pref) $row.prefKey = options.pref; -if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu); -let $link = $label.querySelector("a"); -if ($link) $link.classList.add("bx-focusable"), setNearby($label, { -focus: $link -}); -if (setNearby($row, { -orientation: options.multiLines ? "vertical" : "horizontal" -}), options.multiLines) -$row.dataset.multiLines = "true"; -if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id; -return $row; -} -function getReactProps($elm) { -for (let key in $elm) -if (key.startsWith("__reactProps")) return $elm[key]; -return null; -} -function escapeHtml(html) { -let text = document.createTextNode(html), $span = document.createElement("span"); -return $span.appendChild(text), $span.innerHTML; -} -function isElementVisible($elm) { -let rect = $elm.getBoundingClientRect(); -return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; -} -function removeChildElements($parent) { -if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select"); -while ($parent.firstElementChild) -$parent.firstElementChild.remove(); -} -function clearDataSet($elm) { -Object.keys($elm.dataset).forEach((key) => { -delete $elm.dataset[key]; -}); -} -function renderPresetsList($select, allPresets, selectedValue, options = {}) { -if (removeChildElements($select), options.addOffValue) { -let $option = CE("option", { value: 0 }, t("off")); -$option.selected = selectedValue === 0, $select.appendChild($option); -} -let groups = { -default: t("default") + " 🔒", -custom: t("custom") -}, key; -for (key in groups) { -let $optGroup = CE("optgroup", { label: groups[key] }); -for (let id of allPresets[key]) { -let record = allPresets.data[id], selected = selectedValue === record.id, name = options.selectedIndicator && selected ? "✅ " + record.name : record.name, $option = CE("option", { value: record.id }, name); -if (selected) $option.selected = !0; -$optGroup.appendChild($option); -} -if ($optGroup.hasChildNodes()) $select.appendChild($optGroup); -} -} -function calculateSelectBoxes($root) { -let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select")); -for (let $select of selects) { -let $parent = $select.parentElement; -if ($parent.classList.contains("bx-full-width")) { -$parent.dataset.calculated = "true"; -continue; -} -let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width); -if (!width) continue; -if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) { -if ($select.multiple) width += 20; -if ($select.querySelector("optgroup")) width -= 15; -} else width += 10; -$select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; -} -} +function createButton(options) {let $btn;if (options.url) $btn = CE("a", {class: "bx-button",href: options.url,target: "_blank"});else $btn = CE("button", {class: "bx-button",type: "button"}), options.disabled && ($btn.disabled = !0);let style = options.style || 0;if (style) {let index;for (index of ButtonStyleIndices)style & index && $btn.classList.add(ButtonStyleClass[index]);}if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText));for (let key in options.attributes)if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]);return $btn;} +function createSettingRow(label, $control, options = {}) {let $label, $row = CE("label", {class: "bx-settings-row"}, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control);if (options.pref) $row.prefKey = options.pref;if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu);let $link = $label.querySelector("a");if ($link) $link.classList.add("bx-focusable"), setNearby($label, {focus: $link});if (setNearby($row, {orientation: options.multiLines ? "vertical" : "horizontal"}), options.multiLines)$row.dataset.multiLines = "true";if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id;return $row;} +function getReactProps($elm) {for (let key in $elm)if (key.startsWith("__reactProps")) return $elm[key];return null;} +function escapeHtml(html) {let text = document.createTextNode(html), $span = document.createElement("span");return $span.appendChild(text), $span.innerHTML;} +function isElementVisible($elm) {let rect = $elm.getBoundingClientRect();return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;} +function removeChildElements($parent) {if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select");while ($parent.firstElementChild)$parent.firstElementChild.remove();} +function clearDataSet($elm) {Object.keys($elm.dataset).forEach((key) => {delete $elm.dataset[key];});} +function renderPresetsList($select, allPresets, selectedValue, options = {}) {if (removeChildElements($select), options.addOffValue) {let $option = CE("option", { value: 0 }, t("off"));$option.selected = selectedValue === 0, $select.appendChild($option);}let groups = {default: t("default") + " 🔒",custom: t("custom")}, key;for (key in groups) {let $optGroup = CE("optgroup", { label: groups[key] });for (let id of allPresets[key]) {let record = allPresets.data[id], selected = selectedValue === record.id, name = options.selectedIndicator && selected ? "✅ " + record.name : record.name, $option = CE("option", { value: record.id }, name);if (selected) $option.selected = !0;$optGroup.appendChild($option);}if ($optGroup.hasChildNodes()) $select.appendChild($optGroup);}} +function calculateSelectBoxes($root) {let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select"));for (let $select of selects) {let $parent = $select.parentElement;if ($parent.classList.contains("bx-full-width")) {$parent.dataset.calculated = "true";continue;}let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width);if (!width) continue;if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) {if ($select.multiple) width += 20;if ($select.querySelector("optgroup")) width -= 15;} else width += 10;$select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true";}} var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; -function humanFileSize(size) { -let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); -return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i]; -} -function secondsToHm(seconds) { -let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; -if (m === 60) h += 1, m = 0; -let output = []; -return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); -} -function escapeCssSelector(name) { -return name.replaceAll(".", "-"); -} +function humanFileSize(size) {let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i];} +function secondsToHm(seconds) {let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1;if (m === 60) h += 1, m = 0;let output = [];return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" ");} +function escapeCssSelector(name) {return name.replaceAll(".", "-");} var CE = createElement; window.BX_CE = createElement; -class Toast { -static instance; -static getInstance = () => Toast.instance ?? (Toast.instance = new Toast); -LOG_TAG = "Toast"; -$wrapper; -$msg; -$status; -stack = []; -isShowing = !1; -timeoutId; -DURATION = 3000; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => { -let classList = this.$wrapper.classList; -if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext(); -}), document.documentElement.appendChild(this.$wrapper); -} -show(msg, status, options = {}) { -options = options || {}; -let args = Array.from(arguments); -if (options.instant) this.stack = [args], this.showNext(); -else this.stack.push(args), !this.isShowing && this.showNext(); -} -showNext() { -if (!this.stack.length) { -this.isShowing = !1; -return; -} -this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION); -let [msg, status, options] = this.stack.shift(); -if (options && options.html) this.$msg.innerHTML = msg; -else this.$msg.textContent = msg; -if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status; -else this.$status.classList.add("bx-gone"); -let classList = this.$wrapper.classList; -classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); -} -hide = () => { -this.timeoutId = null; -let classList = this.$wrapper.classList; -classList.remove("bx-show"), classList.add("bx-hide"); -}; -static show(msg, status, options = {}) { -Toast.getInstance().show(msg, status, options); -} -static showNext() { -Toast.getInstance().showNext(); -} -} -class MicrophoneShortcut { -static toggle(showToast = !0) { -if (!window.BX_EXPOSED.streamSession) return !1; -let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0; -try { -return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic; -} catch (e) { -console.log(e); -} -return !1; -} -} -class LocalDb { -static instance; -static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb); -static DB_NAME = "BetterXcloud"; -static DB_VERSION = 4; -static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers"; -static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts"; -static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations"; -static TABLE_CONTROLLER_SETTINGS = "controller_settings"; -static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts"; -db; -open() { -return new Promise((resolve, reject) => { -if (this.db) { -resolve(this.db); -return; -} -let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); -request.onupgradeneeded = (e) => { -let db = e.target.result; -if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined"); -if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, { -keyPath: "id" -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, { -keyPath: "id", -autoIncrement: !0 -}); -if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, { -keyPath: "id", -autoIncrement: !0 -}); -}, request.onerror = (e) => { -console.log(e), alert(e.target.error.message), reject && reject(); -}, request.onsuccess = (e) => { -this.db = e.target.result, resolve(this.db); -}; -}); -} -} -var BypassServers = { -br: t("brazil"), -jp: t("japan"), -kr: t("korea"), -pl: t("poland"), -us: t("united-states") -}, BypassServerIps = { -br: "169.150.198.66", -kr: "121.125.60.151", -jp: "138.199.21.239", -pl: "45.134.212.66", -us: "143.244.47.65" -}; -class BaseSettingsStorage { -storage; -storageKey; -_settings; -definitions; -constructor(storageKey, definitions) { -this.storage = window.localStorage, this.storageKey = storageKey; -for (let [_, setting] of Object.entries(definitions)) { -if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; -setting.ready && setting.ready.call(this, setting); -} -this.definitions = definitions, this._settings = null; -} -get settings() { -if (this._settings) return this._settings; -let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); -for (let key in settings) { -if (!this.definitions.hasOwnProperty(key)) { -delete settings[key]; -continue; -} -settings[key] = this.validateValue("get", key, settings[key]); -} -return this._settings = settings, settings; -} -getDefinition(key) { -if (!this.definitions[key]) return alert("Request invalid definition: " + key), {}; -return this.definitions[key]; -} -hasSetting(key) { -return key in this.settings; -} -getSetting(key, checkUnsupported = !0) { -let definition = this.definitions[key]; -if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; -if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue; -else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default; -if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null); -return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key]; -} -setSetting(key, value, origin) { -if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", { -storageKey: this.storageKey, -settingKey: key -}); -else BxEventBus.Script.emit("setting.changed", { -storageKey: this.storageKey, -settingKey: key -}); -return value; -} -saveSettings() { -this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); -} -validateValue(action, key, value) { -let def = this.definitions[key]; -if (!def) return value; -if (typeof value === "undefined" || value === null) value = def.default; -if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value); -if ("min" in def) value = Math.max(def.min, value); -if ("max" in def) value = Math.min(def.max, value); -if ("options" in def) { -if (!(value in def.options)) value = def.default; -} else if ("multipleOptions" in def) { -if (value.length) { -let validOptions = Object.keys(def.multipleOptions); -value.forEach((item2, idx) => { -validOptions.indexOf(item2) === -1 && value.splice(idx, 1); -}); -} -if (!value.length) value = def.default; -} -if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value); -return value; -} -getLabel(key) { -return this.definitions[key].label || key; -} -getValueText(key, value) { -let definition = this.definitions[key]; -if ("min" in definition) { -let params = definition.params; -if (params.customTextValue) { -if (definition.transformValue) value = definition.transformValue.get.call(definition, value); -let text = params.customTextValue(value, definition.min, definition.max); -if (text) return text; -} -return value.toString(); -} else if ("options" in definition) { -let options = definition.options; -if (value in options) return options[value]; -} else if (typeof value === "boolean") return value ? t("on") : t("off"); -return value.toString(); -} -} -var BxIcon = { -BETTER_XCLOUD: "", -TRUE_ACHIEVEMENTS: "", -STREAM_SETTINGS: "", -STREAM_STATS: "", -CLOSE: "", -CONTROLLER: "", -CREATE_SHORTCUT: "", -DISPLAY: "", -EYE: "", -EYE_SLASH: "", -HOME: "", -LOCAL_CO_OP: "", -NATIVE_MKB: "", -NEW: "", -MANAGE: "", -COPY: "", -TRASH: "", -CURSOR_TEXT: "", -POWER: "", -QUESTION: "", -REFRESH: "", -REMOTE_PLAY: "", -CARET_LEFT: "", -CARET_RIGHT: "", -SCREENSHOT: "", -SPEAKER_MUTED: "", -TOUCH_CONTROL_ENABLE: "", -TOUCH_CONTROL_DISABLE: "", -MICROPHONE: "", -MICROPHONE_MUTED: "", -BATTERY: "", -PLAYTIME: "", -SERVER: "", -DOWNLOAD: "", -UPLOAD: "", -AUDIO: "" -}; -function getSupportedCodecProfiles() { -let options = { -default: t("default") -}; -if (!("getCapabilities" in RTCRtpReceiver)) return options; -let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs; -for (let codec of codecs) { -if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; -let fmtp = codec.sdpFmtpLine.toLowerCase(); -if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0; -else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0; -else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0; -} -if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`; -else options["low"] = t("visual-quality-low"); -if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`; -else options["normal"] = t("visual-quality-normal"); -if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`; -else options["high"] = t("visual-quality-high"); -return options; -} -class GlobalSettingsStorage extends BaseSettingsStorage { -static DEFINITIONS = { -"version.lastCheck": { -default: 0 -}, -"version.latest": { -default: "" -}, -"version.current": { -default: "" -}, -"bx.locale": { -label: t("language"), -default: localStorage.getItem("BetterXcloud.Locale") || "en-US", -options: SUPPORTED_LANGUAGES -}, -"server.region": { -label: t("region"), -note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")), -default: "default" -}, -"server.bypassRestriction": { -label: t("bypass-region-restriction"), -note: "⚠️ " + t("use-this-at-your-own-risk"), -default: "off", -optionsGroup: t("region"), -options: Object.assign({ -off: t("off") -}, BypassServers) -}, -"stream.locale": { -label: t("preferred-game-language"), -default: "default", -options: { -default: t("default"), -"ar-SA": "العربية", -"bg-BG": "Български", -"cs-CZ": "čeština", -"da-DK": "dansk", -"de-DE": "Deutsch", -"el-GR": "Ελληνικά", -"en-GB": "English (UK)", -"en-US": "English (US)", -"es-ES": "español (España)", -"es-MX": "español (Latinoamérica)", -"fi-FI": "suomi", -"fr-FR": "français", -"he-IL": "עברית", -"hu-HU": "magyar", -"it-IT": "italiano", -"ja-JP": "日本語", -"ko-KR": "한국어", -"nb-NO": "norsk bokmål", -"nl-NL": "Nederlands", -"pl-PL": "polski", -"pt-BR": "português (Brasil)", -"pt-PT": "português (Portugal)", -"ro-RO": "Română", -"ru-RU": "русский", -"sk-SK": "slovenčina", -"sv-SE": "svenska", -"th-TH": "ไทย", -"tr-TR": "Türkçe", -"zh-CN": "中文(简体)", -"zh-TW": "中文 (繁體)" -} -}, -"stream.video.resolution": { -label: t("target-resolution"), -default: "auto", -options: { -auto: t("default"), -"720p": "720p", -"1080p": "1080p", -"1080p-hq": "1080p (HQ)" -}, -suggest: { -lowest: "720p", -highest: "1080p-hq" -} -}, -"stream.video.codecProfile": { -label: t("visual-quality"), -default: "default", -options: getSupportedCodecProfiles(), -ready: (setting) => { -let options = setting.options, keys = Object.keys(options); -if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature"); -setting.suggest = { -lowest: keys.length === 1 ? keys[0] : keys[1], -highest: keys[keys.length - 1] -}; -} -}, -"server.ipv6.prefer": { -label: t("prefer-ipv6-server"), -default: !1 -}, -"screenshot.applyFilters": { -requiredVariants: "full", -label: t("screenshot-apply-filters"), -default: !1 -}, -"ui.splashVideo.skip": { -label: t("skip-splash-video"), -default: !1 -}, -"ui.systemMenu.hideHandle": { -label: "⣿ " + t("hide-system-menu-icon"), -default: !1 -}, -"ui.imageQuality": { -requiredVariants: "full", -label: t("image-quality"), -default: 90, -min: 10, -max: 90, -params: { -steps: 10, -exactTicks: 20, -hideSlider: !0, -customTextValue(value, min, max) { -if (value === 90) return t("default"); -return value + "%"; -} -} -}, -"stream.video.combineAudio": { -requiredVariants: "full", -label: t("combine-audio-video-streams"), -default: !1, -experimental: !0, -note: t("combine-audio-video-streams-summary") -}, -"touchController.mode": { -requiredVariants: "full", -label: t("availability"), -default: "all", -options: { -default: t("default"), -off: t("off"), -all: t("all-games") -}, -unsupported: !STATES.userAgent.capabilities.touch, -unsupportedValue: "default" -}, -"touchController.autoOff": { -requiredVariants: "full", -label: t("tc-auto-off"), -default: !1, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.opacity.default": { -requiredVariants: "full", -label: t("default-opacity"), -default: 100, -min: 10, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10, -hideSlider: !0 -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.style.standard": { -requiredVariants: "full", -label: t("tc-standard-layout-style"), -default: "default", -options: { -default: t("default"), -white: t("tc-all-white"), -muted: t("tc-muted-colors") -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"touchController.style.custom": { -requiredVariants: "full", -label: t("tc-custom-layout-style"), -default: "default", -options: { -default: t("default"), -muted: t("tc-muted-colors") -}, -unsupported: !STATES.userAgent.capabilities.touch -}, -"ui.streamMenu.simplify": { -label: t("simplify-stream-menu"), -default: !1 -}, -"mkb.cursor.hideIdle": { -requiredVariants: "full", -label: t("hide-idle-cursor"), -default: !1 -}, -"ui.feedbackDialog.disabled": { -requiredVariants: "full", -label: t("disable-post-stream-feedback-dialog"), -default: !1 -}, -"stream.video.maxBitrate": { -requiredVariants: "full", -label: t("bitrate-video-maximum"), -note: "⚠️ " + t("unexpected-behavior"), -default: 0, -min: 102400, -max: 15360000, -transformValue: { -get(value) { -return value === 0 ? this.max : value; -}, -set(value) { -return value === this.max ? 0 : value; -} -}, -params: { -steps: 102400, -exactTicks: 5120000, -customTextValue: (value, min, max) => { -if (value = parseInt(value), value === max) return t("unlimited"); -else return (value / 1024000).toFixed(1) + " Mb/s"; -} -}, -suggest: { -highest: 0 -} -}, -"gameBar.position": { -requiredVariants: "full", -label: t("position"), -default: "bottom-left", -options: { -off: t("off"), -"bottom-left": t("bottom-left"), -"bottom-right": t("bottom-right") -} -}, -"ui.controllerStatus.show": { -label: t("show-controller-connection-status"), -default: !0 -}, -"mkb.enabled": { -requiredVariants: "full", -label: t("enable-mkb"), -default: !1, -unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb, -ready: (setting) => { -let note, url; -if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657"; -else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer"; -setting.unsupportedNote = () => CE("a", { -href: url, -target: "_blank" -}, "⚠️ " + note); -} -}, -"nativeMkb.mode": { -requiredVariants: "full", -label: t("native-mkb"), -default: "default", -options: { -default: t("default"), -off: t("off"), -on: t("on") -}, -ready: (setting) => { -if (STATES.browser.capabilities.emulatedNativeMkb) ; -else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"]; -else delete setting.options["on"]; -} -}, -"nativeMkb.forcedGames": { -label: t("force-native-mkb-games"), -default: [], -unsupported: !AppInterface && UserAgent.isMobile(), -ready: (setting) => { -if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => { -setting.multipleOptions = payload.data.data; -}); -}, -params: { -size: 6 -} -}, -"ui.reduceAnimations": { -label: t("reduce-animations"), -default: !1 -}, -"loadingScreen.gameArt.show": { -requiredVariants: "full", -label: t("show-game-art"), -default: !0 -}, -"loadingScreen.waitTime.show": { -label: t("show-wait-time"), -default: !0 -}, -"loadingScreen.rocket": { -label: t("rocket-animation"), -default: "show", -options: { -show: t("rocket-always-show"), -"hide-queue": t("rocket-hide-queue"), -hide: t("rocket-always-hide") -} -}, -"ui.controllerFriendly": { -label: t("controller-friendly-ui"), -default: BX_FLAGS.DeviceInfo.deviceType !== "unknown" -}, -"ui.layout": { -requiredVariants: "full", -label: t("layout"), -default: "default", -options: { -default: t("default"), -normal: t("normal"), -tv: t("smart-tv") -} -}, -"ui.hideScrollbar": { -label: t("hide-scrollbar"), -default: !1 -}, -"ui.hideSections": { -requiredVariants: "full", -label: t("hide-sections"), -default: [], -multipleOptions: { -news: t("section-news"), -friends: t("section-play-with-friends"), -"native-mkb": t("section-native-mkb"), -touch: t("section-touch"), -"most-popular": t("section-most-popular"), -"all-games": t("section-all-games") -}, -params: { -size: 0 -} -}, -"ui.gameCard.waitTime.show": { -requiredVariants: "full", -label: t("show-wait-time-in-game-card"), -default: !0 -}, -"block.tracking": { -label: t("disable-xcloud-analytics"), -default: !1 -}, -"block.features": { -requiredVariants: "full", -label: t("disable-features"), -default: [], -multipleOptions: { -chat: t("chat"), -friends: t("friends-followers"), -byog: t("stream-your-own-game"), -"notifications-invites": t("notifications") + ": " + t("invites"), -"notifications-achievements": t("notifications") + ": " + t("achievements") -} -}, -"userAgent.profile": { -label: t("user-agent-profile"), -note: "⚠️ " + t("unexpected-behavior"), -default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default", -options: { -default: t("default"), -"windows-edge": "Edge + Windows", -"macos-safari": "Safari + macOS", -"vr-oculus": "Android TV", -"smarttv-generic": "Smart TV", -"smarttv-tizen": "Samsung Smart TV", -custom: t("custom") -} -}, -"audio.mic.onPlaying": { -label: t("enable-mic-on-startup"), -default: !1 -}, -"audio.volume.booster.enabled": { -requiredVariants: "full", -label: t("enable-volume-control"), -default: !1 -}, -"xhome.enabled": { -requiredVariants: "full", -label: t("enable-remote-play-feature"), -labelIcon: BxIcon.REMOTE_PLAY, -default: !1 -}, -"xhome.video.resolution": { -requiredVariants: "full", -default: "1080p", -options: { -"720p": "720p", -"1080p": "1080p", -"1080p-hq": "1080p (HQ)" -} -}, -"game.fortnite.forceConsole": { -requiredVariants: "full", -label: "🎮 " + t("fortnite-force-console-version"), -default: !1, -note: t("fortnite-allow-stw-mode") -} -}; -constructor() { -super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS); -} -} -class BaseLocalTable { -tableName; -constructor(tableName) { -this.tableName = tableName; -} -async prepareTable(type = "readonly") { -return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName); -} -call(method) { -return new Promise((resolve) => { -let request = method.call(null, ...Array.from(arguments).slice(1)); -request.onsuccess = (e) => { -resolve(e.target.result); -}; -}); -} -async count() { -let table = await this.prepareTable(); -return this.call(table.count.bind(table)); -} -async add(data) { -let table = await this.prepareTable("readwrite"); -return this.call(table.add.bind(table), ...arguments); -} -async put(data) { -let table = await this.prepareTable("readwrite"); -return this.call(table.put.bind(table), ...arguments); -} -async delete(id) { -let table = await this.prepareTable("readwrite"); -return this.call(table.delete.bind(table), ...arguments); -} -async get(id) { -let table = await this.prepareTable(); -return this.call(table.get.bind(table), ...arguments); -} -async getAll() { -let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {}; -return all.forEach((item2) => { -results[item2.id] = item2; -}), results; -} -} -class BasePresetsTable extends BaseLocalTable { -async newPreset(name, data) { -let newRecord = { name, data }; -return await this.add(newRecord); -} -async updatePreset(preset) { -return await this.put(preset); -} -async deletePreset(id) { -return this.delete(id); -} -async getPreset(id) { -if (id === 0) return null; -if (id < 0) return this.DEFAULT_PRESETS[id]; -let preset = await this.get(id); -if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID]; -return preset; -} -async getPresets() { -let all = deepClone(this.DEFAULT_PRESETS), presets = { -default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)), -custom: [], -data: {} -}; -if (await this.count() > 0) { -let items = await this.getAll(), id; -for (id in items) { -let item2 = items[id]; -presets.custom.push(item2.id), all[item2.id] = item2; -} -} -return presets.data = all, presets; -} -async getPresetsData() { -let presetsData = {}; -for (let id in this.DEFAULT_PRESETS) { -let preset = this.DEFAULT_PRESETS[id]; -presetsData[id] = deepClone(preset.data); -} -if (await this.count() > 0) { -let items = await this.getAll(), id; -for (id in items) { -let item2 = items[id]; -presetsData[item2.id] = item2.data; -} -} -return presetsData; -} -} -class KeyboardShortcutsTable extends BasePresetsTable { -static instance; -static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable); -LOG_TAG = "KeyboardShortcutsTable"; -TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: t("standard"), -data: { -mapping: { -"mkb.toggle": { -code: "F8" -}, -"stream.screenshot.capture": { -code: "Slash" -} -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {} -}; -DEFAULT_PRESET_ID = -1; -constructor() { -super(LocalDb.TABLE_KEYBOARD_SHORTCUTS); -BxLogger.info(this.LOG_TAG, "constructor()"); -} -} -class MkbMappingPresetsTable extends BasePresetsTable { -static instance; -static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable); -LOG_TAG = "MkbMappingPresetsTable"; -TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: t("standard"), -data: { -mapping: { -16: ["Backquote"], -12: ["ArrowUp", "Digit1"], -13: ["ArrowDown", "Digit2"], -14: ["ArrowLeft", "Digit3"], -15: ["ArrowRight", "Digit4"], -100: ["KeyW"], -101: ["KeyS"], -102: ["KeyA"], -103: ["KeyD"], -200: ["KeyU"], -201: ["KeyJ"], -202: ["KeyH"], -203: ["KeyK"], -0: ["Space", "KeyE"], -2: ["KeyR"], -1: ["KeyC", "Backspace"], -3: ["KeyV"], -9: ["Enter"], -8: ["Tab"], -4: ["KeyQ"], -5: ["KeyF"], -7: ["Mouse0"], -6: ["Mouse2"], -10: ["KeyX"], -11: ["KeyZ"] -}, -mouse: { -mapTo: 2, -sensitivityX: 100, -sensitivityY: 100, -deadzoneCounterweight: 20 -} -} -}, -[-2]: { -id: -2, -name: "Shooter", -data: { -mapping: { -16: ["Backquote"], -12: ["ArrowUp"], -13: ["ArrowDown"], -14: ["ArrowLeft"], -15: ["ArrowRight"], -100: ["KeyW"], -101: ["KeyS"], -102: ["KeyA"], -103: ["KeyD"], -200: ["KeyI"], -201: ["KeyK"], -202: ["KeyJ"], -203: ["KeyL"], -0: ["Space", "KeyE"], -2: ["KeyR"], -1: ["ControlLeft", "Backspace"], -3: ["KeyV"], -9: ["Enter"], -8: ["Tab"], -4: ["KeyC", "KeyG"], -5: ["KeyQ"], -7: ["Mouse0"], -6: ["Mouse2"], -10: ["ShiftLeft"], -11: ["KeyF"] -}, -mouse: { -mapTo: 2, -sensitivityX: 100, -sensitivityY: 100, -deadzoneCounterweight: 20 -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {}, -mouse: { -mapTo: 2, -sensitivityX: 100, -sensitivityY: 100, -deadzoneCounterweight: 20 -} -}; -DEFAULT_PRESET_ID = -1; -constructor() { -super(LocalDb.TABLE_VIRTUAL_CONTROLLERS); -BxLogger.info(this.LOG_TAG, "constructor()"); -} -} -class GameSettingsStorage extends BaseSettingsStorage { -constructor(id) { -super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS); -} -deleteSetting(pref) { -if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0; -return !1; -} -} -class ControllerCustomizationsTable extends BasePresetsTable { -static instance; -static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)); -TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: "ABXY ⇄ BAYX", -data: { -mapping: { -0: 1, -1: 0, -2: 3, -3: 2 -}, -settings: { -leftStickDeadzone: [0, 100], -rightStickDeadzone: [0, 100], -leftTriggerRange: [0, 100], -rightTriggerRange: [0, 100], -vibrationIntensity: 100 -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {}, -settings: { -leftTriggerRange: [0, 100], -rightTriggerRange: [0, 100], -leftStickDeadzone: [0, 100], -rightStickDeadzone: [0, 100], -vibrationIntensity: 100 -} -}; -DEFAULT_PRESET_ID = 0; -} -class ControllerShortcutsTable extends BasePresetsTable { -static instance; -static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable); -LOG_TAG = "ControllerShortcutsTable"; -TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS; -DEFAULT_PRESETS = { -[-1]: { -id: -1, -name: "Type A", -data: { -mapping: { -3: AppInterface ? "device.volume.inc" : "stream.volume.inc", -0: AppInterface ? "device.volume.dec" : "stream.volume.dec", -2: "stream.stats.toggle", -1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", -5: "stream.screenshot.capture", -9: "stream.menu.show" -} -} -}, -[-2]: { -id: -2, -name: "Type B", -data: { -mapping: { -12: AppInterface ? "device.volume.inc" : "stream.volume.inc", -13: AppInterface ? "device.volume.dec" : "stream.volume.dec", -15: "stream.stats.toggle", -14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle", -4: "stream.screenshot.capture", -8: "stream.menu.show" -} -} -} -}; -BLANK_PRESET_DATA = { -mapping: {} -}; -DEFAULT_PRESET_ID = -1; -constructor() { -super(LocalDb.TABLE_CONTROLLER_SHORTCUTS); -BxLogger.info(this.LOG_TAG, "constructor()"); -} -} -class StreamSettingsStorage extends BaseSettingsStorage { -static DEFINITIONS = { -"deviceVibration.mode": { -requiredVariants: "full", -label: t("device-vibration"), -default: "off", -options: { -off: t("off"), -on: t("on"), -auto: t("device-vibration-not-using-gamepad") -} -}, -"deviceVibration.intensity": { -requiredVariants: "full", -label: t("vibration-intensity"), -default: 50, -min: 10, -max: 100, -params: { -steps: 10, -suffix: "%", -exactTicks: 20 -} -}, -"controller.pollingRate": { -requiredVariants: "full", -label: t("polling-rate"), -default: 4, -min: 4, -max: 60, -params: { -steps: 4, -exactTicks: 20, -reverse: !0, -customTextValue(value) { -value = parseInt(value); -let text = +(1000 / value).toFixed(2) + " Hz"; -if (value === 4) text = `${text} (${t("default")})`; -return text; -} -} -}, -"controller.settings": { -default: {} -}, -"nativeMkb.scroll.sensitivityX": { -requiredVariants: "full", -label: t("horizontal-scroll-sensitivity"), -default: 0, -min: 0, -max: 1e4, -params: { -steps: 10, -exactTicks: 2000, -customTextValue: (value) => { -if (!value) return t("default"); -return (value / 100).toFixed(1) + "x"; -} -} -}, -"nativeMkb.scroll.sensitivityY": { -requiredVariants: "full", -label: t("vertical-scroll-sensitivity"), -default: 0, -min: 0, -max: 1e4, -params: { -steps: 10, -exactTicks: 2000, -customTextValue: (value) => { -if (!value) return t("default"); -return (value / 100).toFixed(1) + "x"; -} -} -}, -"mkb.p1.preset.mappingId": { -requiredVariants: "full", -default: -1 -}, -"mkb.p1.slot": { -requiredVariants: "full", -default: 1, -min: 1, -max: 4, -params: { -hideSlider: !0 -} -}, -"mkb.p2.preset.mappingId": { -requiredVariants: "full", -default: 0 -}, -"mkb.p2.slot": { -requiredVariants: "full", -default: 0, -min: 0, -max: 4, -params: { -hideSlider: !0, -customTextValue(value) { -return value = parseInt(value), value === 0 ? t("off") : value.toString(); -} -} -}, -"keyboardShortcuts.preset.inGameId": { -requiredVariants: "full", -default: -1 -}, -"video.player.type": { -label: t("renderer"), -default: "default", -options: { -default: t("default"), -webgl2: t("webgl2") -}, -suggest: { -lowest: "default", -highest: "webgl2" -} -}, -"video.processing": { -label: t("clarity-boost"), -default: "usm", -options: { -usm: t("unsharp-masking"), -cas: t("amd-fidelity-cas") -}, -suggest: { -lowest: "usm", -highest: "cas" -} -}, -"video.player.powerPreference": { -label: t("renderer-configuration"), -default: "default", -options: { -default: t("default"), -"low-power": t("battery-saving"), -"high-performance": t("high-performance") -}, -suggest: { -highest: "low-power" -} -}, -"video.maxFps": { -label: t("limit-fps"), -default: 60, -min: 10, -max: 60, -params: { -steps: 10, -exactTicks: 10, -customTextValue: (value) => { -return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; -} -} -}, -"video.processing.sharpness": { -label: t("sharpness"), -default: 0, -min: 0, -max: 10, -params: { -exactTicks: 2, -customTextValue: (value) => { -return value = parseInt(value), value === 0 ? t("off") : value.toString(); -} -}, -suggest: { -lowest: 0, -highest: 2 -} -}, -"video.ratio": { -label: t("aspect-ratio"), -note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, -default: "16:9", -options: { -"16:9": `16:9 (${t("default")})`, -"18:9": "18:9", -"21:9": "21:9", -"16:10": "16:10", -"4:3": "4:3", -fill: t("stretch") -} -}, -"video.position": { -label: t("position"), -note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0, -default: "center", -options: { -top: t("top"), -"top-half": t("top-half"), -center: `${t("center")} (${t("default")})`, -"bottom-half": t("bottom-half"), -bottom: t("bottom") -} -}, -"video.saturation": { -label: t("saturation"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"video.contrast": { -label: t("contrast"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"video.brightness": { -label: t("brightness"), -default: 100, -min: 50, -max: 150, -params: { -suffix: "%", -ticks: 25 -} -}, -"audio.volume": { -label: t("volume"), -default: 100, -min: 0, -max: 600, -params: { -steps: 10, -suffix: "%", -ticks: 100 -} -}, -"stats.items": { -label: t("stats"), -default: ["ping", "fps", "btr", "dt", "pl", "fl"], -multipleOptions: { -time: t("clock"), -play: t("playtime"), -batt: t("battery"), -ping: t("stat-ping"), -jit: t("jitter"), -fps: t("stat-fps"), -btr: t("stat-bitrate"), -dt: t("stat-decode-time"), -pl: t("stat-packets-lost"), -fl: t("stat-frames-lost"), -dl: t("downloaded"), -ul: t("uploaded") -}, -params: { -size: 0 -}, -ready: (setting) => { -let multipleOptions = setting.multipleOptions; -if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; -for (let key in multipleOptions) -multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key]; -} -}, -"stats.showWhenPlaying": { -label: t("show-stats-on-startup"), -default: !1 -}, -"stats.quickGlance.enabled": { -label: "👀 " + t("enable-quick-glance-mode"), -default: !0 -}, -"stats.position": { -label: t("position"), -default: "top-right", -options: { -"top-left": t("top-left"), -"top-center": t("top-center"), -"top-right": t("top-right") -} -}, -"stats.textSize": { -label: t("text-size"), -default: "0.9rem", -options: { -"0.9rem": t("small"), -"1.0rem": t("normal"), -"1.1rem": t("large") -} -}, -"stats.opacity.all": { -label: t("opacity"), -default: 80, -min: 50, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10 -} -}, -"stats.opacity.background": { -label: t("background-opacity"), -default: 100, -min: 0, -max: 100, -params: { -steps: 10, -suffix: "%", -ticks: 10 -} -}, -"stats.colors": { -label: t("conditional-formatting"), -default: !1 -}, -"localCoOp.enabled": { -requiredVariants: "full", -label: t("enable-local-co-op-support"), -labelIcon: BxIcon.LOCAL_CO_OP, -default: !1, -note: () => CE("div", !1, CE("a", { -href: "https://github.com/redphx/better-xcloud/discussions/275", -target: "_blank" -}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior")) -} -}; -gameSettings = {}; -xboxTitleId = -1; -constructor() { -super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS); -} -setGameId(id) { -this.xboxTitleId = id; -} -getGameSettings(id) { -if (id > -1) { -if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id); -return this.gameSettings[id]; -} -return null; -} -getSetting(key, checkUnsupported) { -return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported); -} -getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) { -let gameSettings = this.getGameSettings(id); -if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported); -if (returnBaseValue) return super.getSetting(key, checkUnsupported); -return; -} -setSetting(key, value, origin) { -return this.setSettingByGame(this.xboxTitleId, key, value, origin); -} -setSettingByGame(id, key, value, origin) { -let gameSettings = this.getGameSettings(id); -if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin); -return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin); -} -hasGameSetting(id, key) { -let gameSettings = this.getGameSettings(id); -return !!(gameSettings && gameSettings.hasSetting(key)); -} -getControllerSetting(gamepadId) { -let controllerSetting = this.getSetting("controller.settings")[gamepadId]; -if (!controllerSetting) controllerSetting = {}; -if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1; -if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0; -return controllerSetting; -} -} -var STORAGE = { -Global: new GlobalSettingsStorage, -Stream: new StreamSettingsStorage -}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage); +class Toast {static instance;static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);LOG_TAG = "Toast";$wrapper;$msg;$status;stack = [];isShowing = !1;timeoutId;DURATION = 3000;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {let classList = this.$wrapper.classList;if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();}), document.documentElement.appendChild(this.$wrapper);}show(msg, status, options = {}) {options = options || {};let args = Array.from(arguments);if (options.instant) this.stack = [args], this.showNext();else this.stack.push(args), !this.isShowing && this.showNext();}showNext() {if (!this.stack.length) {this.isShowing = !1;return;}this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION);let [msg, status, options] = this.stack.shift();if (options && options.html) this.$msg.innerHTML = msg;else this.$msg.textContent = msg;if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;else this.$status.classList.add("bx-gone");let classList = this.$wrapper.classList;classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");}hide = () => {this.timeoutId = null;let classList = this.$wrapper.classList;classList.remove("bx-show"), classList.add("bx-hide");};static show(msg, status, options = {}) {Toast.getInstance().show(msg, status, options);}static showNext() {Toast.getInstance().showNext();}} +class MicrophoneShortcut {static toggle(showToast = !0) {if (!window.BX_EXPOSED.streamSession) return !1;let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0;try {return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic;} catch (e) {console.log(e);}return !1;}} +class LocalDb {static instance;static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb);static DB_NAME = "BetterXcloud";static DB_VERSION = 4;static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers";static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts";static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations";static TABLE_CONTROLLER_SETTINGS = "controller_settings";static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts";db;open() {return new Promise((resolve, reject) => {if (this.db) {resolve(this.db);return;}let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);request.onupgradeneeded = (e) => {let db = e.target.result;if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {keyPath: "id",autoIncrement: !0});if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {keyPath: "id",autoIncrement: !0});if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {keyPath: "id"});if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, {keyPath: "id",autoIncrement: !0});if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {keyPath: "id",autoIncrement: !0});}, request.onerror = (e) => {console.log(e), alert(e.target.error.message), reject && reject();}, request.onsuccess = (e) => {this.db = e.target.result, resolve(this.db);};});}} +var BypassServers = {br: t("brazil"),jp: t("japan"),kr: t("korea"),pl: t("poland"),us: t("united-states")}, BypassServerIps = {br: "169.150.198.66",kr: "121.125.60.151",jp: "138.199.21.239",pl: "45.134.212.66",us: "143.244.47.65"}; +class BaseSettingsStorage {storage;storageKey;_settings;definitions;constructor(storageKey, definitions) {this.storage = window.localStorage, this.storageKey = storageKey;for (let [_, setting] of Object.entries(definitions)) {if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants];setting.ready && setting.ready.call(this, setting);}this.definitions = definitions, this._settings = null;}get settings() {if (this._settings) return this._settings;let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");for (let key in settings) {if (!this.definitions.hasOwnProperty(key)) {delete settings[key];continue;}settings[key] = this.validateValue("get", key, settings[key]);}return this._settings = settings, settings;}getDefinition(key) {if (!this.definitions[key]) return alert("Request invalid definition: " + key), {};return this.definitions[key];}hasSetting(key) {return key in this.settings;}getSetting(key, checkUnsupported = !0) {let definition = this.definitions[key];if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key];}setSetting(key, value, origin) {if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", {storageKey: this.storageKey,settingKey: key});else BxEventBus.Script.emit("setting.changed", {storageKey: this.storageKey,settingKey: key});return value;}saveSettings() {this.storage.setItem(this.storageKey, JSON.stringify(this.settings));}validateValue(action, key, value) {let def = this.definitions[key];if (!def) return value;if (typeof value === "undefined" || value === null) value = def.default;if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);if ("min" in def) value = Math.max(def.min, value);if ("max" in def) value = Math.min(def.max, value);if ("options" in def) {if (!(value in def.options)) value = def.default;} else if ("multipleOptions" in def) {if (value.length) {let validOptions = Object.keys(def.multipleOptions);value.forEach((item2, idx) => {validOptions.indexOf(item2) === -1 && value.splice(idx, 1);});}if (!value.length) value = def.default;}if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);return value;}getLabel(key) {return this.definitions[key].label || key;}getValueText(key, value) {let definition = this.definitions[key];if ("min" in definition) {let params = definition.params;if (params.customTextValue) {if (definition.transformValue) value = definition.transformValue.get.call(definition, value);let text = params.customTextValue(value, definition.min, definition.max);if (text) return text;}return value.toString();} else if ("options" in definition) {let options = definition.options;if (value in options) return options[value];} else if (typeof value === "boolean") return value ? t("on") : t("off");return value.toString();}} +var BxIcon = {BETTER_XCLOUD: "",TRUE_ACHIEVEMENTS: "",STREAM_SETTINGS: "",STREAM_STATS: "",CLOSE: "",CONTROLLER: "",CREATE_SHORTCUT: "",DISPLAY: "",EYE: "",EYE_SLASH: "",HOME: "",LOCAL_CO_OP: "",NATIVE_MKB: "",NEW: "",MANAGE: "",COPY: "",TRASH: "",CURSOR_TEXT: "",POWER: "",QUESTION: "",REFRESH: "",REMOTE_PLAY: "",CARET_LEFT: "",CARET_RIGHT: "",SCREENSHOT: "",SPEAKER_MUTED: "",TOUCH_CONTROL_ENABLE: "",TOUCH_CONTROL_DISABLE: "",MICROPHONE: "",MICROPHONE_MUTED: "",BATTERY: "",PLAYTIME: "",SERVER: "",DOWNLOAD: "",UPLOAD: "",AUDIO: ""}; +function getSupportedCodecProfiles() {let options = {default: t("default")};if (!("getCapabilities" in RTCRtpReceiver)) return options;let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;for (let codec of codecs) {if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;let fmtp = codec.sdpFmtpLine.toLowerCase();if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;}if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`;else options["low"] = t("visual-quality-low");if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`;else options["normal"] = t("visual-quality-normal");if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`;else options["high"] = t("visual-quality-high");return options;} +class GlobalSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"version.lastCheck": {default: 0},"version.latest": {default: ""},"version.current": {default: ""},"bx.locale": {label: t("language"),default: localStorage.getItem("BetterXcloud.Locale") || "en-US",options: SUPPORTED_LANGUAGES},"server.region": {label: t("region"),note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),default: "default"},"server.bypassRestriction": {label: t("bypass-region-restriction"),note: "⚠️ " + t("use-this-at-your-own-risk"),default: "off",optionsGroup: t("region"),options: Object.assign({off: t("off")}, BypassServers)},"stream.locale": {label: t("preferred-game-language"),default: "default",options: {default: t("default"),"ar-SA": "العربية","bg-BG": "Български","cs-CZ": "čeština","da-DK": "dansk","de-DE": "Deutsch","el-GR": "Ελληνικά","en-GB": "English (UK)","en-US": "English (US)","es-ES": "español (España)","es-MX": "español (Latinoamérica)","fi-FI": "suomi","fr-FR": "français","he-IL": "עברית","hu-HU": "magyar","it-IT": "italiano","ja-JP": "日本語","ko-KR": "한국어","nb-NO": "norsk bokmål","nl-NL": "Nederlands","pl-PL": "polski","pt-BR": "português (Brasil)","pt-PT": "português (Portugal)","ro-RO": "Română","ru-RU": "русский","sk-SK": "slovenčina","sv-SE": "svenska","th-TH": "ไทย","tr-TR": "Türkçe","zh-CN": "中文(简体)","zh-TW": "中文 (繁體)"}},"stream.video.resolution": {label: t("target-resolution"),default: "auto",options: {auto: t("default"),"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"},suggest: {lowest: "720p",highest: "1080p-hq"}},"stream.video.codecProfile": {label: t("visual-quality"),default: "default",options: getSupportedCodecProfiles(),ready: (setting) => {let options = setting.options, keys = Object.keys(options);if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");setting.suggest = {lowest: keys.length === 1 ? keys[0] : keys[1],highest: keys[keys.length - 1]};}},"server.ipv6.prefer": {label: t("prefer-ipv6-server"),default: !1},"screenshot.applyFilters": {requiredVariants: "full",label: t("screenshot-apply-filters"),default: !1},"ui.splashVideo.skip": {label: t("skip-splash-video"),default: !1},"ui.systemMenu.hideHandle": {label: "⣿ " + t("hide-system-menu-icon"),default: !1},"ui.imageQuality": {requiredVariants: "full",label: t("image-quality"),default: 90,min: 10,max: 90,params: {steps: 10,exactTicks: 20,hideSlider: !0,customTextValue(value, min, max) {if (value === 90) return t("default");return value + "%";}}},"stream.video.combineAudio": {requiredVariants: "full",label: t("combine-audio-video-streams"),default: !1,experimental: !0,note: t("combine-audio-video-streams-summary")},"touchController.mode": {requiredVariants: "full",label: t("availability"),default: "all",options: {default: t("default"),off: t("off"),all: t("all-games")},unsupported: !STATES.userAgent.capabilities.touch,unsupportedValue: "default"},"touchController.autoOff": {requiredVariants: "full",label: t("tc-auto-off"),default: !1,unsupported: !STATES.userAgent.capabilities.touch},"touchController.opacity.default": {requiredVariants: "full",label: t("default-opacity"),default: 100,min: 10,max: 100,params: {steps: 10,suffix: "%",ticks: 10,hideSlider: !0},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.standard": {requiredVariants: "full",label: t("tc-standard-layout-style"),default: "default",options: {default: t("default"),white: t("tc-all-white"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"touchController.style.custom": {requiredVariants: "full",label: t("tc-custom-layout-style"),default: "default",options: {default: t("default"),muted: t("tc-muted-colors")},unsupported: !STATES.userAgent.capabilities.touch},"ui.streamMenu.simplify": {label: t("simplify-stream-menu"),default: !1},"mkb.cursor.hideIdle": {requiredVariants: "full",label: t("hide-idle-cursor"),default: !1},"ui.feedbackDialog.disabled": {requiredVariants: "full",label: t("disable-post-stream-feedback-dialog"),default: !1},"stream.video.maxBitrate": {requiredVariants: "full",label: t("bitrate-video-maximum"),note: "⚠️ " + t("unexpected-behavior"),default: 0,min: 102400,max: 15360000,transformValue: {get(value) {return value === 0 ? this.max : value;},set(value) {return value === this.max ? 0 : value;}},params: {steps: 102400,exactTicks: 5120000,customTextValue: (value, min, max) => {if (value = parseInt(value), value === max) return t("unlimited");else return (value / 1024000).toFixed(1) + " Mb/s";}},suggest: {highest: 0}},"gameBar.position": {requiredVariants: "full",label: t("position"),default: "bottom-left",options: {off: t("off"),"bottom-left": t("bottom-left"),"bottom-right": t("bottom-right")}},"ui.controllerStatus.show": {label: t("show-controller-connection-status"),default: !0},"mkb.enabled": {requiredVariants: "full",label: t("enable-mkb"),default: !1,unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,ready: (setting) => {let note, url;if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";setting.unsupportedNote = () => CE("a", {href: url,target: "_blank"}, "⚠️ " + note);}},"nativeMkb.mode": {requiredVariants: "full",label: t("native-mkb"),default: "default",options: {default: t("default"),off: t("off"),on: t("on")},ready: (setting) => {if (STATES.browser.capabilities.emulatedNativeMkb) ;else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];else delete setting.options["on"];}},"nativeMkb.forcedGames": {label: t("force-native-mkb-games"),default: [],unsupported: !AppInterface && UserAgent.isMobile(),ready: (setting) => {if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {setting.multipleOptions = payload.data.data;});},params: {size: 6}},"ui.reduceAnimations": {label: t("reduce-animations"),default: !1},"loadingScreen.gameArt.show": {requiredVariants: "full",label: t("show-game-art"),default: !0},"loadingScreen.waitTime.show": {label: t("show-wait-time"),default: !0},"loadingScreen.rocket": {label: t("rocket-animation"),default: "show",options: {show: t("rocket-always-show"),"hide-queue": t("rocket-hide-queue"),hide: t("rocket-always-hide")}},"ui.controllerFriendly": {label: t("controller-friendly-ui"),default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"},"ui.layout": {requiredVariants: "full",label: t("layout"),default: "default",options: {default: t("default"),normal: t("normal"),tv: t("smart-tv")}},"ui.hideScrollbar": {label: t("hide-scrollbar"),default: !1},"ui.hideSections": {requiredVariants: "full",label: t("hide-sections"),default: [],multipleOptions: {news: t("section-news"),friends: t("section-play-with-friends"),"native-mkb": t("section-native-mkb"),touch: t("section-touch"),"most-popular": t("section-most-popular"),"all-games": t("section-all-games")},params: {size: 0}},"ui.gameCard.waitTime.show": {requiredVariants: "full",label: t("show-wait-time-in-game-card"),default: !0},"block.tracking": {label: t("disable-xcloud-analytics"),default: !1},"block.features": {requiredVariants: "full",label: t("disable-features"),default: [],multipleOptions: {chat: t("chat"),friends: t("friends-followers"),byog: t("stream-your-own-game"),"notifications-invites": t("notifications") + ": " + t("invites"),"notifications-achievements": t("notifications") + ": " + t("achievements")}},"userAgent.profile": {label: t("user-agent-profile"),note: "⚠️ " + t("unexpected-behavior"),default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",options: {default: t("default"),"windows-edge": "Edge + Windows","macos-safari": "Safari + macOS","vr-oculus": "Android TV","smarttv-generic": "Smart TV","smarttv-tizen": "Samsung Smart TV",custom: t("custom")}},"audio.mic.onPlaying": {label: t("enable-mic-on-startup"),default: !1},"audio.volume.booster.enabled": {requiredVariants: "full",label: t("enable-volume-control"),default: !1},"xhome.enabled": {requiredVariants: "full",label: t("enable-remote-play-feature"),labelIcon: BxIcon.REMOTE_PLAY,default: !1},"xhome.video.resolution": {requiredVariants: "full",default: "1080p",options: {"720p": "720p","1080p": "1080p","1080p-hq": "1080p (HQ)"}},"game.fortnite.forceConsole": {requiredVariants: "full",label: "🎮 " + t("fortnite-force-console-version"),default: !1,note: t("fortnite-allow-stw-mode")}};constructor() {super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);}} +class BaseLocalTable {tableName;constructor(tableName) {this.tableName = tableName;}async prepareTable(type = "readonly") {return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);}call(method) {return new Promise((resolve) => {let request = method.call(null, ...Array.from(arguments).slice(1));request.onsuccess = (e) => {resolve(e.target.result);};});}async count() {let table = await this.prepareTable();return this.call(table.count.bind(table));}async add(data) {let table = await this.prepareTable("readwrite");return this.call(table.add.bind(table), ...arguments);}async put(data) {let table = await this.prepareTable("readwrite");return this.call(table.put.bind(table), ...arguments);}async delete(id) {let table = await this.prepareTable("readwrite");return this.call(table.delete.bind(table), ...arguments);}async get(id) {let table = await this.prepareTable();return this.call(table.get.bind(table), ...arguments);}async getAll() {let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};return all.forEach((item2) => {results[item2.id] = item2;}), results;}} +class BasePresetsTable extends BaseLocalTable {async newPreset(name, data) {let newRecord = { name, data };return await this.add(newRecord);}async updatePreset(preset) {return await this.put(preset);}async deletePreset(id) {return this.delete(id);}async getPreset(id) {if (id === 0) return null;if (id < 0) return this.DEFAULT_PRESETS[id];let preset = await this.get(id);if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];return preset;}async getPresets() {let all = deepClone(this.DEFAULT_PRESETS), presets = {default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)),custom: [],data: {}};if (await this.count() > 0) {let items = await this.getAll(), id;for (id in items) {let item2 = items[id];presets.custom.push(item2.id), all[item2.id] = item2;}}return presets.data = all, presets;}async getPresetsData() {let presetsData = {};for (let id in this.DEFAULT_PRESETS) {let preset = this.DEFAULT_PRESETS[id];presetsData[id] = deepClone(preset.data);}if (await this.count() > 0) {let items = await this.getAll(), id;for (id in items) {let item2 = items[id];presetsData[item2.id] = item2.data;}}return presetsData;}} +class KeyboardShortcutsTable extends BasePresetsTable {static instance;static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);LOG_TAG = "KeyboardShortcutsTable";TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;DEFAULT_PRESETS = {[-1]: {id: -1,name: t("standard"),data: {mapping: {"mkb.toggle": {code: "F8"},"stream.screenshot.capture": {code: "Slash"}}}}};BLANK_PRESET_DATA = {mapping: {}};DEFAULT_PRESET_ID = -1;constructor() {super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);BxLogger.info(this.LOG_TAG, "constructor()");}} +class MkbMappingPresetsTable extends BasePresetsTable {static instance;static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable);LOG_TAG = "MkbMappingPresetsTable";TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;DEFAULT_PRESETS = {[-1]: {id: -1,name: t("standard"),data: {mapping: {16: ["Backquote"],12: ["ArrowUp", "Digit1"],13: ["ArrowDown", "Digit2"],14: ["ArrowLeft", "Digit3"],15: ["ArrowRight", "Digit4"],100: ["KeyW"],101: ["KeyS"],102: ["KeyA"],103: ["KeyD"],200: ["KeyU"],201: ["KeyJ"],202: ["KeyH"],203: ["KeyK"],0: ["Space", "KeyE"],2: ["KeyR"],1: ["KeyC", "Backspace"],3: ["KeyV"],9: ["Enter"],8: ["Tab"],4: ["KeyQ"],5: ["KeyF"],7: ["Mouse0"],6: ["Mouse2"],10: ["KeyX"],11: ["KeyZ"]},mouse: {mapTo: 2,sensitivityX: 100,sensitivityY: 100,deadzoneCounterweight: 20}}},[-2]: {id: -2,name: "Shooter",data: {mapping: {16: ["Backquote"],12: ["ArrowUp"],13: ["ArrowDown"],14: ["ArrowLeft"],15: ["ArrowRight"],100: ["KeyW"],101: ["KeyS"],102: ["KeyA"],103: ["KeyD"],200: ["KeyI"],201: ["KeyK"],202: ["KeyJ"],203: ["KeyL"],0: ["Space", "KeyE"],2: ["KeyR"],1: ["ControlLeft", "Backspace"],3: ["KeyV"],9: ["Enter"],8: ["Tab"],4: ["KeyC", "KeyG"],5: ["KeyQ"],7: ["Mouse0"],6: ["Mouse2"],10: ["ShiftLeft"],11: ["KeyF"]},mouse: {mapTo: 2,sensitivityX: 100,sensitivityY: 100,deadzoneCounterweight: 20}}}};BLANK_PRESET_DATA = {mapping: {},mouse: {mapTo: 2,sensitivityX: 100,sensitivityY: 100,deadzoneCounterweight: 20}};DEFAULT_PRESET_ID = -1;constructor() {super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);BxLogger.info(this.LOG_TAG, "constructor()");}} +class GameSettingsStorage extends BaseSettingsStorage {constructor(id) {super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS);}deleteSetting(pref) {if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0;return !1;}} +class ControllerCustomizationsTable extends BasePresetsTable {static instance;static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;DEFAULT_PRESETS = {[-1]: {id: -1,name: "ABXY ⇄ BAYX",data: {mapping: {0: 1,1: 0,2: 3,3: 2},settings: {leftStickDeadzone: [0, 100],rightStickDeadzone: [0, 100],leftTriggerRange: [0, 100],rightTriggerRange: [0, 100],vibrationIntensity: 100}}}};BLANK_PRESET_DATA = {mapping: {},settings: {leftTriggerRange: [0, 100],rightTriggerRange: [0, 100],leftStickDeadzone: [0, 100],rightStickDeadzone: [0, 100],vibrationIntensity: 100}};DEFAULT_PRESET_ID = 0;} +class ControllerShortcutsTable extends BasePresetsTable {static instance;static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);LOG_TAG = "ControllerShortcutsTable";TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;DEFAULT_PRESETS = {[-1]: {id: -1,name: "Type A",data: {mapping: {3: AppInterface ? "device.volume.inc" : "stream.volume.inc",0: AppInterface ? "device.volume.dec" : "stream.volume.dec",2: "stream.stats.toggle",1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",5: "stream.screenshot.capture",9: "stream.menu.show"}}},[-2]: {id: -2,name: "Type B",data: {mapping: {12: AppInterface ? "device.volume.inc" : "stream.volume.inc",13: AppInterface ? "device.volume.dec" : "stream.volume.dec",15: "stream.stats.toggle",14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",4: "stream.screenshot.capture",8: "stream.menu.show"}}}};BLANK_PRESET_DATA = {mapping: {}};DEFAULT_PRESET_ID = -1;constructor() {super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);BxLogger.info(this.LOG_TAG, "constructor()");}} +class StreamSettingsStorage extends BaseSettingsStorage {static DEFINITIONS = {"deviceVibration.mode": {requiredVariants: "full",label: t("device-vibration"),default: "off",options: {off: t("off"),on: t("on"),auto: t("device-vibration-not-using-gamepad")}},"deviceVibration.intensity": {requiredVariants: "full",label: t("vibration-intensity"),default: 50,min: 10,max: 100,params: {steps: 10,suffix: "%",exactTicks: 20}},"controller.pollingRate": {requiredVariants: "full",label: t("polling-rate"),default: 4,min: 4,max: 60,params: {steps: 4,exactTicks: 20,reverse: !0,customTextValue(value) {value = parseInt(value);let text = +(1000 / value).toFixed(2) + " Hz";if (value === 4) text = `${text} (${t("default")})`;return text;}}},"controller.settings": {default: {}},"nativeMkb.scroll.sensitivityX": {requiredVariants: "full",label: t("horizontal-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"nativeMkb.scroll.sensitivityY": {requiredVariants: "full",label: t("vertical-scroll-sensitivity"),default: 0,min: 0,max: 1e4,params: {steps: 10,exactTicks: 2000,customTextValue: (value) => {if (!value) return t("default");return (value / 100).toFixed(1) + "x";}}},"mkb.p1.preset.mappingId": {requiredVariants: "full",default: -1},"mkb.p1.slot": {requiredVariants: "full",default: 1,min: 1,max: 4,params: {hideSlider: !0}},"mkb.p2.preset.mappingId": {requiredVariants: "full",default: 0},"mkb.p2.slot": {requiredVariants: "full",default: 0,min: 0,max: 4,params: {hideSlider: !0,customTextValue(value) {return value = parseInt(value), value === 0 ? t("off") : value.toString();}}},"keyboardShortcuts.preset.inGameId": {requiredVariants: "full",default: -1},"video.player.type": {label: t("renderer"),default: "default",options: {default: t("default"),webgl2: t("webgl2")},suggest: {lowest: "default",highest: "webgl2"}},"video.processing": {label: t("clarity-boost"),default: "usm",options: {usm: t("unsharp-masking"),cas: t("amd-fidelity-cas")},suggest: {lowest: "usm",highest: "cas"}},"video.player.powerPreference": {label: t("renderer-configuration"),default: "default",options: {default: t("default"),"low-power": t("battery-saving"),"high-performance": t("high-performance")},suggest: {highest: "low-power"}},"video.maxFps": {label: t("limit-fps"),default: 60,min: 10,max: 60,params: {steps: 10,exactTicks: 10,customTextValue: (value) => {return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";}}},"video.processing.sharpness": {label: t("sharpness"),default: 0,min: 0,max: 10,params: {exactTicks: 2,customTextValue: (value) => {return value = parseInt(value), value === 0 ? t("off") : value.toString();}},suggest: {lowest: 0,highest: 2}},"video.ratio": {label: t("aspect-ratio"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "16:9",options: {"16:9": `16:9 (${t("default")})`,"18:9": "18:9","21:9": "21:9","16:10": "16:10","4:3": "4:3",fill: t("stretch")}},"video.position": {label: t("position"),note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,default: "center",options: {top: t("top"),"top-half": t("top-half"),center: `${t("center")} (${t("default")})`,"bottom-half": t("bottom-half"),bottom: t("bottom")}},"video.saturation": {label: t("saturation"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.contrast": {label: t("contrast"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"video.brightness": {label: t("brightness"),default: 100,min: 50,max: 150,params: {suffix: "%",ticks: 25}},"audio.volume": {label: t("volume"),default: 100,min: 0,max: 600,params: {steps: 10,suffix: "%",ticks: 100}},"stats.items": {label: t("stats"),default: ["ping", "fps", "btr", "dt", "pl", "fl"],multipleOptions: {time: t("clock"),play: t("playtime"),batt: t("battery"),ping: t("stat-ping"),jit: t("jitter"),fps: t("stat-fps"),btr: t("stat-bitrate"),dt: t("stat-decode-time"),pl: t("stat-packets-lost"),fl: t("stat-frames-lost"),dl: t("downloaded"),ul: t("uploaded")},params: {size: 0},ready: (setting) => {let multipleOptions = setting.multipleOptions;if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];for (let key in multipleOptions)multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];}},"stats.showWhenPlaying": {label: t("show-stats-on-startup"),default: !1},"stats.quickGlance.enabled": {label: "👀 " + t("enable-quick-glance-mode"),default: !0},"stats.position": {label: t("position"),default: "top-right",options: {"top-left": t("top-left"),"top-center": t("top-center"),"top-right": t("top-right")}},"stats.textSize": {label: t("text-size"),default: "0.9rem",options: {"0.9rem": t("small"),"1.0rem": t("normal"),"1.1rem": t("large")}},"stats.opacity.all": {label: t("opacity"),default: 80,min: 50,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.opacity.background": {label: t("background-opacity"),default: 100,min: 0,max: 100,params: {steps: 10,suffix: "%",ticks: 10}},"stats.colors": {label: t("conditional-formatting"),default: !1},"localCoOp.enabled": {requiredVariants: "full",label: t("enable-local-co-op-support"),labelIcon: BxIcon.LOCAL_CO_OP,default: !1,note: () => CE("div", !1, CE("a", {href: "https://github.com/redphx/better-xcloud/discussions/275",target: "_blank"}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))}};gameSettings = {};xboxTitleId = -1;constructor() {super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);}setGameId(id) {this.xboxTitleId = id;}getGameSettings(id) {if (id > -1) {if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id);return this.gameSettings[id];}return null;}getSetting(key, checkUnsupported) {return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported);}getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) {let gameSettings = this.getGameSettings(id);if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported);if (returnBaseValue) return super.getSetting(key, checkUnsupported);return;}setSetting(key, value, origin) {return this.setSettingByGame(this.xboxTitleId, key, value, origin);}setSettingByGame(id, key, value, origin) {let gameSettings = this.getGameSettings(id);if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);}hasGameSetting(id, key) {let gameSettings = this.getGameSettings(id);return !!(gameSettings && gameSettings.hasSetting(key));}getControllerSetting(gamepadId) {let controllerSetting = this.getSetting("controller.settings")[gamepadId];if (!controllerSetting) controllerSetting = {};if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;return controllerSetting;}} +var STORAGE = {Global: new GlobalSettingsStorage,Stream: new StreamSettingsStorage}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage); STORAGE.Stream = streamSettingsStorage; var globalSettingsStorage = STORAGE.Global, getGlobalPrefDefinition = globalSettingsStorage.getDefinition.bind(globalSettingsStorage), getGlobalPref = globalSettingsStorage.getSetting.bind(globalSettingsStorage), setGlobalPref = globalSettingsStorage.setSetting.bind(globalSettingsStorage); -function isGlobalPref(prefKey) { -return ALL_PREFS.global.includes(prefKey); -} -function isStreamPref(prefKey) { -return ALL_PREFS.stream.includes(prefKey); -} -function getPrefInfo(prefKey) { -if (isGlobalPref(prefKey)) return { -storage: STORAGE.Global, -definition: getGlobalPrefDefinition(prefKey) -}; -else if (isStreamPref(prefKey)) return { -storage: STORAGE.Stream, -definition: getStreamPrefDefinition(prefKey) -}; -return alert("Missing pref definition: " + prefKey), {}; -} -function setPref(prefKey, value, origin) { -if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin); -else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin); -} -function checkForUpdate() { -if (SCRIPT_VERSION.includes("beta")) return; -let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getGlobalPref("version.current"), lastCheck = getGlobalPref("version.lastCheck"), now = Math.round(+new Date / 1000); -if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; -setGlobalPref("version.lastCheck", now, "direct"), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { -setGlobalPref("version.latest", json.tag_name.substring(1), "direct"), setGlobalPref("version.current", SCRIPT_VERSION, "direct"); -}), Translations.updateTranslations(currentVersion === SCRIPT_VERSION); -} -function disablePwa() { -if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return; -if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", { -value: !0 -}); -} -function hashCode(str) { -let hash = 0; -for (let i = 0, len = str.length;i < len; i++) { -let chr = str.charCodeAt(i); -hash = (hash << 5) - hash + chr, hash |= 0; -} -return hash; -} -function renderString(str, obj) { -return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => { -let name = p1 || p2; -return name in obj ? obj[name] : match; -}); -} -function ceilToNearest(value, interval) { -return Math.ceil(value / interval) * interval; -} -function floorToNearest(value, interval) { -return Math.floor(value / interval) * interval; -} -async function copyToClipboard(text, showToast = !0) { -try { -return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0; -} catch (err) { -console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 }); -} -return !1; -} -function productTitleToSlug(title) { -return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); -} -function parseDetailsPath(path) { -let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path); -if (!matches?.groups) return {}; -let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId; -return { titleSlug, productId }; -} -function clearAllData() { -for (let i = 0;i < localStorage.length; i++) { -let key = localStorage.key(i); -if (!key) continue; -if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key); -} -try { -indexedDB.deleteDatabase(LocalDb.DB_NAME); -} catch (e) {} -alert(t("clear-data-success")); -} -function blockAllNotifications() { -let blockFeatures = getGlobalPref("block.features"); -return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value)); -} -function blockSomeNotifications() { -let blockFeatures = getGlobalPref("block.features"); -if (blockAllNotifications()) return !1; -return ["friends", "notifications-achievements", "notifications-invites"].some((value) => blockFeatures.includes(value)); -} -function isPlainObject(input) { -return typeof input === "object" && input !== null && input.constructor === Object; -} -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; -quickGlanceObserver; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), 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.quickGlanceStop(), this.hideSettingsUi(); -} -isHidden = () => this.$container.classList.contains("bx-gone"); -isGlancing = () => this.$container.dataset.display === "glancing"; -quickGlanceSetup() { -if (!STATES.isPlaying || this.quickGlanceObserver) return; -let $uiContainer = document.querySelector("div[data-testid=ui-container]"); -if (!$uiContainer) return; -this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { -for (let record of mutationList) { -let $target = record.target; -if (!$target.className || !$target.className.startsWith("GripHandle")) continue; -if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0); -else this.stop(!0); -} -}), this.quickGlanceObserver.observe($uiContainer, { -attributes: !0, -attributeFilter: ["aria-expanded"], -subtree: !0 -}); -} -quickGlanceStop() { -this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null; -} -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) streamStats.quickGlanceSetup(), !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() {} -} +function isGlobalPref(prefKey) {return ALL_PREFS.global.includes(prefKey);} +function isStreamPref(prefKey) {return ALL_PREFS.stream.includes(prefKey);} +function getPrefInfo(prefKey) {if (isGlobalPref(prefKey)) return {storage: STORAGE.Global,definition: getGlobalPrefDefinition(prefKey)};else if (isStreamPref(prefKey)) return {storage: STORAGE.Stream,definition: getStreamPrefDefinition(prefKey)};return alert("Missing pref definition: " + prefKey), {};} +function setPref(prefKey, value, origin) {if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin);else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin);} +function checkForUpdate() {if (SCRIPT_VERSION.includes("beta")) return;let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getGlobalPref("version.current"), lastCheck = getGlobalPref("version.lastCheck"), now = Math.round(+new Date / 1000);if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return;setGlobalPref("version.lastCheck", now, "direct"), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => {setGlobalPref("version.latest", json.tag_name.substring(1), "direct"), setGlobalPref("version.current", SCRIPT_VERSION, "direct");}), Translations.updateTranslations(currentVersion === SCRIPT_VERSION);} +function disablePwa() {if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return;if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", {value: !0});} +function hashCode(str) {let hash = 0;for (let i = 0, len = str.length;i < len; i++) {let chr = str.charCodeAt(i);hash = (hash << 5) - hash + chr, hash |= 0;}return hash;} +function renderString(str, obj) {return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => {let name = p1 || p2;return name in obj ? obj[name] : match;});} +function ceilToNearest(value, interval) {return Math.ceil(value / interval) * interval;} +function floorToNearest(value, interval) {return Math.floor(value / interval) * interval;} +async function copyToClipboard(text, showToast = !0) {try {return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0;} catch (err) {console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 });}return !1;} +function productTitleToSlug(title) {return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase();} +function parseDetailsPath(path) {let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path);if (!matches?.groups) return {};let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId;return { titleSlug, productId };} +function clearAllData() {for (let i = 0;i < localStorage.length; i++) {let key = localStorage.key(i);if (!key) continue;if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key);}try {indexedDB.deleteDatabase(LocalDb.DB_NAME);} catch (e) {}alert(t("clear-data-success"));} +function blockAllNotifications() {let blockFeatures = getGlobalPref("block.features");return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value));} +function blockSomeNotifications() {let blockFeatures = getGlobalPref("block.features");if (blockAllNotifications()) return !1;return ["friends", "notifications-achievements", "notifications-invites"].some((value) => blockFeatures.includes(value));} +function isPlainObject(input) {return typeof input === "object" && input !== null && input.constructor === Object;} +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;quickGlanceObserver;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), 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.quickGlanceStop(), this.hideSettingsUi();}isHidden = () => this.$container.classList.contains("bx-gone");isGlancing = () => this.$container.dataset.display === "glancing";quickGlanceSetup() {if (!STATES.isPlaying || this.quickGlanceObserver) return;let $uiContainer = document.querySelector("div[data-testid=ui-container]");if (!$uiContainer) return;this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {for (let record of mutationList) {let $target = record.target;if (!$target.className || !$target.className.startsWith("GripHandle")) continue;if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0);else this.stop(!0);}}), this.quickGlanceObserver.observe($uiContainer, {attributes: !0,attributeFilter: ["aria-expanded"],subtree: !0});}quickGlanceStop() {this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null;}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) streamStats.quickGlanceSetup(), !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() {}} class MkbHandler {} -class MkbPopup { -static instance; -static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup); -popupType; -$popup; -$title; -$btnActivate; -mkbHandler; -constructor() { -this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => { -let $newButton = this.createActivateButton(); -this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton; -}); -} -attachMkbHandler(handler) { -this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller"); -} -toggleVisibility(show) { -this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1); -} -moveOffscreen(doMove) { -this.$popup.classList.toggle("bx-offscreen", doMove); -} -createActivateButton() { -let options = { -style: 1 | 1024 | 128, -label: t("activate"), -onClick: this.onActivate -}, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) }); -return createButton(options); -} -onActivate = (e) => { -e.preventDefault(), this.mkbHandler.toggle(!0); -}; -render() { -this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({ -label: t("ignore"), -style: 8, -onClick: (e) => { -e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1); -} -}), createButton({ -label: t("manage"), -icon: BxIcon.MANAGE, -style: 64, -onClick: () => { -let dialog = SettingsDialog.getInstance(); -dialog.focusTab("mkb"), dialog.show(); -} -}))), document.documentElement.appendChild(this.$popup); -} -reset() { -this.toggleVisibility(!0), this.moveOffscreen(!1); -} -} -class NativeMkbHandler extends MkbHandler { -static instance; -static getInstance() { -if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler; -else NativeMkbHandler.instance = null; -return NativeMkbHandler.instance; -} -LOG_TAG = "NativeMkbHandler"; -static isAllowed = () => { -return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on"; -}; -pointerClient; -enabled = !1; -mouseButtonsPressed = 0; -mouseVerticalMultiply = 0; -mouseHorizontalMultiply = 0; -inputChannel; -popup; -constructor() { -super(); -BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); -} -onKeyboardEvent(e) { -if (e.type === "keyup" && e.code === "F8") { -e.preventDefault(), this.toggle(); -return; -} -} -onPointerLockRequested(e) { -AppInterface.requestPointerCapture(), this.start(); -} -onPointerLockExited(e) { -AppInterface.releasePointerCapture(), this.stop(); -} -onPollingModeChanged = (e) => { -let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; -this.popup.moveOffscreen(move); -}; -onDialogShown = () => { -document.pointerLockElement && document.exitPointerLock(); -}; -handleEvent(event) { -switch (event.type) { -case "keyup": -this.onKeyboardEvent(event); -break; -case BxEvent.POINTER_LOCK_REQUESTED: -this.onPointerLockRequested(event); -break; -case BxEvent.POINTER_LOCK_EXITED: -this.onPointerLockExited(event); -break; -case BxEvent.XCLOUD_POLLING_MODE_CHANGED: -this.onPollingModeChanged(event); -break; -} -} -init() { -this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1); -try { -this.pointerClient.start(STATES.pointerServerPort, this); -} catch (e) { -Toast.show("Cannot enable Mouse & Keyboard feature"); -} -this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown); -let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) { -let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); -Toast.show(msg, t("native-mkb"), { html: !0 }); -} -this.waitForMouseData(!1); -} -toggle(force) { -let setEnable; -if (typeof force !== "undefined") setEnable = force; -else setEnable = !this.enabled; -if (setEnable) document.documentElement.requestPointerLock(); -else document.exitPointerLock(); -} -updateInputConfigurationAsync(enabled) { -window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({ -enableKeyboardInput: enabled, -enableMouseInput: enabled, -enableAbsoluteMouse: !1, -enableTouchInput: !1 -}); -} -start() { -this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); -} -stop() { -this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0); -} -destroy() { -this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock(); -} -handleMouseMove(data) { -this.sendMouseInput({ -X: data.movementX, -Y: data.movementY, -Buttons: this.mouseButtonsPressed, -WheelX: 0, -WheelY: 0 -}); -} -handleMouseClick(data) { -let { pointerButton, pressed } = data; -if (pressed) this.mouseButtonsPressed |= pointerButton; -else this.mouseButtonsPressed ^= pointerButton; -this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: this.mouseButtonsPressed, -WheelX: 0, -WheelY: 0 -}); -} -handleMouseWheel(data) { -let { vertical, horizontal } = data, mouseWheelX = horizontal; -if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply; -let mouseWheelY = vertical; -if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply; -return this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: this.mouseButtonsPressed, -WheelX: mouseWheelX, -WheelY: mouseWheelY -}), !0; -} -setVerticalScrollMultiplier(vertical) { -this.mouseVerticalMultiply = vertical; -} -setHorizontalScrollMultiplier(horizontal) { -this.mouseHorizontalMultiply = horizontal; -} -waitForMouseData(showPopup) { -this.popup.toggleVisibility(showPopup); -} -isEnabled() { -return this.enabled; -} -sendMouseInput(data) { -data.Type = 0, this.inputChannel?.queueMouseInput(data); -} -resetMouseInput() { -this.mouseButtonsPressed = 0, this.sendMouseInput({ -X: 0, -Y: 0, -Buttons: 0, -WheelX: 0, -WheelY: 0 -}); -} -} -function showGamepadToast(gamepad) { -if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; -if (gamepad._noToast) return; -BxLogger.info("Gamepad", gamepad); -let text = "🎮"; -if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`; -let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, ""); -text += ` - ${gamepadId}`; -let status; -if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status"); -else status = t("disconnected"); -Toast.show(text, status, { instant: !1 }); -} -function simplifyGamepadName(name) { -return name.replace(/\s+\(.*Vendor: ([0-9a-f]{4}) Product: ([0-9a-f]{4})\)$/, " ($1-$2)"); -} -function getUniqueGamepadNames() { -let gamepads = window.navigator.getGamepads(), names = []; -for (let gamepad of gamepads) -if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id); -return names; -} -function hasGamepad() { -let gamepads = window.navigator.getGamepads(); -for (let gamepad of gamepads) -if (gamepad?.connected) return !0; -return !1; -} -function generateVirtualControllerMapping(index, override = {}) { -return Object.assign({}, { -GamepadIndex: index, -A: 0, -B: 0, -X: 0, -Y: 0, -LeftShoulder: 0, -RightShoulder: 0, -LeftTrigger: 0, -RightTrigger: 0, -View: 0, -Menu: 0, -LeftThumb: 0, -RightThumb: 0, -DPadUp: 0, -DPadDown: 0, -DPadLeft: 0, -DPadRight: 0, -Nexus: 0, -LeftThumbXAxis: 0, -LeftThumbYAxis: 0, -RightThumbXAxis: 0, -RightThumbYAxis: 0, -PhysicalPhysicality: 0, -VirtualPhysicality: 0, -Dirty: !1, -Virtual: !1 -}, override); -} -function getGamepadPrompt(gamepadKey) { -return GamepadKeyName[gamepadKey][1]; -} -var XCLOUD_GAMEPAD_KEY_MAPPING = { -0: "A", -1: "B", -2: "X", -3: "Y", -12: "DPadUp", -15: "DPadRight", -13: "DPadDown", -14: "DPadLeft", -4: "LeftShoulder", -5: "RightShoulder", -6: "LeftTrigger", -7: "RightTrigger", -10: "LeftThumb", -11: "RightThumb", -104: "LeftStickAxes", -204: "RightStickAxes", -8: "View", -9: "Menu", -16: "Nexus", -17: "Share", -102: "LeftThumbXAxis", -103: "LeftThumbXAxis", -100: "LeftThumbYAxis", -101: "LeftThumbYAxis", -202: "RightThumbXAxis", -203: "RightThumbXAxis", -200: "RightThumbYAxis", -201: "RightThumbYAxis" -}; -function toXcloudGamepadKey(gamepadKey) { -return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey]; -} -var PointerToMouseButton = { -1: 0, -2: 2, -4: 1 -}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; -class WebSocketMouseDataProvider extends MouseDataProvider { -pointerClient; -isConnected = !1; -init() { -this.pointerClient = PointerClient.getInstance(), this.isConnected = !1; -try { -this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0; -} catch (e) { -Toast.show("Cannot enable Mouse & Keyboard feature"); -} -} -start() { -this.isConnected && AppInterface.requestPointerCapture(); -} -stop() { -this.isConnected && AppInterface.releasePointerCapture(); -} -destroy() { -this.isConnected && this.pointerClient?.stop(); -} -} -class PointerLockMouseDataProvider extends MouseDataProvider { -start() { -window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu); -} -stop() { -document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu); -} -onMouseMoveEvent = (e) => { -this.mkbHandler.handleMouseMove({ -movementX: e.movementX, -movementY: e.movementY -}); -}; -onMouseEvent = (e) => { -e.preventDefault(); -let data = { -mouseButton: e.button, -pressed: e.type === "mousedown" -}; -this.mkbHandler.handleMouseClick(data); -}; -onWheelEvent = (e) => { -if (!KeyHelper.getKeyFromEvent(e)) return; -let data = { -vertical: e.deltaY, -horizontal: e.deltaX -}; -if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); -}; -disableContextMenu = (e) => e.preventDefault(); -} -class EmulatedMkbHandler extends MkbHandler { -static instance; -static getInstance() { -if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler; -else EmulatedMkbHandler.instance = null; -return EmulatedMkbHandler.instance; -} -static LOG_TAG = "EmulatedMkbHandler"; -static isAllowed() { -return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile()); -} -PRESET; -VIRTUAL_GAMEPAD = { -id: VIRTUAL_GAMEPAD_ID, -index: 0, -connected: !1, -hapticActuators: null, -mapping: "standard", -axes: [0, 0, 0, 0], -buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })), -timestamp: performance.now(), -vibrationActuator: null -}; -nativeGetGamepads; -xCloudGamepad = generateVirtualControllerMapping(0); -initialized = !1; -enabled = !1; -mouseDataProvider; -isPolling = !1; -prevWheelCode = null; -wheelStoppedTimeoutId = null; -detectMouseStoppedTimeoutId = null; -escKeyDownTime = -1; -LEFT_STICK_X = []; -LEFT_STICK_Y = []; -RIGHT_STICK_X = []; -RIGHT_STICK_Y = []; -popup; -STICK_MAP = { -102: [this.LEFT_STICK_X, -1], -103: [this.LEFT_STICK_X, 1], -100: [this.LEFT_STICK_Y, 1], -101: [this.LEFT_STICK_Y, -1], -202: [this.RIGHT_STICK_X, -1], -203: [this.RIGHT_STICK_X, 1], -200: [this.RIGHT_STICK_Y, 1], -201: [this.RIGHT_STICK_Y, -1] -}; -constructor() { -super(); -BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this); -} -isEnabled = () => this.enabled; -patchedGetGamepads = () => { -let gamepads = this.nativeGetGamepads() || []; -return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads; -}; -getVirtualGamepad = () => this.VIRTUAL_GAMEPAD; -updateStick(stick, x, y) { -let gamepad = this.xCloudGamepad; -if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y; -else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y; -window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); -} -vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); -resetXcloudGamepads() { -let index = getStreamPref("mkb.p1.slot") - 1; -this.xCloudGamepad = generateVirtualControllerMapping(0, { -GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0, -Dirty: !0 -}), this.VIRTUAL_GAMEPAD.index = index; -} -pressButton(buttonIndex, pressed) { -let xCloudKey = toXcloudGamepadKey(buttonIndex); -if (buttonIndex >= 100) { -let [valueArr] = this.STICK_MAP[buttonIndex]; -for (let i = valueArr.length - 1;i >= 0; i--) -if (valueArr[i] === buttonIndex) valueArr.splice(i, 1); -pressed && valueArr.push(buttonIndex); -let value; -if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1]; -else value = 0; -this.xCloudGamepad[xCloudKey] = value; -} else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0; -window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]); -} -onKeyboardEvent = (e) => { -let isKeyDown = e.type === "keydown"; -if (e.code === "Escape") { -if (e.preventDefault(), this.enabled && isKeyDown) { -if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now(); -else if (performance.now() - this.escKeyDownTime >= 1000) this.stop(); -} else this.escKeyDownTime = -1; -return; -} -if (!this.isPolling || !this.PRESET) return; -if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return; -let buttonIndex = this.PRESET.mapping[e.code || e.key]; -if (typeof buttonIndex === "undefined") return; -if (e.repeat) return; -e.preventDefault(), this.pressButton(buttonIndex, isKeyDown); -}; -onMouseStopped = () => { -if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return; -let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1; -this.updateStick(analog, 0, 0); -}; -handleMouseClick(data) { -let mouseButton; -if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton; -else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton]; -let key = { -code: "Mouse" + mouseButton -}; -if (!this.PRESET) return; -let buttonIndex = this.PRESET.mapping[key.code]; -if (typeof buttonIndex === "undefined") return; -this.pressButton(buttonIndex, data.pressed); -} -handleMouseMove(data) { -let preset = this.PRESET; -if (!preset) return; -let mouseMapTo = preset.mouse["mapTo"]; -if (mouseMapTo === 0) return; -this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50); -let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y); -if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; -else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length; -let analog = mouseMapTo === 1 ? 0 : 1; -this.updateStick(analog, x, y); -} -handleMouseWheel(data) { -let code = ""; -if (data.vertical < 0) code = "ScrollUp"; -else if (data.vertical > 0) code = "ScrollDown"; -else if (data.horizontal < 0) code = "ScrollLeft"; -else if (data.horizontal > 0) code = "ScrollRight"; -if (!code) return !1; -if (!this.PRESET) return !1; -let key = { -code -}, buttonIndex = this.PRESET.mapping[key.code]; -if (typeof buttonIndex === "undefined") return !1; -if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0); -return this.wheelStoppedTimeoutId = window.setTimeout(() => { -this.prevWheelCode = null, this.pressButton(buttonIndex, !1); -}, 20), !0; -} -async toggle(force) { -if (!this.initialized) return; -if (typeof force !== "undefined") this.enabled = force; -else this.enabled = !this.enabled; -if (this.enabled) try { -await document.body.requestPointerLock({ unadjustedMovement: !0 }); -} catch (e) { -document.body.requestPointerLock(), console.log(e); -} -else document.pointerLockElement && document.exitPointerLock(); -} -refreshPresetData() { -this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads(); -} -waitForMouseData(showPopup) { -this.popup.toggleVisibility(showPopup); -} -onPollingModeChanged = (e) => { -let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none"; -this.popup.moveOffscreen(move); -}; -onDialogShown = () => { -document.pointerLockElement && document.exitPointerLock(); -}; -onPointerLockChange = () => { -if (document.pointerLockElement) this.start(); -else this.stop(); -}; -onPointerLockError = (e) => { -console.log(e), this.stop(); -}; -onPointerLockRequested = () => { -this.start(); -}; -onPointerLockExited = () => { -this.mouseDataProvider?.stop(); -}; -handleEvent(event) { -switch (event.type) { -case BxEvent.POINTER_LOCK_REQUESTED: -this.onPointerLockRequested(); -break; -case BxEvent.POINTER_LOCK_EXITED: -this.onPointerLockExited(); -break; -} -} -init() { -if (!STATES.browser.capabilities.mkb) { -this.initialized = !1; -return; -} -if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this); -else this.mouseDataProvider = new PointerLockMouseDataProvider(this); -if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this); -else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError); -if (MkbPopup.getInstance().reset(), AppInterface) { -let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle"); -if (shortcutKey) { -let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` }); -Toast.show(msg, t("native-mkb"), { html: !0 }); -} -this.waitForMouseData(!1); -} else this.waitForMouseData(!0); -} -destroy() { -if (!this.initialized) return; -if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this); -else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError); -window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged); -} -start() { -if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); -this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start(); -let virtualGamepad = this.getVirtualGamepad(); -virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", { -gamepad: virtualGamepad -}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 }); -} -stop() { -this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1; -let virtualGamepad = this.getVirtualGamepad(); -if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", { -gamepad: virtualGamepad -}), window.navigator.getGamepads = this.nativeGetGamepads; -this.waitForMouseData(!0), this.mouseDataProvider?.stop(); -} -static setupEvents() { -if (BxEventBus.Stream.on("state.playing", () => { -if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init(); -else EmulatedMkbHandler.getInstance()?.init(); -}), EmulatedMkbHandler.isAllowed()) -BxEventBus.Stream.on("mkb.setting.updated", () => { -EmulatedMkbHandler.getInstance()?.refreshPresetData(); -}); -} -} -class StreamSettings { -static settings = { -settings: {}, -xCloudPollingMode: "all", -deviceVibrationIntensity: 0, -controllerPollingRate: 4, -controllers: {}, -mkbPreset: null, -keyboardShortcuts: {} -}; -static async refreshControllerSettings() { -let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads(); -for (let gamepad of gamepads) { -if (!gamepad?.connected) continue; -if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; -let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data); -controllers[gamepad.id] = { -shortcuts: shortcutsMapping, -customization: customizationData -}; -} -settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration(); -} -static preCalculateControllerRange(obj, target, values) { -if (values && Array.isArray(values)) { -let [from, to] = values; -if (from > 1 || to < 100) obj[target] = [from / 100, to / 100]; -} -} -static convertControllerCustomization(customization) { -if (!customization) return null; -let converted = { -mapping: {}, -ranges: {}, -vibrationIntensity: 1 -}, gamepadKey; -for (gamepadKey in customization.mapping) { -let gamepadStr = toXcloudGamepadKey(gamepadKey); -if (!gamepadStr) continue; -let mappedKey = customization.mapping[gamepadKey]; -if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey); -else converted.mapping[gamepadStr] = !1; -} -return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted; -} -static async refreshDeviceVibration() { -if (!STATES.browser.capabilities.deviceVibration) return; -let mode = getStreamPref("deviceVibration.mode"), intensity = 0; -if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100; -StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {}); -} -static async refreshMkbSettings() { -let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = { -mapping: {}, -mouse: Object.assign({}, orgPresetData.mouse) -}, key; -for (key in orgPresetData.mapping) { -let buttonIndex = parseInt(key); -if (!orgPresetData.mapping[buttonIndex]) continue; -for (let keyName of orgPresetData.mapping[buttonIndex]) -if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex; -} -let mouse = converted.mouse; -mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {}); -} -static async refreshKeyboardShortcuts() { -let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId"); -if (presetId === 0) { -settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); -return; -} -let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action; -for (action in orgPresetData) { -let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`; -converted[key] = action; -} -settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {}); -} -static async refreshAllSettings() { -window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts(); -} -static findKeyboardShortcut(targetAction) { -let shortcuts = StreamSettings.settings.keyboardShortcuts; -for (let codeStr in shortcuts) -if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr); -return null; -} -static setup() { -let listener = () => { -StreamSettings.refreshControllerSettings(); -}; -window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings(); -} -} -class BxNumberStepper extends HTMLInputElement { -intervalId = null; -isHolding; -controlValue; -controlMin; -controlMax; -uiMin; -uiMax; -steps; -options; -onChange; -$text; -$btnInc; -$btnDec; -$range; -onRangeInput; -onClick; -onPointerUp; -onPointerDown; -setValue; -normalizeValue; -static create(key, value, min, max, options = {}, onChange) { -options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; -let $text, $btnInc, $btnDec, $range, self = CE("div", { -class: "bx-number-stepper", -id: `bx_setting_${escapeCssSelector(key)}` -}, CE("div", !1, $btnDec = CE("button", { -_dataset: { -type: "dec" -}, -type: "button", -class: options.hideSlider ? "bx-focusable" : "", -tabindex: options.hideSlider ? 0 : -1 -}, "-"), $text = CE("span"), $btnInc = CE("button", { -_dataset: { -type: "inc" -}, -type: "button", -class: options.hideSlider ? "bx-focusable" : "", -tabindex: options.hideSlider ? 0 : -1 -}, "+"))); -if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self; -if ($range = CE("input", { -id: `bx_inp_setting_${key}`, -type: "range", -min: self.uiMin, -max: self.uiMax, -value: options.reverse ? -value : value, -step: self.steps, -tabindex: 0 -}), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) { -let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId }); -if ($range.setAttribute("list", markersId), options.exactTicks) { -let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks; -if (start === min) start += options.exactTicks; -for (let i = start;i < max; i += options.exactTicks) -$markers.appendChild(CE("option", { -value: options.reverse ? -i : i -})); -} else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks) -$markers.appendChild(CE("option", { value: i })); -self.appendChild($markers); -} -return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, { -focus: options.hideSlider ? $btnInc : $range -}), Object.defineProperty(self, "value", { -get() { -return self.controlValue; -}, -set(value2) { -BxNumberStepper.setValue.call(self, value2); -} -}), Object.defineProperty(self, "disabled", { -get() { -return $range.disabled; -}, -set(value2) { -$btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2; -} -}), self; -} -static setValue(value) { -if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString(); -BxNumberStepper.updateButtonsVisibility.call(this); -} -static normalizeValue(value) { -return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value; -} -static onRangeInput(e) { -let value = parseInt(e.target.value); -if (this.options.reverse) value *= -1; -if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value); -} -static onClick(e) { -if (e.preventDefault(), this.isHolding) return; -let $btn = e.target.closest("button"); -$btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; -} -static onPointerDown(e) { -BxNumberStepper.clearIntervalId.call(this); -let $btn = e.target.closest("button"); -if (!$btn) return; -this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => { -BxNumberStepper.buttonPressed.call(this, e2, $btn); -}, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 }); -} -static onPointerUp(e) { -BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1; -} -static onContextMenu(e) { -e.preventDefault(); -} -static updateTextValue() { -let value = this.controlValue, textContent = null; -if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax); -if (textContent === null) textContent = value.toString() + this.options.suffix; -return textContent; -} -static buttonPressed(e, $btn) { -BxNumberStepper.change.call(this, $btn.dataset.type); -} -static change(direction) { -let value = this.controlValue; -if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps); -else value = Math.min(this.uiMax, value + this.steps); -value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue); -} -static clearIntervalId() { -this.intervalId && clearInterval(this.intervalId), this.intervalId = null; -} -static updateButtonsVisibility() { -if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this); -} -} -class SettingElement { -static renderOptions(key, setting, currentValue, onChange) { -let $control = CE("select", { -tabindex: 0 -}), $parent; -if (setting.optionsGroup) $parent = CE("optgroup", { -label: setting.optionsGroup -}), $control.appendChild($parent); -else $parent = $control; -for (let value in setting.options) { -let label = setting.options[value], $option = CE("option", { value }, label); -$parent.appendChild($option); -} -return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { -let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value; -!e.ignoreOnChange && onChange(e, value); -}), $control.setValue = (value) => { -$control.value = value; -}, $control; -} -static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) { -let $control = CE("select", { -multiple: !0, -tabindex: 0 -}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions; -$control.setAttribute("size", size.toString()); -for (let value in setting.multipleOptions) { -let label = setting.multipleOptions[value], $option = CE("option", { value }, label); -$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { -e.preventDefault(); -let target = e.target; -target.selected = !target.selected; -let $parent = target.parentElement; -$parent.focus(), BxEvent.dispatch($parent, "input"); -}), $control.appendChild($option); -} -return $control.addEventListener("mousedown", function(e) { -let self = this, orgScrollTop = self.scrollTop; -window.setTimeout(() => self.scrollTop = orgScrollTop, 0); -}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { -let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); -!e.ignoreOnChange && onChange(e, values); -}), Object.defineProperty($control, "value", { -get() { -return Array.from($control.options).filter((option) => option.selected).map((option) => option.value); -}, -set(value) { -let values = value.split(","); -Array.from($control.options).forEach((option) => { -option.selected = values.includes(option.value); -}); -} -}), $control; -} -static renderCheckbox(key, setting, currentValue, onChange) { -let $control = CE("input", { type: "checkbox", tabindex: 0 }); -return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => { -!e.ignoreOnChange && onChange(e, e.target.checked); -}), $control.setValue = (value) => { -$control.checked = !!value; -}, $control; -} -static renderNumberStepper(key, setting, value, onChange, options = {}) { -return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange); -} -static METHOD_MAP = { -options: SettingElement.renderOptions, -"multiple-options": SettingElement.renderMultipleOptions, -"number-stepper": SettingElement.renderNumberStepper, -checkbox: SettingElement.renderCheckbox -}; -static render(type, key, setting, currentValue, onChange, options) { -let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); -if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`; -if (type === "options" || type === "multiple-options") $control.name = $control.id; -return $control; -} -static fromPref(key, onChange, overrideParams = {}) { -let { definition, storage } = getPrefInfo(key); -if (!definition) return null; -let currentValue = storage.getSetting(key), type; -if ("options" in definition) type = "options"; -else if ("multipleOptions" in definition) type = "multiple-options"; -else if (typeof definition.default === "number") type = "number-stepper"; -else type = "checkbox"; -let params = {}; -if ("params" in definition) params = Object.assign(overrideParams, definition.params || {}); -if (params.disabled) currentValue = definition.default; -return SettingElement.render(type, key, definition, currentValue, (e, value) => { -if (isGlobalPref(key)) setGlobalPref(key, value, "ui"); -else { -let id = SettingsManager.getInstance().getTargetGameId(); -setGamePref(id, key, value, "ui"); -} -onChange && onChange(e, value); -}, params); -} -} -class BxSelectElement extends HTMLSelectElement { -isControllerFriendly; -optionsList; -indicatorsList; -$indicators; -visibleIndex; -isMultiple; -$select; -$btnNext; -$btnPrev; -$label; -$checkBox; -static create($select, forceFriendly = !1) { -let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly"); -if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select; -$select.removeAttribute("tabindex"); -let $wrapper = CE("div", { -class: "bx-select", -_dataset: { -controllerFriendly: isControllerFriendly -} -}); -if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width"); -let $content, self = $wrapper; -self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = []; -let $btnPrev, $btnNext; -if (isControllerFriendly) { -$btnPrev = createButton({ -label: "<", -style: 64 -}), $btnNext = createButton({ -label: ">", -style: 64 -}), setNearby($wrapper, { -orientation: "horizontal", -focus: $btnNext -}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev; -let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self); -$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext); -} else $select.addEventListener("change", (e) => { -self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -}); -if (self.isMultiple) $content = CE("button", { -class: "bx-select-value bx-focusable", -tabindex: 0 -}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => { -self.$checkBox.click(); -}), self.$checkBox.addEventListener("input", (e) => { -let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex); -$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); -}); -else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators); -return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => { -mutationList.forEach((mutation) => { -if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -}); -}).observe($select, { -subtree: !0, -childList: !0, -attributes: !0 -}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", { -get() { -return $select.value; -}, -set(value) { -self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self); -} -}), Object.defineProperty(self, "disabled", { -get() { -return $select.disabled; -}, -set(value) { -$select.disabled = value; -} -}), self.addEventListener = function() { -$select.addEventListener.apply($select, arguments); -}, self.removeEventListener = function() { -$select.removeEventListener.apply($select, arguments); -}, self.dispatchEvent = function() { -return $select.dispatchEvent.apply($select, arguments); -}, self.appendChild = function(node) { -return $select.appendChild(node), node; -}, self; -} -static resetIndicators() { -let { -optionsList, -indicatorsList, -$indicators -} = this, targetSize = optionsList.length; -if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize) -indicatorsList.pop()?.remove(); -else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) { -let $indicator = CE("span", {}); -indicatorsList.push($indicator), $indicators.appendChild($indicator); -} -for (let $indicator of indicatorsList) -clearDataSet($indicator); -$indicators.classList.toggle("bx-invisible", targetSize <= 1); -} -static getOptionAtIndex(index) { -return this.optionsList[index]; -} -static render(e) { -let { -$label, -$btnNext, -$btnPrev, -$checkBox, -optionsList, -indicatorsList -} = this; -if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex; -this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex); -let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = ""; -if ($option) { -let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup"); -if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) { -let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " "; -$label.innerHTML = ""; -let fragment = document.createDocumentFragment(); -fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment); -} else $label.textContent = content; -} else $label.textContent = content; -if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); -let disableButtons = optionsList.length <= 1; -$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons); -for (let i = 0;i < optionsList.length; i++) { -let $option2 = optionsList[i], $indicator = indicatorsList[i]; -if (!$option2 || !$indicator) continue; -if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true"; -if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true"; -} -} -static normalizeIndex(index) { -return Math.min(Math.max(index, 0), this.optionsList.length - 1); -} -static onPrevNext(e) { -if (!e.target) return; -let { -$btnNext, -$select, -isMultiple, -visibleIndex: currentIndex -} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1; -if (newIndex > this.optionsList.length - 1) newIndex = 0; -else if (newIndex < 0) newIndex = this.optionsList.length - 1; -if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; -if (isMultiple) BxSelectElement.render.call(this); -else BxEvent.dispatch($select, "input"); -} -} -class XboxApi { -static CACHED_TITLES = {}; -static async getProductTitle(xboxTitleId) { -if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; -try { -let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle; -return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle; -} catch (e) {} -return; -} -} -class SettingsManager { -static instance; -static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager); -$streamSettingsSelection; -$tips; -playingGameId = -1; -targetGameId = -1; -SETTINGS = { -"localCoOp.enabled": { -onChange: () => { -BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled")); -} -}, -"deviceVibration.mode": { -onChange: StreamSettings.refreshControllerSettings -}, -"deviceVibration.intensity": { -onChange: StreamSettings.refreshControllerSettings -}, -"controller.pollingRate": { -onChange: StreamSettings.refreshControllerSettings -}, -"controller.settings": { -onChange: StreamSettings.refreshControllerSettings -}, -"nativeMkb.scroll.sensitivityX": { -onChange: () => { -let value = getStreamPref("nativeMkb.scroll.sensitivityX"); -NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100); -} -}, -"nativeMkb.scroll.sensitivityY": { -onChange: () => { -let value = getStreamPref("nativeMkb.scroll.sensitivityY"); -NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100); -} -}, -"video.player.type": { -onChange: () => { -if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer(); -}, -alwaysTriggerOnChange: !0 -}, -"video.player.powerPreference": { -onChange: () => { -let streamPlayer = STATES.currentStream.streamPlayer; -if (!streamPlayer) return; -streamPlayer.reloadPlayer(), updateVideoPlayer(); -} -}, -"video.processing": { -onChange: updateVideoPlayer -}, -"video.processing.sharpness": { -onChange: updateVideoPlayer -}, -"video.maxFps": { -onChange: () => { -let value = getStreamPref("video.maxFps"); -limitVideoPlayerFps(value); -} -}, -"video.ratio": { -onChange: updateVideoPlayer -}, -"video.brightness": { -onChange: updateVideoPlayer -}, -"video.contrast": { -onChange: updateVideoPlayer -}, -"video.saturation": { -onChange: updateVideoPlayer -}, -"video.position": { -onChange: updateVideoPlayer -}, -"audio.volume": { -onChange: () => { -let value = getStreamPref("audio.volume"); -SoundShortcut.setGainNodeVolume(value); -} -}, -"stats.items": { -onChange: StreamStats.refreshStyles -}, -"stats.quickGlance.enabled": { -onChange: () => { -let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance(); -value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); -} -}, -"stats.position": { -onChange: StreamStats.refreshStyles -}, -"stats.textSize": { -onChange: StreamStats.refreshStyles -}, -"stats.opacity.all": { -onChange: StreamStats.refreshStyles -}, -"stats.opacity.background": { -onChange: StreamStats.refreshStyles -}, -"stats.colors": { -onChange: StreamStats.refreshStyles -}, -"mkb.p1.preset.mappingId": { -onChange: StreamSettings.refreshMkbSettings -}, -"mkb.p1.slot": { -onChange: () => { -EmulatedMkbHandler.getInstance()?.resetXcloudGamepads(); -} -}, -"keyboardShortcuts.preset.inGameId": { -onChange: StreamSettings.refreshKeyboardShortcuts -} -}; -constructor() { -BxEventBus.Stream.on("setting.changed", (data) => { -if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey); -}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => { -this.switchGameSettings(id); -}), this.renderStreamSettingsSelection(); -} -updateStreamElement(key, onChanges) { -let info = this.SETTINGS[key]; -if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange); -else info.onChange(); -let $elm = info.$element; -if (!$elm) return; -let value = getGamePref(this.targetGameId, key, !0); -if ("setValue" in $elm) $elm.setValue(value); -else $elm.value = value.toString(); -this.updateDataset($elm, key); -} -switchGameSettings(id) { -if (setGameIdPref(id), this.targetGameId === id) return; -let onChanges = new Set, oldGameId = this.targetGameId; -this.targetGameId = id; -let key; -for (key in this.SETTINGS) { -if (!isStreamPref(key)) continue; -let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0); -if (oldValue === newValue) continue; -this.updateStreamElement(key, onChanges); -} -onChanges.forEach((onChange) => { -onChange && onChange(); -}), this.$tips.classList.toggle("bx-gone", id < 0); -} -setElement(pref, $elm) { -if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; -this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm; -} -getElement(pref, params) { -if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {}; -let $elm = this.SETTINGS[pref].$element; -if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm; -return this.updateDataset($elm, pref), $elm; -} -hasElement(pref) { -return !!this.SETTINGS[pref]?.$element; -} -updateDataset($elm, pref) { -if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true"; -else delete $elm.dataset.override; -} -renderStreamSettingsSelection() { -this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`); -let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0); -$select.addEventListener("input", (e) => { -let id = parseInt($select.value); -BxEventBus.Stream.emit("gameSettings.switched", { id }); -}), this.$streamSettingsSelection = CE("div", { -class: "bx-stream-settings-selection bx-gone", -_nearby: { orientation: "vertical" } -}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => { -this.playingGameId = id, setGameIdPref(id); -let $optGroup = $select.querySelector("optgroup"); -while ($optGroup.childElementCount > 1) -$optGroup.lastElementChild?.remove(); -if (id >= 0) { -let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id); -$optGroup.appendChild(CE("option", { -value: id -}, title)), $select.value = id.toString(); -} else $select.value = "-1"; -BxEventBus.Stream.emit("gameSettings.switched", { id }); -}); -} -getStreamSettingsSelection() { -return this.$streamSettingsSelection; -} -getTargetGameId() { -return this.targetGameId; -} -} -function onChangeVideoPlayerType() { -let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance(); -if (!settingsManager.hasElement("video.processing")) return; -let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); -if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); -else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; -$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"); -} -function limitVideoPlayerFps(targetFps) { -STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); -} -function updateVideoPlayer() { -let streamPlayer = STATES.currentStream.streamPlayer; -if (!streamPlayer) return; -limitVideoPlayerFps(getStreamPref("video.maxFps")); -let options = { -processing: getStreamPref("video.processing"), -sharpness: getStreamPref("video.processing.sharpness"), -saturation: getStreamPref("video.saturation"), -contrast: getStreamPref("video.contrast"), -brightness: getStreamPref("video.brightness") -}; -streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); -} +class MkbPopup {static instance;static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup);popupType;$popup;$title;$btnActivate;mkbHandler;constructor() {this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => {let $newButton = this.createActivateButton();this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton;});}attachMkbHandler(handler) {this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller");}toggleVisibility(show) {this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1);}moveOffscreen(doMove) {this.$popup.classList.toggle("bx-offscreen", doMove);}createActivateButton() {let options = {style: 1 | 1024 | 128,label: t("activate"),onClick: this.onActivate}, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) });return createButton(options);}onActivate = (e) => {e.preventDefault(), this.mkbHandler.toggle(!0);};render() {this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({label: t("ignore"),style: 8,onClick: (e) => {e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1);}}), createButton({label: t("manage"),icon: BxIcon.MANAGE,style: 64,onClick: () => {let dialog = SettingsDialog.getInstance();dialog.focusTab("mkb"), dialog.show();}}))), document.documentElement.appendChild(this.$popup);}reset() {this.toggleVisibility(!0), this.moveOffscreen(!1);}} +class NativeMkbHandler extends MkbHandler {static instance;static getInstance() {if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler;else NativeMkbHandler.instance = null;return NativeMkbHandler.instance;}LOG_TAG = "NativeMkbHandler";static isAllowed = () => {return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on";};pointerClient;enabled = !1;mouseButtonsPressed = 0;mouseVerticalMultiply = 0;mouseHorizontalMultiply = 0;inputChannel;popup;constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);}onKeyboardEvent(e) {if (e.type === "keyup" && e.code === "F8") {e.preventDefault(), this.toggle();return;}}onPointerLockRequested(e) {AppInterface.requestPointerCapture(), this.start();}onPointerLockExited(e) {AppInterface.releasePointerCapture(), this.stop();}onPollingModeChanged = (e) => {let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";this.popup.moveOffscreen(move);};onDialogShown = () => {document.pointerLockElement && document.exitPointerLock();};handleEvent(event) {switch (event.type) {case "keyup":this.onKeyboardEvent(event);break;case BxEvent.POINTER_LOCK_REQUESTED:this.onPointerLockRequested(event);break;case BxEvent.POINTER_LOCK_EXITED:this.onPointerLockExited(event);break;case BxEvent.XCLOUD_POLLING_MODE_CHANGED:this.onPollingModeChanged(event);break;}}init() {this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1);try {this.pointerClient.start(STATES.pointerServerPort, this);} catch (e) {Toast.show("Cannot enable Mouse & Keyboard feature");}this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown);let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");if (shortcutKey) {let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });Toast.show(msg, t("native-mkb"), { html: !0 });}this.waitForMouseData(!1);}toggle(force) {let setEnable;if (typeof force !== "undefined") setEnable = force;else setEnable = !this.enabled;if (setEnable) document.documentElement.requestPointerLock();else document.exitPointerLock();}updateInputConfigurationAsync(enabled) {window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({enableKeyboardInput: enabled,enableMouseInput: enabled,enableAbsoluteMouse: !1,enableTouchInput: !1});}start() {this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });}stop() {this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0);}destroy() {this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock();}handleMouseMove(data) {this.sendMouseInput({X: data.movementX,Y: data.movementY,Buttons: this.mouseButtonsPressed,WheelX: 0,WheelY: 0});}handleMouseClick(data) {let { pointerButton, pressed } = data;if (pressed) this.mouseButtonsPressed |= pointerButton;else this.mouseButtonsPressed ^= pointerButton;this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({X: 0,Y: 0,Buttons: this.mouseButtonsPressed,WheelX: 0,WheelY: 0});}handleMouseWheel(data) {let { vertical, horizontal } = data, mouseWheelX = horizontal;if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply;let mouseWheelY = vertical;if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply;return this.sendMouseInput({X: 0,Y: 0,Buttons: this.mouseButtonsPressed,WheelX: mouseWheelX,WheelY: mouseWheelY}), !0;}setVerticalScrollMultiplier(vertical) {this.mouseVerticalMultiply = vertical;}setHorizontalScrollMultiplier(horizontal) {this.mouseHorizontalMultiply = horizontal;}waitForMouseData(showPopup) {this.popup.toggleVisibility(showPopup);}isEnabled() {return this.enabled;}sendMouseInput(data) {data.Type = 0, this.inputChannel?.queueMouseInput(data);}resetMouseInput() {this.mouseButtonsPressed = 0, this.sendMouseInput({X: 0,Y: 0,Buttons: 0,WheelX: 0,WheelY: 0});}} +function showGamepadToast(gamepad) {if (gamepad.id === VIRTUAL_GAMEPAD_ID) return;if (gamepad._noToast) return;BxLogger.info("Gamepad", gamepad);let text = "🎮";if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`;let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");text += ` - ${gamepadId}`;let status;if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");else status = t("disconnected");Toast.show(text, status, { instant: !1 });} +function simplifyGamepadName(name) {return name.replace(/\s+\(.*Vendor: ([0-9a-f]{4}) Product: ([0-9a-f]{4})\)$/, " ($1-$2)");} +function getUniqueGamepadNames() {let gamepads = window.navigator.getGamepads(), names = [];for (let gamepad of gamepads)if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id);return names;} +function hasGamepad() {let gamepads = window.navigator.getGamepads();for (let gamepad of gamepads)if (gamepad?.connected) return !0;return !1;} +function generateVirtualControllerMapping(index, override = {}) {return Object.assign({}, {GamepadIndex: index,A: 0,B: 0,X: 0,Y: 0,LeftShoulder: 0,RightShoulder: 0,LeftTrigger: 0,RightTrigger: 0,View: 0,Menu: 0,LeftThumb: 0,RightThumb: 0,DPadUp: 0,DPadDown: 0,DPadLeft: 0,DPadRight: 0,Nexus: 0,LeftThumbXAxis: 0,LeftThumbYAxis: 0,RightThumbXAxis: 0,RightThumbYAxis: 0,PhysicalPhysicality: 0,VirtualPhysicality: 0,Dirty: !1,Virtual: !1}, override);} +function getGamepadPrompt(gamepadKey) {return GamepadKeyName[gamepadKey][1];} +var XCLOUD_GAMEPAD_KEY_MAPPING = {0: "A",1: "B",2: "X",3: "Y",12: "DPadUp",15: "DPadRight",13: "DPadDown",14: "DPadLeft",4: "LeftShoulder",5: "RightShoulder",6: "LeftTrigger",7: "RightTrigger",10: "LeftThumb",11: "RightThumb",104: "LeftStickAxes",204: "RightStickAxes",8: "View",9: "Menu",16: "Nexus",17: "Share",102: "LeftThumbXAxis",103: "LeftThumbXAxis",100: "LeftThumbYAxis",101: "LeftThumbYAxis",202: "RightThumbXAxis",203: "RightThumbXAxis",200: "RightThumbYAxis",201: "RightThumbYAxis"}; +function toXcloudGamepadKey(gamepadKey) {return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];} +var PointerToMouseButton = {1: 0,2: 2,4: 1}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller"; +class WebSocketMouseDataProvider extends MouseDataProvider {pointerClient;isConnected = !1;init() {this.pointerClient = PointerClient.getInstance(), this.isConnected = !1;try {this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0;} catch (e) {Toast.show("Cannot enable Mouse & Keyboard feature");}}start() {this.isConnected && AppInterface.requestPointerCapture();}stop() {this.isConnected && AppInterface.releasePointerCapture();}destroy() {this.isConnected && this.pointerClient?.stop();}} +class PointerLockMouseDataProvider extends MouseDataProvider {start() {window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu);}stop() {document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu);}onMouseMoveEvent = (e) => {this.mkbHandler.handleMouseMove({movementX: e.movementX,movementY: e.movementY});};onMouseEvent = (e) => {e.preventDefault();let data = {mouseButton: e.button,pressed: e.type === "mousedown"};this.mkbHandler.handleMouseClick(data);};onWheelEvent = (e) => {if (!KeyHelper.getKeyFromEvent(e)) return;let data = {vertical: e.deltaY,horizontal: e.deltaX};if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault();};disableContextMenu = (e) => e.preventDefault();} +class EmulatedMkbHandler extends MkbHandler {static instance;static getInstance() {if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler;else EmulatedMkbHandler.instance = null;return EmulatedMkbHandler.instance;}static LOG_TAG = "EmulatedMkbHandler";static isAllowed() {return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile());}PRESET;VIRTUAL_GAMEPAD = {id: VIRTUAL_GAMEPAD_ID,index: 0,connected: !1,hapticActuators: null,mapping: "standard",axes: [0, 0, 0, 0],buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),timestamp: performance.now(),vibrationActuator: null};nativeGetGamepads;xCloudGamepad = generateVirtualControllerMapping(0);initialized = !1;enabled = !1;mouseDataProvider;isPolling = !1;prevWheelCode = null;wheelStoppedTimeoutId = null;detectMouseStoppedTimeoutId = null;escKeyDownTime = -1;LEFT_STICK_X = [];LEFT_STICK_Y = [];RIGHT_STICK_X = [];RIGHT_STICK_Y = [];popup;STICK_MAP = {102: [this.LEFT_STICK_X, -1],103: [this.LEFT_STICK_X, 1],100: [this.LEFT_STICK_Y, 1],101: [this.LEFT_STICK_Y, -1],202: [this.RIGHT_STICK_X, -1],203: [this.RIGHT_STICK_X, 1],200: [this.RIGHT_STICK_Y, 1],201: [this.RIGHT_STICK_Y, -1]};constructor() {super();BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);}isEnabled = () => this.enabled;patchedGetGamepads = () => {let gamepads = this.nativeGetGamepads() || [];return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads;};getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;updateStick(stick, x, y) {let gamepad = this.xCloudGamepad;if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y;else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y;window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);}vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);resetXcloudGamepads() {let index = getStreamPref("mkb.p1.slot") - 1;this.xCloudGamepad = generateVirtualControllerMapping(0, {GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0,Dirty: !0}), this.VIRTUAL_GAMEPAD.index = index;}pressButton(buttonIndex, pressed) {let xCloudKey = toXcloudGamepadKey(buttonIndex);if (buttonIndex >= 100) {let [valueArr] = this.STICK_MAP[buttonIndex];for (let i = valueArr.length - 1;i >= 0; i--)if (valueArr[i] === buttonIndex) valueArr.splice(i, 1);pressed && valueArr.push(buttonIndex);let value;if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1];else value = 0;this.xCloudGamepad[xCloudKey] = value;} else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);}onKeyboardEvent = (e) => {let isKeyDown = e.type === "keydown";if (e.code === "Escape") {if (e.preventDefault(), this.enabled && isKeyDown) {if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now();else if (performance.now() - this.escKeyDownTime >= 1000) this.stop();} else this.escKeyDownTime = -1;return;}if (!this.isPolling || !this.PRESET) return;if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;let buttonIndex = this.PRESET.mapping[e.code || e.key];if (typeof buttonIndex === "undefined") return;if (e.repeat) return;e.preventDefault(), this.pressButton(buttonIndex, isKeyDown);};onMouseStopped = () => {if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return;let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1;this.updateStick(analog, 0, 0);};handleMouseClick(data) {let mouseButton;if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton;else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton];let key = {code: "Mouse" + mouseButton};if (!this.PRESET) return;let buttonIndex = this.PRESET.mapping[key.code];if (typeof buttonIndex === "undefined") return;this.pressButton(buttonIndex, data.pressed);}handleMouseMove(data) {let preset = this.PRESET;if (!preset) return;let mouseMapTo = preset.mouse["mapTo"];if (mouseMapTo === 0) return;this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y);if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length;let analog = mouseMapTo === 1 ? 0 : 1;this.updateStick(analog, x, y);}handleMouseWheel(data) {let code = "";if (data.vertical < 0) code = "ScrollUp";else if (data.vertical > 0) code = "ScrollDown";else if (data.horizontal < 0) code = "ScrollLeft";else if (data.horizontal > 0) code = "ScrollRight";if (!code) return !1;if (!this.PRESET) return !1;let key = {code}, buttonIndex = this.PRESET.mapping[key.code];if (typeof buttonIndex === "undefined") return !1;if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0);return this.wheelStoppedTimeoutId = window.setTimeout(() => {this.prevWheelCode = null, this.pressButton(buttonIndex, !1);}, 20), !0;}async toggle(force) {if (!this.initialized) return;if (typeof force !== "undefined") this.enabled = force;else this.enabled = !this.enabled;if (this.enabled) try {await document.body.requestPointerLock({ unadjustedMovement: !0 });} catch (e) {document.body.requestPointerLock(), console.log(e);}else document.pointerLockElement && document.exitPointerLock();}refreshPresetData() {this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads();}waitForMouseData(showPopup) {this.popup.toggleVisibility(showPopup);}onPollingModeChanged = (e) => {let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";this.popup.moveOffscreen(move);};onDialogShown = () => {document.pointerLockElement && document.exitPointerLock();};onPointerLockChange = () => {if (document.pointerLockElement) this.start();else this.stop();};onPointerLockError = (e) => {console.log(e), this.stop();};onPointerLockRequested = () => {this.start();};onPointerLockExited = () => {this.mouseDataProvider?.stop();};handleEvent(event) {switch (event.type) {case BxEvent.POINTER_LOCK_REQUESTED:this.onPointerLockRequested();break;case BxEvent.POINTER_LOCK_EXITED:this.onPointerLockExited();break;}}init() {if (!STATES.browser.capabilities.mkb) {this.initialized = !1;return;}if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this);else this.mouseDataProvider = new PointerLockMouseDataProvider(this);if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError);if (MkbPopup.getInstance().reset(), AppInterface) {let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");if (shortcutKey) {let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });Toast.show(msg, t("native-mkb"), { html: !0 });}this.waitForMouseData(!1);} else this.waitForMouseData(!0);}destroy() {if (!this.initialized) return;if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError);window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);}start() {if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start();let virtualGamepad = this.getVirtualGamepad();virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {gamepad: virtualGamepad}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });}stop() {this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1;let virtualGamepad = this.getVirtualGamepad();if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {gamepad: virtualGamepad}), window.navigator.getGamepads = this.nativeGetGamepads;this.waitForMouseData(!0), this.mouseDataProvider?.stop();}static setupEvents() {if (BxEventBus.Stream.on("state.playing", () => {if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init();else EmulatedMkbHandler.getInstance()?.init();}), EmulatedMkbHandler.isAllowed())BxEventBus.Stream.on("mkb.setting.updated", () => {EmulatedMkbHandler.getInstance()?.refreshPresetData();});}} +class StreamSettings {static settings = {settings: {},xCloudPollingMode: "all",deviceVibrationIntensity: 0,controllerPollingRate: 4,controllers: {},mkbPreset: null,keyboardShortcuts: {}};static async refreshControllerSettings() {let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads();for (let gamepad of gamepads) {if (!gamepad?.connected) continue;if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data);controllers[gamepad.id] = {shortcuts: shortcutsMapping,customization: customizationData};}settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration();}static preCalculateControllerRange(obj, target, values) {if (values && Array.isArray(values)) {let [from, to] = values;if (from > 1 || to < 100) obj[target] = [from / 100, to / 100];}}static convertControllerCustomization(customization) {if (!customization) return null;let converted = {mapping: {},ranges: {},vibrationIntensity: 1}, gamepadKey;for (gamepadKey in customization.mapping) {let gamepadStr = toXcloudGamepadKey(gamepadKey);if (!gamepadStr) continue;let mappedKey = customization.mapping[gamepadKey];if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey);else converted.mapping[gamepadStr] = !1;}return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted;}static async refreshDeviceVibration() {if (!STATES.browser.capabilities.deviceVibration) return;let mode = getStreamPref("deviceVibration.mode"), intensity = 0;if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100;StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {});}static async refreshMkbSettings() {let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = {mapping: {},mouse: Object.assign({}, orgPresetData.mouse)}, key;for (key in orgPresetData.mapping) {let buttonIndex = parseInt(key);if (!orgPresetData.mapping[buttonIndex]) continue;for (let keyName of orgPresetData.mapping[buttonIndex])if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex;}let mouse = converted.mouse;mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {});}static async refreshKeyboardShortcuts() {let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId");if (presetId === 0) {settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});return;}let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action;for (action in orgPresetData) {let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`;converted[key] = action;}settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});}static async refreshAllSettings() {window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts();}static findKeyboardShortcut(targetAction) {let shortcuts = StreamSettings.settings.keyboardShortcuts;for (let codeStr in shortcuts)if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr);return null;}static setup() {let listener = () => {StreamSettings.refreshControllerSettings();};window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings();}} +class BxNumberStepper extends HTMLInputElement {intervalId = null;isHolding;controlValue;controlMin;controlMax;uiMin;uiMax;steps;options;onChange;$text;$btnInc;$btnDec;$range;onRangeInput;onClick;onPointerUp;onPointerDown;setValue;normalizeValue;static create(key, value, min, max, options = {}, onChange) {options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;let $text, $btnInc, $btnDec, $range, self = CE("div", {class: "bx-number-stepper",id: `bx_setting_${escapeCssSelector(key)}`}, CE("div", !1, $btnDec = CE("button", {_dataset: {type: "dec"},type: "button",class: options.hideSlider ? "bx-focusable" : "",tabindex: options.hideSlider ? 0 : -1}, "-"), $text = CE("span"), $btnInc = CE("button", {_dataset: {type: "inc"},type: "button",class: options.hideSlider ? "bx-focusable" : "",tabindex: options.hideSlider ? 0 : -1}, "+")));if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self;if ($range = CE("input", {id: `bx_inp_setting_${key}`,type: "range",min: self.uiMin,max: self.uiMax,value: options.reverse ? -value : value,step: self.steps,tabindex: 0}), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) {let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });if ($range.setAttribute("list", markersId), options.exactTicks) {let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks;if (start === min) start += options.exactTicks;for (let i = start;i < max; i += options.exactTicks)$markers.appendChild(CE("option", {value: options.reverse ? -i : i}));} else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks)$markers.appendChild(CE("option", { value: i }));self.appendChild($markers);}return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, {focus: options.hideSlider ? $btnInc : $range}), Object.defineProperty(self, "value", {get() {return self.controlValue;},set(value2) {BxNumberStepper.setValue.call(self, value2);}}), Object.defineProperty(self, "disabled", {get() {return $range.disabled;},set(value2) {$btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2;}}), self;}static setValue(value) {if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString();BxNumberStepper.updateButtonsVisibility.call(this);}static normalizeValue(value) {return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value;}static onRangeInput(e) {let value = parseInt(e.target.value);if (this.options.reverse) value *= -1;if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value);}static onClick(e) {if (e.preventDefault(), this.isHolding) return;let $btn = e.target.closest("button");$btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;}static onPointerDown(e) {BxNumberStepper.clearIntervalId.call(this);let $btn = e.target.closest("button");if (!$btn) return;this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => {BxNumberStepper.buttonPressed.call(this, e2, $btn);}, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 });}static onPointerUp(e) {BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;}static onContextMenu(e) {e.preventDefault();}static updateTextValue() {let value = this.controlValue, textContent = null;if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);if (textContent === null) textContent = value.toString() + this.options.suffix;return textContent;}static buttonPressed(e, $btn) {BxNumberStepper.change.call(this, $btn.dataset.type);}static change(direction) {let value = this.controlValue;if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps);else value = Math.min(this.uiMax, value + this.steps);value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue);}static clearIntervalId() {this.intervalId && clearInterval(this.intervalId), this.intervalId = null;}static updateButtonsVisibility() {if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this);}} +class SettingElement {static renderOptions(key, setting, currentValue, onChange) {let $control = CE("select", {tabindex: 0}), $parent;if (setting.optionsGroup) $parent = CE("optgroup", {label: setting.optionsGroup}), $control.appendChild($parent);else $parent = $control;for (let value in setting.options) {let label = setting.options[value], $option = CE("option", { value }, label);$parent.appendChild($option);}return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;!e.ignoreOnChange && onChange(e, value);}), $control.setValue = (value) => {$control.value = value;}, $control;}static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {let $control = CE("select", {multiple: !0,tabindex: 0}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;$control.setAttribute("size", size.toString());for (let value in setting.multipleOptions) {let label = setting.multipleOptions[value], $option = CE("option", { value }, label);$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {e.preventDefault();let target = e.target;target.selected = !target.selected;let $parent = target.parentElement;$parent.focus(), BxEvent.dispatch($parent, "input");}), $control.appendChild($option);}return $control.addEventListener("mousedown", function(e) {let self = this, orgScrollTop = self.scrollTop;window.setTimeout(() => self.scrollTop = orgScrollTop, 0);}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);!e.ignoreOnChange && onChange(e, values);}), Object.defineProperty($control, "value", {get() {return Array.from($control.options).filter((option) => option.selected).map((option) => option.value);},set(value) {let values = value.split(",");Array.from($control.options).forEach((option) => {option.selected = values.includes(option.value);});}}), $control;}static renderCheckbox(key, setting, currentValue, onChange) {let $control = CE("input", { type: "checkbox", tabindex: 0 });return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {!e.ignoreOnChange && onChange(e, e.target.checked);}), $control.setValue = (value) => {$control.checked = !!value;}, $control;}static renderNumberStepper(key, setting, value, onChange, options = {}) {return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);}static METHOD_MAP = {options: SettingElement.renderOptions,"multiple-options": SettingElement.renderMultipleOptions,"number-stepper": SettingElement.renderNumberStepper,checkbox: SettingElement.renderCheckbox};static render(type, key, setting, currentValue, onChange, options) {let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;if (type === "options" || type === "multiple-options") $control.name = $control.id;return $control;}static fromPref(key, onChange, overrideParams = {}) {let { definition, storage } = getPrefInfo(key);if (!definition) return null;let currentValue = storage.getSetting(key), type;if ("options" in definition) type = "options";else if ("multipleOptions" in definition) type = "multiple-options";else if (typeof definition.default === "number") type = "number-stepper";else type = "checkbox";let params = {};if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});if (params.disabled) currentValue = definition.default;return SettingElement.render(type, key, definition, currentValue, (e, value) => {if (isGlobalPref(key)) setGlobalPref(key, value, "ui");else {let id = SettingsManager.getInstance().getTargetGameId();setGamePref(id, key, value, "ui");}onChange && onChange(e, value);}, params);}} +class BxSelectElement extends HTMLSelectElement {isControllerFriendly;optionsList;indicatorsList;$indicators;visibleIndex;isMultiple;$select;$btnNext;$btnPrev;$label;$checkBox;static create($select, forceFriendly = !1) {let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly");if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;$select.removeAttribute("tabindex");let $wrapper = CE("div", {class: "bx-select",_dataset: {controllerFriendly: isControllerFriendly}});if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");let $content, self = $wrapper;self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];let $btnPrev, $btnNext;if (isControllerFriendly) {$btnPrev = createButton({label: "<",style: 64}), $btnNext = createButton({label: ">",style: 64}), setNearby($wrapper, {orientation: "horizontal",focus: $btnNext}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);} else $select.addEventListener("change", (e) => {self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);});if (self.isMultiple) $content = CE("button", {class: "bx-select-value bx-focusable",tabindex: 0}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {self.$checkBox.click();}), self.$checkBox.addEventListener("input", (e) => {let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");});else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {mutationList.forEach((mutation) => {if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);});}).observe($select, {subtree: !0,childList: !0,attributes: !0}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {get() {return $select.value;},set(value) {self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);}}), Object.defineProperty(self, "disabled", {get() {return $select.disabled;},set(value) {$select.disabled = value;}}), self.addEventListener = function() {$select.addEventListener.apply($select, arguments);}, self.removeEventListener = function() {$select.removeEventListener.apply($select, arguments);}, self.dispatchEvent = function() {return $select.dispatchEvent.apply($select, arguments);}, self.appendChild = function(node) {return $select.appendChild(node), node;}, self;}static resetIndicators() {let {optionsList,indicatorsList,$indicators} = this, targetSize = optionsList.length;if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)indicatorsList.pop()?.remove();else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {let $indicator = CE("span", {});indicatorsList.push($indicator), $indicators.appendChild($indicator);}for (let $indicator of indicatorsList)clearDataSet($indicator);$indicators.classList.toggle("bx-invisible", targetSize <= 1);}static getOptionAtIndex(index) {return this.optionsList[index];}static render(e) {let {$label,$btnNext,$btnPrev,$checkBox,optionsList,indicatorsList} = this;if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";if ($option) {let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";$label.innerHTML = "";let fragment = document.createDocumentFragment();fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);} else $label.textContent = content;} else $label.textContent = content;if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);let disableButtons = optionsList.length <= 1;$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons);for (let i = 0;i < optionsList.length; i++) {let $option2 = optionsList[i], $indicator = indicatorsList[i];if (!$option2 || !$indicator) continue;if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";}}static normalizeIndex(index) {return Math.min(Math.max(index, 0), this.optionsList.length - 1);}static onPrevNext(e) {if (!e.target) return;let {$btnNext,$select,isMultiple,visibleIndex: currentIndex} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;if (newIndex > this.optionsList.length - 1) newIndex = 0;else if (newIndex < 0) newIndex = this.optionsList.length - 1;if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;if (isMultiple) BxSelectElement.render.call(this);else BxEvent.dispatch($select, "input");}} +class XboxApi {static CACHED_TITLES = {};static async getProductTitle(xboxTitleId) {if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];try {let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle;} catch (e) {}return;}} +class SettingsManager {static instance;static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);$streamSettingsSelection;$tips;playingGameId = -1;targetGameId = -1;SETTINGS = {"localCoOp.enabled": {onChange: () => {BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));}},"deviceVibration.mode": {onChange: StreamSettings.refreshControllerSettings},"deviceVibration.intensity": {onChange: StreamSettings.refreshControllerSettings},"controller.pollingRate": {onChange: StreamSettings.refreshControllerSettings},"controller.settings": {onChange: StreamSettings.refreshControllerSettings},"nativeMkb.scroll.sensitivityX": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityX");NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);}},"nativeMkb.scroll.sensitivityY": {onChange: () => {let value = getStreamPref("nativeMkb.scroll.sensitivityY");NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);}},"video.player.type": {onChange: () => {if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer();},alwaysTriggerOnChange: !0},"video.player.powerPreference": {onChange: () => {let streamPlayer = STATES.currentStream.streamPlayer;if (!streamPlayer) return;streamPlayer.reloadPlayer(), updateVideoPlayer();}},"video.processing": {onChange: updateVideoPlayer},"video.processing.sharpness": {onChange: updateVideoPlayer},"video.maxFps": {onChange: () => {let value = getStreamPref("video.maxFps");limitVideoPlayerFps(value);}},"video.ratio": {onChange: updateVideoPlayer},"video.brightness": {onChange: updateVideoPlayer},"video.contrast": {onChange: updateVideoPlayer},"video.saturation": {onChange: updateVideoPlayer},"video.position": {onChange: updateVideoPlayer},"audio.volume": {onChange: () => {let value = getStreamPref("audio.volume");SoundShortcut.setGainNodeVolume(value);}},"stats.items": {onChange: StreamStats.refreshStyles},"stats.quickGlance.enabled": {onChange: () => {let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();}},"stats.position": {onChange: StreamStats.refreshStyles},"stats.textSize": {onChange: StreamStats.refreshStyles},"stats.opacity.all": {onChange: StreamStats.refreshStyles},"stats.opacity.background": {onChange: StreamStats.refreshStyles},"stats.colors": {onChange: StreamStats.refreshStyles},"mkb.p1.preset.mappingId": {onChange: StreamSettings.refreshMkbSettings},"mkb.p1.slot": {onChange: () => {EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();}},"keyboardShortcuts.preset.inGameId": {onChange: StreamSettings.refreshKeyboardShortcuts}};constructor() {BxEventBus.Stream.on("setting.changed", (data) => {if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.switchGameSettings(id);}), this.renderStreamSettingsSelection();}updateStreamElement(key, onChanges) {let info = this.SETTINGS[key];if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange);else info.onChange();let $elm = info.$element;if (!$elm) return;let value = getGamePref(this.targetGameId, key, !0);if ("setValue" in $elm) $elm.setValue(value);else $elm.value = value.toString();this.updateDataset($elm, key);}switchGameSettings(id) {if (setGameIdPref(id), this.targetGameId === id) return;let onChanges = new Set, oldGameId = this.targetGameId;this.targetGameId = id;let key;for (key in this.SETTINGS) {if (!isStreamPref(key)) continue;let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0);if (oldValue === newValue) continue;this.updateStreamElement(key, onChanges);}onChanges.forEach((onChange) => {onChange && onChange();}), this.$tips.classList.toggle("bx-gone", id < 0);}setElement(pref, $elm) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;}getElement(pref, params) {if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};let $elm = this.SETTINGS[pref].$element;if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;return this.updateDataset($elm, pref), $elm;}hasElement(pref) {return !!this.SETTINGS[pref]?.$element;}updateDataset($elm, pref) {if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";else delete $elm.dataset.override;}renderStreamSettingsSelection() {this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`);let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0);$select.addEventListener("input", (e) => {let id = parseInt($select.value);BxEventBus.Stream.emit("gameSettings.switched", { id });}), this.$streamSettingsSelection = CE("div", {class: "bx-stream-settings-selection bx-gone",_nearby: { orientation: "vertical" }}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {this.playingGameId = id, setGameIdPref(id);let $optGroup = $select.querySelector("optgroup");while ($optGroup.childElementCount > 1)$optGroup.lastElementChild?.remove();if (id >= 0) {let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id);$optGroup.appendChild(CE("option", {value: id}, title)), $select.value = id.toString();} else $select.value = "-1";BxEventBus.Stream.emit("gameSettings.switched", { id });});}getStreamSettingsSelection() {return this.$streamSettingsSelection;}getTargetGameId() {return this.targetGameId;}} +function onChangeVideoPlayerType() {let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();if (!settingsManager.hasElement("video.processing")) return;let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2");} +function limitVideoPlayerFps(targetFps) {STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);} +function updateVideoPlayer() {let streamPlayer = STATES.currentStream.streamPlayer;if (!streamPlayer) return;limitVideoPlayerFps(getStreamPref("video.maxFps"));let options = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")};streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();} window.addEventListener("resize", updateVideoPlayer); -class NavigationDialog { -dialogManager; -onMountedCallbacks = []; -constructor() { -this.dialogManager = NavigationDialogManager.getInstance(); -} -isCancellable() { -return !0; -} -isOverlayVisible() { -return !0; -} -show(configs = {}, clearStack = !1) { -if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded(); -} -hide() { -NavigationDialogManager.getInstance().hide(); -} -getFocusedElement() { -let $activeElement = document.activeElement; -if (!$activeElement) return null; -if (this.$container.contains($activeElement)) return $activeElement; -return null; -} -onBeforeMount(configs = {}) {} -onMounted(configs = {}) { -for (let callback of this.onMountedCallbacks) -callback.call(this); -} -onBeforeUnmount() {} -onUnmounted() {} -handleKeyPress(key) { -return !1; -} -handleGamepad(button) { -return !1; -} -} -class NavigationDialogManager { -static instance; -static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager); -LOG_TAG = "NavigationDialogManager"; -static GAMEPAD_POLLING_INTERVAL = 50; -static GAMEPAD_KEYS = [ -0, -1, -2, -3, -12, -15, -13, -14, -4, -5, -6, -7, -10, -11, -8, -9 -]; -static GAMEPAD_DIRECTION_MAP = { -12: 1, -13: 3, -14: 4, -15: 2, -100: 1, -101: 3, -102: 4, -103: 2 -}; -static SIBLING_PROPERTY_MAP = { -horizontal: { -4: "previousElementSibling", -2: "nextElementSibling" -}, -vertical: { -1: "previousElementSibling", -3: "nextElementSibling" -} -}; -gamepadPollingIntervalId = null; -gamepadLastStates = []; -gamepadHoldingIntervalId = null; -$overlay; -$container; -dialog = null; -dialogsStack = []; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { -e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide(); -}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => { -if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; -let $dialog = mutationList[0].addedNodes[0]; -if (!$dialog || !($dialog instanceof HTMLElement)) return; -calculateSelectBoxes($dialog); -}).observe(this.$container, { childList: !0 }); -} -updateActiveInput(input) { -document.documentElement.dataset.activeInput = input; -} -handleEvent(event) { -switch (event.type) { -case "keydown": -this.updateActiveInput("keyboard"); -let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode); -if (handled) { -event.preventDefault(), event.stopPropagation(); -return; -} -if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3); -else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") { -if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2); -} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") { -if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); -} else if (keyCode === "Escape") handled = !0, this.hide(); -if (handled) event.preventDefault(), event.stopPropagation(); -break; -} -} -isShowing() { -return this.$container && !this.$container.classList.contains("bx-gone"); -} -pollGamepad = () => { -let gamepads = window.navigator.getGamepads(); -for (let gamepad of gamepads) { -if (!gamepad || !gamepad.connected) continue; -if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; -let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed; -if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState; -if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue; -for (let key of NavigationDialogManager.GAMEPAD_KEYS) -if (lastKey === key && !buttons[key].pressed) { -releasedButton = key; -break; -} else if (buttons[key].pressed) { -heldButton = key; -break; -} -if (heldButton === null && releasedButton === null && axes && axes.length >= 2) { -if (lastKey) { -let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101); -if (releasedHorizontal || releasedVertical) releasedButton = lastKey; -else heldButton = lastKey; -} else if (axes[0] < -0.5) heldButton = 102; -else if (axes[0] > 0.5) heldButton = 103; -else if (axes[1] < -0.5) heldButton = 100; -else if (axes[1] > 0.5) heldButton = 101; -} -if (heldButton !== null) { -if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => { -let lastState2 = this.gamepadLastStates[gamepad.index]; -if (lastState2) { -if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { -this.handleGamepad(gamepad, heldButton); -return; -} -} -this.clearGamepadHoldingInterval(); -}, 100); -continue; -} -if (releasedButton === null) { -this.clearGamepadHoldingInterval(); -continue; -} -if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; -if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return; -if (releasedButton === 0) { -document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 })); -return; -} else if (releasedButton === 1) { -this.hide(); -return; -} -} -}; -handleGamepad(gamepad, key) { -let handled = this.dialog?.handleGamepad(key); -if (handled) return !0; -let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key]; -if (!direction) return !1; -if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") { -let $range = document.activeElement; -if (direction === 4 || direction === 2) { -let $numberStepper = $range.closest(".bx-number-stepper"); -if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc"); -else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")); -handled = !0; -} -} -if (!handled) this.focusDirection(direction); -return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0; -} -clearGamepadHoldingInterval() { -this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null; -} -show(dialog, configs = {}, clearStack = !1) { -this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); -} -hide() { -if (this.clearGamepadHoldingInterval(), !this.isShowing()) return; -if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) { -let dialogIndex = this.dialogsStack.indexOf(this.dialog); -if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex); -} -if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show(); -} -focus($elm) { -if (!$elm) return !1; -if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus); -else return $elm.nearby.focus(); -return $elm.focus(), $elm === document.activeElement; -} -getOrientation($elm) { -let nearby = $elm.nearby || {}; -if (nearby.selfOrientation) return nearby.selfOrientation; -let orientation, $current = $elm.parentElement; -while ($current !== this.$container) { -let tmp = $current.nearby?.orientation; -if ($current.nearby && tmp) { -orientation = tmp; -break; -} -$current = $current.parentElement; -} -return orientation = orientation || "vertical", setNearby($elm, { -selfOrientation: orientation -}), orientation; -} -findNextTarget($focusing, direction, checkParent = !1, checked = []) { -if (!$focusing || $focusing === this.$container) return null; -if (checked.includes($focusing)) return null; -checked.push($focusing); -let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); -if (nearby[1] && direction === 1) return nearby[1]; -else if (nearby[3] && direction === 3) return nearby[3]; -else if (nearby[4] && direction === 4) return nearby[4]; -else if (nearby[2] && direction === 2) return nearby[2]; -let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; -if (siblingProperty) { -let $sibling = $target; -while ($sibling[siblingProperty]) { -$sibling = $sibling[siblingProperty]; -let $focusable = this.findFocusableElement($sibling, direction); -if ($focusable) return $focusable; -} -} -if (nearby.loop) { -if (nearby.loop(direction)) return null; -} -if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked); -return null; -} -findFocusableElement($elm, direction) { -if (!$elm) return null; -if (!!$elm.disabled) return null; -if (!isElementVisible($elm)) return null; -if ($elm.tabIndex > -1) return $elm; -let focus = $elm.nearby?.focus; -if (focus) { -if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction); -else if (typeof focus === "function") { -if (focus()) return document.activeElement; -} -} -let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical"; -if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse(); -for (let $child of children) { -if (!$child || !($child instanceof HTMLElement)) return null; -let $target = this.findFocusableElement($child, direction); -if ($target) return $target; -} -return null; -} -startGamepadPolling() { -this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); -} -stopGamepadPolling() { -this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; -} -focusDirection(direction) { -let dialog = this.dialog; -if (!dialog) return; -let $focusing = dialog.getFocusedElement(); -if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; -let $target = this.findNextTarget($focusing, direction, !0); -this.focus($target); -} -unmountCurrentDialog() { -let dialog = this.dialog; -dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; -} -} +class NavigationDialog {dialogManager;onMountedCallbacks = [];constructor() {this.dialogManager = NavigationDialogManager.getInstance();}isCancellable() {return !0;}isOverlayVisible() {return !0;}show(configs = {}, clearStack = !1) {if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();}hide() {NavigationDialogManager.getInstance().hide();}getFocusedElement() {let $activeElement = document.activeElement;if (!$activeElement) return null;if (this.$container.contains($activeElement)) return $activeElement;return null;}onBeforeMount(configs = {}) {}onMounted(configs = {}) {for (let callback of this.onMountedCallbacks)callback.call(this);}onBeforeUnmount() {}onUnmounted() {}handleKeyPress(key) {return !1;}handleGamepad(button) {return !1;}} +class NavigationDialogManager {static instance;static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);LOG_TAG = "NavigationDialogManager";static GAMEPAD_POLLING_INTERVAL = 50;static GAMEPAD_KEYS = [0,1,2,3,12,15,13,14,4,5,6,7,10,11,8,9];static GAMEPAD_DIRECTION_MAP = {12: 1,13: 3,14: 4,15: 2,100: 1,101: 3,102: 4,103: 2};static SIBLING_PROPERTY_MAP = {horizontal: {4: "previousElementSibling",2: "nextElementSibling"},vertical: {1: "previousElementSibling",3: "nextElementSibling"}};gamepadPollingIntervalId = null;gamepadLastStates = [];gamepadHoldingIntervalId = null;$overlay;$container;dialog = null;dialogsStack = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;let $dialog = mutationList[0].addedNodes[0];if (!$dialog || !($dialog instanceof HTMLElement)) return;calculateSelectBoxes($dialog);}).observe(this.$container, { childList: !0 });}updateActiveInput(input) {document.documentElement.dataset.activeInput = input;}handleEvent(event) {switch (event.type) {case "keydown":this.updateActiveInput("keyboard");let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);if (handled) {event.preventDefault(), event.stopPropagation();return;}if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));} else if (keyCode === "Escape") handled = !0, this.hide();if (handled) event.preventDefault(), event.stopPropagation();break;}}isShowing() {return this.$container && !this.$container.classList.contains("bx-gone");}pollGamepad = () => {let gamepads = window.navigator.getGamepads();for (let gamepad of gamepads) {if (!gamepad || !gamepad.connected) continue;if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;for (let key of NavigationDialogManager.GAMEPAD_KEYS)if (lastKey === key && !buttons[key].pressed) {releasedButton = key;break;} else if (buttons[key].pressed) {heldButton = key;break;}if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {if (lastKey) {let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);if (releasedHorizontal || releasedVertical) releasedButton = lastKey;else heldButton = lastKey;} else if (axes[0] < -0.5) heldButton = 102;else if (axes[0] > 0.5) heldButton = 103;else if (axes[1] < -0.5) heldButton = 100;else if (axes[1] > 0.5) heldButton = 101;}if (heldButton !== null) {if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {let lastState2 = this.gamepadLastStates[gamepad.index];if (lastState2) {if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {this.handleGamepad(gamepad, heldButton);return;}}this.clearGamepadHoldingInterval();}, 100);continue;}if (releasedButton === null) {this.clearGamepadHoldingInterval();continue;}if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;if (releasedButton === 0) {document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));return;} else if (releasedButton === 1) {this.hide();return;}}};handleGamepad(gamepad, key) {let handled = this.dialog?.handleGamepad(key);if (handled) return !0;let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];if (!direction) return !1;if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {let $range = document.activeElement;if (direction === 4 || direction === 2) {let $numberStepper = $range.closest(".bx-number-stepper");if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));handled = !0;}}if (!handled) this.focusDirection(direction);return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;}clearGamepadHoldingInterval() {this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;}show(dialog, configs = {}, clearStack = !1) {this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();}hide() {if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {let dialogIndex = this.dialogsStack.indexOf(this.dialog);if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);}if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();}focus($elm) {if (!$elm) return !1;if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);else return $elm.nearby.focus();return $elm.focus(), $elm === document.activeElement;}getOrientation($elm) {let nearby = $elm.nearby || {};if (nearby.selfOrientation) return nearby.selfOrientation;let orientation, $current = $elm.parentElement;while ($current !== this.$container) {let tmp = $current.nearby?.orientation;if ($current.nearby && tmp) {orientation = tmp;break;}$current = $current.parentElement;}return orientation = orientation || "vertical", setNearby($elm, {selfOrientation: orientation}), orientation;}findNextTarget($focusing, direction, checkParent = !1, checked = []) {if (!$focusing || $focusing === this.$container) return null;if (checked.includes($focusing)) return null;checked.push($focusing);let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);if (nearby[1] && direction === 1) return nearby[1];else if (nearby[3] && direction === 3) return nearby[3];else if (nearby[4] && direction === 4) return nearby[4];else if (nearby[2] && direction === 2) return nearby[2];let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];if (siblingProperty) {let $sibling = $target;while ($sibling[siblingProperty]) {$sibling = $sibling[siblingProperty];let $focusable = this.findFocusableElement($sibling, direction);if ($focusable) return $focusable;}}if (nearby.loop) {if (nearby.loop(direction)) return null;}if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);return null;}findFocusableElement($elm, direction) {if (!$elm) return null;if (!!$elm.disabled) return null;if (!isElementVisible($elm)) return null;if ($elm.tabIndex > -1) return $elm;let focus = $elm.nearby?.focus;if (focus) {if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);else if (typeof focus === "function") {if (focus()) return document.activeElement;}}let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();for (let $child of children) {if (!$child || !($child instanceof HTMLElement)) return null;let $target = this.findFocusableElement($child, direction);if ($target) return $target;}return null;}startGamepadPolling() {this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);}stopGamepadPolling() {this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;}focusDirection(direction) {let dialog = this.dialog;if (!dialog) return;let $focusing = dialog.getFocusedElement();if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;let $target = this.findNextTarget($focusing, direction, !0);this.focus($target);}unmountCurrentDialog() {let dialog = this.dialog;dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;}} var LOG_TAG = "TouchController"; -class TouchController { -static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", { -data: JSON.stringify({ -content: '{"layoutId":""}', -target: "/streaming/touchcontrols/showlayoutv2", -type: "Message" -}), -origin: "better-xcloud" -}); -static #$style; -static #enabled = !1; -static #dataChannel; -static #customLayouts = {}; -static #baseCustomLayouts = {}; -static #currentLayoutId; -static #customList; -static #xboxTitleId = null; -static setXboxTitleId(xboxTitleId) { -TouchController.#xboxTitleId = xboxTitleId; -} -static getCustomLayouts() { -let xboxTitleId = TouchController.#xboxTitleId; -if (!xboxTitleId) return null; -return TouchController.#customLayouts[xboxTitleId]; -} -static enable() { -TouchController.#enabled = !0; -} -static disable() { -TouchController.#enabled = !1; -} -static isEnabled() { -return TouchController.#enabled; -} -static #showDefault() { -TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER); -} -static #show() { -document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen"); -} -static toggleVisibility() { -if (!TouchController.#dataChannel) return !1; -let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement; -if (!$container) return !1; -return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen"); -} -static reset() { -TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = ""); -} -static #dispatchMessage(msg) { -TouchController.#dataChannel && window.setTimeout(() => { -TouchController.#dataChannel.dispatchEvent(msg); -}, 10); -} -static #dispatchLayouts(data) { -TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); -} -static async requestCustomLayouts(retries = 1) { -let xboxTitleId = TouchController.#xboxTitleId; -if (!xboxTitleId) return; -if (xboxTitleId in TouchController.#customLayouts) { -TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); -return; -} -if (retries = retries || 1, retries > 2) { -TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); -return; -} -try { -let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {}; -json.layouts.forEach(async (layoutName) => { -let baseLayouts = {}; -if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; -else try { -let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`); -baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts; -} catch (e) {} -Object.assign(layouts, baseLayouts); -}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000); -} catch (e) { -TouchController.requestCustomLayouts(retries + 1); -} -} -static applyCustomLayout(layoutId, delay = 0) { -if (!window.BX_EXPOSED.touchLayoutManager) { -let listener = (e) => { -if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); -}; -window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 }); -return; -} -let xboxTitleId = TouchController.#xboxTitleId; -if (!xboxTitleId) { -BxLogger.error(LOG_TAG, "Invalid xboxTitleId"); -return; -} -if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; -if (!layoutId) { -BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); -return; -} -let layoutChanged = TouchController.#currentLayoutId !== layoutId; -TouchController.#currentLayoutId = layoutId; -let layoutData = TouchController.#customLayouts[xboxTitleId]; -if (!xboxTitleId || !layoutId || !layoutData) { -TouchController.#enabled && TouchController.#showDefault(); -return; -} -let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; -if (!layout) return; -let msg, html = !1; -if (layout.author) { -let author = `${escapeHtml(layout.author)}`; -msg = t("touch-control-layout-by", { name: author }), html = !0; -} else msg = t("touch-control-layout"); -layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => { -window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({ -type: "showLayout", -scope: xboxTitleId, -subscope: "base", -layout: { -id: "System.Standard", -displayName: "System", -layoutFile: layout -} -}); -}, delay); -} -static updateCustomList() { -TouchController.#customList = GhPagesUtils.getTouchControlCustomList(); -} -static getCustomList() { -return TouchController.#customList; -} -static setup() { -window.testTouchLayout = (layout) => { -let { touchLayoutManager } = window.BX_EXPOSED; -touchLayoutManager && touchLayoutManager.changeLayoutForScope({ -type: "showLayout", -scope: "" + TouchController.#xboxTitleId, -subscope: "base", -layout: { -id: "System.Standard", -displayName: "Custom", -layoutFile: layout -} -}); -}; -let $style = document.createElement("style"); -document.documentElement.appendChild($style), TouchController.#$style = $style; -let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom"); -BxEventBus.Stream.on("dataChannelCreated", (payload) => { -let { dataChannel } = payload; -if (dataChannel?.label !== "message") return; -let filter = ""; -if (TouchController.#enabled) { -if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)"; -else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)"; -} else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)"; -if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`; -else $style.textContent = ""; -TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => { -window.setTimeout(TouchController.#show, 1000); -}); -let focused = !1; -dataChannel.addEventListener("message", (msg) => { -if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; -if (msg.data.includes("touchcontrols/showtitledefault")) { -if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts(); -else TouchController.#showDefault(); -return; -} -try { -if (msg.data.includes("/titleinfo")) { -let json = JSON.parse(JSON.parse(msg.data).content); -if (focused = json.focused, !json.focused) TouchController.#show(); -TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); -} -} catch (e) { -BxLogger.error(LOG_TAG, "Load custom layout", e); -} -}); -}); -} -} +class TouchController {static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", {data: JSON.stringify({content: '{"layoutId":""}',target: "/streaming/touchcontrols/showlayoutv2",type: "Message"}),origin: "better-xcloud"});static #$style;static #enabled = !1;static #dataChannel;static #customLayouts = {};static #baseCustomLayouts = {};static #currentLayoutId;static #customList;static #xboxTitleId = null;static setXboxTitleId(xboxTitleId) {TouchController.#xboxTitleId = xboxTitleId;}static getCustomLayouts() {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return null;return TouchController.#customLayouts[xboxTitleId];}static enable() {TouchController.#enabled = !0;}static disable() {TouchController.#enabled = !1;}static isEnabled() {return TouchController.#enabled;}static #showDefault() {TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);}static #show() {document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");}static toggleVisibility() {if (!TouchController.#dataChannel) return !1;let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement;if (!$container) return !1;return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen");}static reset() {TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = "");}static #dispatchMessage(msg) {TouchController.#dataChannel && window.setTimeout(() => {TouchController.#dataChannel.dispatchEvent(msg);}, 10);}static #dispatchLayouts(data) {TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);}static async requestCustomLayouts(retries = 1) {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return;if (xboxTitleId in TouchController.#customLayouts) {TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);return;}if (retries = retries || 1, retries > 2) {TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);return;}try {let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {};json.layouts.forEach(async (layoutName) => {let baseLayouts = {};if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName];else try {let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;} catch (e) {}Object.assign(layouts, baseLayouts);}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);} catch (e) {TouchController.requestCustomLayouts(retries + 1);}}static applyCustomLayout(layoutId, delay = 0) {if (!window.BX_EXPOSED.touchLayoutManager) {let listener = (e) => {if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0);};window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 });return;}let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) {BxLogger.error(LOG_TAG, "Invalid xboxTitleId");return;}if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;if (!layoutId) {BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault();return;}let layoutChanged = TouchController.#currentLayoutId !== layoutId;TouchController.#currentLayoutId = layoutId;let layoutData = TouchController.#customLayouts[xboxTitleId];if (!xboxTitleId || !layoutId || !layoutData) {TouchController.#enabled && TouchController.#showDefault();return;}let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];if (!layout) return;let msg, html = !1;if (layout.author) {let author = `${escapeHtml(layout.author)}`;msg = t("touch-control-layout-by", { name: author }), html = !0;} else msg = t("touch-control-layout");layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => {window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "System",layoutFile: layout}});}, delay);}static updateCustomList() {TouchController.#customList = GhPagesUtils.getTouchControlCustomList();}static getCustomList() {return TouchController.#customList;}static setup() {window.testTouchLayout = (layout) => {let { touchLayoutManager } = window.BX_EXPOSED;touchLayoutManager && touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: "" + TouchController.#xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "Custom",layoutFile: layout}});};let $style = document.createElement("style");document.documentElement.appendChild($style), TouchController.#$style = $style;let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom");BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label !== "message") return;let filter = "";if (TouchController.#enabled) {if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)";else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)";} else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)";if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;else $style.textContent = "";TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {window.setTimeout(TouchController.#show, 1000);});let focused = !1;dataChannel.addEventListener("message", (msg) => {if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;if (msg.data.includes("touchcontrols/showtitledefault")) {if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts();else TouchController.#showDefault();return;}try {if (msg.data.includes("/titleinfo")) {let json = JSON.parse(JSON.parse(msg.data).content);if (focused = json.focused, !json.focused) TouchController.#show();TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());}} catch (e) {BxLogger.error(LOG_TAG, "Load custom layout", e);}});});}} var controller_customization_default = "var shareButtonPressed=currentGamepad.buttons[17]?.pressed,shareButtonHandled=!1,xCloudGamepad=$xCloudGamepadVar$;if(currentGamepad.id in window.BX_STREAM_SETTINGS.controllers){let controller=window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];if(controller?.customization){let{mapping,ranges}=controller.customization,pressedButtons={},releasedButtons={},isModified=!1;if(ranges.LeftTrigger){let[from,to]=ranges.LeftTrigger;xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTrigger>to?1:xCloudGamepad.LeftTrigger,xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTriggerto?1:xCloudGamepad.RightTrigger,xCloudGamepad.RightTrigger=xCloudGamepad.RightTriggerto?1:range;if(newRange=newRangeto?1:range;if(newRange=newRange=0.1)pressedButtons[targetX]=rangeX,pressedButtons[targetY]=rangeY}releasedButtons[sourceX]=0,releasedButtons[sourceY]=0,isModified=!0}else if(typeof mappedKey===\"string\"){let pressed=!1,value=0;if(key===\"LeftTrigger\"||key===\"RightTrigger\"){let currentRange=xCloudGamepad[key];if(mappedKey===\"LeftTrigger\"||mappedKey===\"RightTrigger\")pressed=currentRange>=0.1,value=currentRange;else pressed=!0,value=currentRange>=0.9?1:0}else if(xCloudGamepad[key])pressed=!0,value=xCloudGamepad[key];if(pressed)pressedButtons[mappedKey]=value,releasedButtons[key]=0,isModified=!0}else if(mappedKey===!1)pressedButtons[key]=0,isModified=!0}isModified&&Object.assign(xCloudGamepad,releasedButtons,pressedButtons)}}if(shareButtonPressed&&!shareButtonHandled)window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n"; var poll_gamepad_default = "var self=this;if(window.BX_EXPOSED.disableGamepadPolling){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(50):self.pollGamepadssetTimeoutTimerID=window.setTimeout(self.pollGamepads,50);return}var currentGamepad=$gamepadVar$,btnHome=currentGamepad.buttons[16];if(btnHome){if(!self.bxHomeStates)self.bxHomeStates={};let intervalMs=0,hijack=!1;if(btnHome.pressed)if(hijack=!0,intervalMs=16,self.gamepadIsIdle.set(currentGamepad.index,!1),self.bxHomeStates[currentGamepad.index]){let lastTimestamp=self.bxHomeStates[currentGamepad.index].timestamp;if(currentGamepad.timestamp!==lastTimestamp){if(self.bxHomeStates[currentGamepad.index].timestamp=currentGamepad.timestamp,window.BX_EXPOSED.handleControllerShortcut(currentGamepad))self.bxHomeStates[currentGamepad.index].shortcutPressed+=1}}else window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index),self.bxHomeStates[currentGamepad.index]={shortcutPressed:0,timestamp:currentGamepad.timestamp};else if(self.bxHomeStates[currentGamepad.index]){hijack=!0;let info=structuredClone(self.bxHomeStates[currentGamepad.index]);if(self.bxHomeStates[currentGamepad.index]=null,info.shortcutPressed===0){let fakeGamepadMappings=[{GamepadIndex:currentGamepad.index,A:0,B:0,X:0,Y:0,LeftShoulder:0,RightShoulder:0,LeftTrigger:0,RightTrigger:0,View:0,Menu:0,LeftThumb:0,RightThumb:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,Nexus:1,LeftThumbXAxis:0,LeftThumbYAxis:0,RightThumbXAxis:0,RightThumbYAxis:0,PhysicalPhysicality:0,VirtualPhysicality:0,Dirty:!0,Virtual:!1}];intervalMs=currentGamepad.timestamp-info.timestamp>=500?500:100,self.inputSink.onGamepadInput(performance.now()-intervalMs,fakeGamepadMappings)}else intervalMs=window.BX_STREAM_SETTINGS.controllerPollingRate}if(hijack&&intervalMs){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(intervalMs):self.pollGamepadssetTimeoutTimerID=setTimeout(self.pollGamepads,intervalMs);return}}\n"; var expose_stream_session_default = 'var self=this;window.BX_EXPOSED.streamSession=self;var orgSetMicrophoneState=self.setMicrophoneState.bind(self);self.setMicrophoneState=(state)=>{orgSetMicrophoneState(state),window.BxEventBus.Stream.emit("microphone.state.changed",{state})};window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));var updateDimensionsStr=self.updateDimensions.toString();if(updateDimensionsStr.startsWith("function "))updateDimensionsStr=updateDimensionsStr.substring(9);var renderTargetVar=updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];updateDimensionsStr=updateDimensionsStr.replaceAll(renderTargetVar+".scroll","scroll");updateDimensionsStr=updateDimensionsStr.replace(`if(${renderTargetVar}){`,`\nif (${renderTargetVar}) {\nconst scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\nconst scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);eval(`this.updateDimensions = function ${updateDimensionsStr}`);\n'; @@ -4831,4284 +141,122 @@ 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}}`; -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 = ` +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 = ` const [logTag, logLevel, logMessage] = Array.from(arguments); const logFunc = [console.debug, console.log, console.warn, console.error][logLevel]; logFunc(logTag, '//', logMessage); -`; -return str = str.replaceAll(text, text + newCode), str; -}, -enableConsoleLogging(str) { -let text = "static isConsoleLoggingAllowed(){"; -if (!str.includes(text)) return !1; -return str = str.replaceAll(text, text + "return true;"), str; -}, -playVibration(str) { -let text = "}playVibration(e){"; -if (!str.includes(text)) return !1; -return str = str.replaceAll(text, text + vibration_adjust_default), str; -}, -disableGamepadDisconnectedScreen(str) { -let index = str.indexOf('"GamepadDisconnected_Title",'); -if (index < 0) return !1; -let constIndex = str.indexOf("const", index - 30); -return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str; -}, -patchUpdateInputConfigurationAsync(str) { -let text = "async updateInputConfigurationAsync(e){"; -if (!str.includes(text)) return !1; -let newCode = "e.enableTouchInput = true;"; -return str = str.replace(text, text + newCode), str; -}, -disableStreamGate(str) { -let index = str.indexOf('case"partially-ready":'); -if (index < 0) return !1; -let bracketIndex = str.indexOf("=>{", index - 150) + 3; -return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str; -}, -exposeTouchLayoutManager(str) { -let text = "this._perScopeLayoutsStream=new"; -if (!str.includes(text)) return !1; -let newCode = ` +`;return str = str.replaceAll(text, text + newCode), str;},enableConsoleLogging(str) {let text = "static isConsoleLoggingAllowed(){";if (!str.includes(text)) return !1;return str = str.replaceAll(text, text + "return true;"), str;},playVibration(str) {let text = "}playVibration(e){";if (!str.includes(text)) return !1;return str = str.replaceAll(text, text + vibration_adjust_default), str;},disableGamepadDisconnectedScreen(str) {let index = str.indexOf('"GamepadDisconnected_Title",');if (index < 0) return !1;let constIndex = str.indexOf("const", index - 30);return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str;},patchUpdateInputConfigurationAsync(str) {let text = "async updateInputConfigurationAsync(e){";if (!str.includes(text)) return !1;let newCode = "e.enableTouchInput = true;";return str = str.replace(text, text + newCode), str;},disableStreamGate(str) {let index = str.indexOf('case"partially-ready":');if (index < 0) return !1;let bracketIndex = str.indexOf("=>{", index - 150) + 3;return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str;},exposeTouchLayoutManager(str) {let text = "this._perScopeLayoutsStream=new";if (!str.includes(text)) return !1;let newCode = ` true; window.BX_EXPOSED["touchLayoutManager"] = this; window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}")); -`; -return str = str.replace(text, newCode + text), str; -}, -patchBabylonRendererClass(str) { -let index = str.indexOf(".current.render(),"); -if (index < 0) return !1; -index -= 1; -let newCode = ` -if (window.BX_EXPOSED.stopTakRendering) { -try { -document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen'); -${str[index]}.current.dispose(); -} catch (e) {} -window.BX_EXPOSED.stopTakRendering = false; -return; -} -`; -return str = str.substring(0, index) + newCode + str.substring(index), str; -}, -supportLocalCoOp(str) { -let text = "this.gamepadMappingsToSend=[],"; -if (!str.includes(text)) return !1; -let newCode = `true; ${local_co_op_enable_default}; true,`; -return str = str.replace(text, text + newCode), str; -}, -forceFortniteConsole(str) { -let text = "sendTouchInputEnabledMessage(e){"; -if (!str.includes(text)) return !1; -let newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);"; -return str = str.replace(text, text + newCode), str; -}, -disableTakRenderer(str) { -let text = "const{TakRenderer:"; -if (!str.includes(text)) return !1; -let autoOffCode = ""; -if (getGlobalPref("touchController.mode") === "off") autoOffCode = "return;"; -else if (getGlobalPref("touchController.autoOff")) autoOffCode = ` +`;return str = str.replace(text, newCode + text), str;},patchBabylonRendererClass(str) {let index = str.indexOf(".current.render(),");if (index < 0) return !1;index -= 1;let newCode = ` +if (window.BX_EXPOSED.stopTakRendering) {try {document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen');${str[index]}.current.dispose();} catch (e) {}window.BX_EXPOSED.stopTakRendering = false;return;} +`;return str = str.substring(0, index) + newCode + str.substring(index), str;},supportLocalCoOp(str) {let text = "this.gamepadMappingsToSend=[],";if (!str.includes(text)) return !1;let newCode = `true; ${local_co_op_enable_default}; true,`;return str = str.replace(text, text + newCode), str;},forceFortniteConsole(str) {let text = "sendTouchInputEnabledMessage(e){";if (!str.includes(text)) return !1;let newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);";return str = str.replace(text, text + newCode), str;},disableTakRenderer(str) {let text = "const{TakRenderer:";if (!str.includes(text)) return !1;let autoOffCode = "";if (getGlobalPref("touchController.mode") === "off") autoOffCode = "return;";else if (getGlobalPref("touchController.autoOff")) autoOffCode = ` const gamepads = window.navigator.getGamepads(); let gamepadFound = false; -for (let gamepad of gamepads) { -if (gamepad && gamepad.connected) { -gamepadFound = true; -break; -} -} -if (gamepadFound) { -return; -} -`; -let newCode = ` +for (let gamepad of gamepads) {if (gamepad && gamepad.connected) {gamepadFound = true;break;}} +if (gamepadFound) {return;} +`;let newCode = ` ${autoOffCode} const titleInfo = window.BX_EXPOSED.getTitleInfo(); -if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) { -return; -} -`; -return str = str.replace(text, newCode + text), str; -}, -streamCombineSources(str) { -let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen"; -if (!str.includes(text)) return !1; -return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str; -}, -patchStreamHud(str) { -let text = "let{onCollapse"; -if (!str.includes(text)) return !1; -let newCode = ` +if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {return;} +`;return str = str.replace(text, newCode + text), str;},streamCombineSources(str) {let text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen";if (!str.includes(text)) return !1;return str = str.replace(text, "this.useCombinedAudioVideoStream=true"), str;},patchStreamHud(str) {let text = "let{onCollapse";if (!str.includes(text)) return !1;let newCode = ` // Expose onShowStreamMenu window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu; // Restore the "..." button e.guideUI = null; -`; -if (getGlobalPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;"; -return str = str.replace(text, newCode + text), str; -}, -broadcastPollingMode(str) { -let text = ".setPollingMode=e=>{"; -if (!str.includes(text)) return !1; -let newCode = ` +`;if (getGlobalPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;";return str = str.replace(text, newCode + text), str;},broadcastPollingMode(str) {let text = ".setPollingMode=e=>{";if (!str.includes(text)) return !1;let newCode = ` window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase(); BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED); -`; -return str = str.replace(text, text + newCode), str; -}, -patchGamepadPolling(str) { -let index = str.indexOf(".shouldHandleGamepadInput)())return void"); -if (index < 0) return !1; -return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str; -}, -patchXcloudTitleInfo(str) { -let text = "async cloudConnect", index = str.indexOf(text); -if (index < 0) return !1; -let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; -if (!params) return !1; -let titleInfoVar = params.split(",")[0], newCode = ` +`;return str = str.replace(text, text + newCode), str;},patchGamepadPolling(str) {let index = str.indexOf(".shouldHandleGamepadInput)())return void");if (index < 0) return !1;return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str;},patchXcloudTitleInfo(str) {let text = "async cloudConnect", index = str.indexOf(text);if (index < 0) return !1;let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];if (!params) return !1;let titleInfoVar = params.split(",")[0], newCode = ` ${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar}); BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar}); -`; -return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; -}, -patchRemotePlayMkb(str) { -let text = "async homeConsoleConnect", index = str.indexOf(text); -if (index < 0) return !1; -let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1]; -if (!params) return !1; -let configsVar = params.split(",")[1], newCode = ` -Object.assign(${configsVar}.inputConfiguration, { -enableMouseInput: false, -enableKeyboardInput: false, -enableAbsoluteMouse: false, -}); +`;return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;},patchRemotePlayMkb(str) {let text = "async homeConsoleConnect", index = str.indexOf(text);if (index < 0) return !1;let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];if (!params) return !1;let configsVar = params.split(",")[1], newCode = ` +Object.assign(${configsVar}.inputConfiguration, {enableMouseInput: false,enableKeyboardInput: false,enableAbsoluteMouse: false,}); BxLogger.info('patchRemotePlayMkb', ${configsVar}); -`; -return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str; -}, -patchAudioMediaStream(str) { -let text = ".srcObject=this.audioMediaStream,"; -if (!str.includes(text)) return !1; -let newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),"; -return str = str.replace(text, text + newCode), str; -}, -patchCombinedAudioVideoMediaStream(str) { -let text = ".srcObject=this.combinedAudioVideoStream"; -if (!str.includes(text)) return !1; -let newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)"; -return str = str.replace(text, text + newCode), str; -}, -patchTouchControlDefaultOpacity(str) { -let text = "opacityMultiplier:1"; -if (!str.includes(text)) return !1; -let newCode = `opacityMultiplier: ${(getGlobalPref("touchController.opacity.default") / 100).toFixed(1)}`; -return str = str.replace(text, newCode), str; -}, -patchShowSensorControls(str) { -let text = ",{shouldShowSensorControls:"; -if (!str.includes(text)) return !1; -let newCode = ",{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||"; -return str = str.replace(text, newCode), str; -}, -exposeStreamSession(str) { -let text = ",this._connectionType="; -if (!str.includes(text)) return !1; -let newCode = `; +`;return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;},patchAudioMediaStream(str) {let text = ".srcObject=this.audioMediaStream,";if (!str.includes(text)) return !1;let newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),";return str = str.replace(text, text + newCode), str;},patchCombinedAudioVideoMediaStream(str) {let text = ".srcObject=this.combinedAudioVideoStream";if (!str.includes(text)) return !1;let newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)";return str = str.replace(text, text + newCode), str;},patchTouchControlDefaultOpacity(str) {let text = "opacityMultiplier:1";if (!str.includes(text)) return !1;let newCode = `opacityMultiplier: ${(getGlobalPref("touchController.opacity.default") / 100).toFixed(1)}`;return str = str.replace(text, newCode), str;},patchShowSensorControls(str) {let text = ",{shouldShowSensorControls:";if (!str.includes(text)) return !1;let newCode = ",{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||";return str = str.replace(text, newCode), str;},exposeStreamSession(str) {let text = ",this._connectionType=";if (!str.includes(text)) return !1;let newCode = `; ${expose_stream_session_default} -true` + text; -return str = str.replace(text, newCode), str; -}, -skipFeedbackDialog(str) { -let text = "shouldTransitionToFeedback(e){"; -if (!str.includes(text)) return !1; -return str = str.replace(text, text + "return !1;"), str; -}, -enableNativeMkb(str) { -let index = str.indexOf(".mouseSupported&&"); -if (index < 0) return !1; -let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`; -if (!str.includes(text)) return !1; -return str = str.replace(text, text + "return true;"), str; -}, -patchMouseAndKeyboardEnabled(str) { -let text = "get mouseAndKeyboardEnabled(){"; -if (!str.includes(text)) return !1; -return str = str.replace(text, text + "return true;"), str; -}, -exposeInputChannel(str) { -let index = str.indexOf("this.flushData="); -if (index < 0) return !1; -let newCode = "window.BX_EXPOSED.inputChannel = this,"; -return str = PatcherUtils.insertAt(str, index, newCode), str; -}, -disableNativeRequestPointerLock(str) { -let text = "async requestPointerLock(){"; -if (!str.includes(text)) return !1; -return str = str.replace(text, text + "return;"), str; -}, -patchRequestInfoCrash(str) { -let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");'; -if (!str.includes(text)) return !1; -return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str; -}, -exposeDialogRoutes(str) { -let text = "return{goBack:function(){"; -if (!str.includes(text)) return !1; -return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str; -}, -enableTvRoutes(str) { -let index = str.indexOf(".LoginDeviceCode.path,"); -if (index < 0) return !1; -let match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100)); -if (!match) return !1; -let funcName = match[1]; -if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1; -return str = str.substring(0, index) + "|| true" + str.substring(index), str; -}, -ignoreNewsSection(str) { -let index = str.indexOf('Logger("CarouselRow")'); -if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const ", index, 200)), index < 0) return !1; -return str = PatcherUtils.insertAt(str, index, "return null;"), str; -}, -ignorePlayWithFriendsSection(str) { -let index = str.indexOf('location:"PlayWithFriendsRow",'); -if (index < 0) return !1; -if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1; -return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str; -}, -ignoreAllGamesSection(str) { -let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer'); -if (index < 0) return !1; -if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1; -if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1; -return str = PatcherUtils.insertAt(str, index, "true ? null :"), str; -}, -ignorePlayWithTouchSection(str) { -let index = str.indexOf('("Play_With_Touch"),'); -if (index < 0) return !1; -if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1; -return str = PatcherUtils.insertAt(str, index, "return null;"), str; -}, -ignoreSiglSections(str) { -let index = str.indexOf("SiglRow-module__heroCard___"); -if (index < 0) return !1; -if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1; -let PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), siglIds = [], sections = { -"native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", -"most-popular": "e7590b22-e299-44db-ae22-25c61405454c" -}; -for (let section of PREF_HIDE_SECTIONS) { -let galleryId = sections[section]; -galleryId && siglIds.push(galleryId); -} -let newCode = ` -if (e && e.id) { -const siglId = e.id; -if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) { -return null; -} -} -`; -return str = PatcherUtils.insertAt(str, index, newCode), str; -}, -overrideStorageGetSettings(str) { -let text = "}getSetting(e){"; -if (!str.includes(text)) return !1; -let newCode = ` +true` + text;return str = str.replace(text, newCode), str;},skipFeedbackDialog(str) {let text = "shouldTransitionToFeedback(e){";if (!str.includes(text)) return !1;return str = str.replace(text, text + "return !1;"), str;},enableNativeMkb(str) {let index = str.indexOf(".mouseSupported&&");if (index < 0) return !1;let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`;if (!str.includes(text)) return !1;return str = str.replace(text, text + "return true;"), str;},patchMouseAndKeyboardEnabled(str) {let text = "get mouseAndKeyboardEnabled(){";if (!str.includes(text)) return !1;return str = str.replace(text, text + "return true;"), str;},exposeInputChannel(str) {let index = str.indexOf("this.flushData=");if (index < 0) return !1;let newCode = "window.BX_EXPOSED.inputChannel = this,";return str = PatcherUtils.insertAt(str, index, newCode), str;},disableNativeRequestPointerLock(str) {let text = "async requestPointerLock(){";if (!str.includes(text)) return !1;return str = str.replace(text, text + "return;"), str;},patchRequestInfoCrash(str) {let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");';if (!str.includes(text)) return !1;return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str;},exposeDialogRoutes(str) {let text = "return{goBack:function(){";if (!str.includes(text)) return !1;return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str;},enableTvRoutes(str) {let index = str.indexOf(".LoginDeviceCode.path,");if (index < 0) return !1;let match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100));if (!match) return !1;let funcName = match[1];if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1;return str = str.substring(0, index) + "|| true" + str.substring(index), str;},ignoreNewsSection(str) {let index = str.indexOf('Logger("CarouselRow")');if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const ", index, 200)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, "return null;"), str;},ignorePlayWithFriendsSection(str) {let index = str.indexOf('location:"PlayWithFriendsRow",');if (index < 0) return !1;if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1;return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str;},ignoreAllGamesSection(str) {let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer');if (index < 0) return !1;if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1;if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, "true ? null :"), str;},ignorePlayWithTouchSection(str) {let index = str.indexOf('("Play_With_Touch"),');if (index < 0) return !1;if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, "return null;"), str;},ignoreSiglSections(str) {let index = str.indexOf("SiglRow-module__heroCard___");if (index < 0) return !1;if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1;let PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), siglIds = [], sections = {"native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486","most-popular": "e7590b22-e299-44db-ae22-25c61405454c"};for (let section of PREF_HIDE_SECTIONS) {let galleryId = sections[section];galleryId && siglIds.push(galleryId);}let newCode = ` +if (e && e.id) {const siglId = e.id;if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) {return null;}} +`;return str = PatcherUtils.insertAt(str, index, newCode), str;},overrideStorageGetSettings(str) {let text = "}getSetting(e){";if (!str.includes(text)) return !1;let newCode = ` // console.log('setting', this.baseStorageKey, e); -if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) { -const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey]; -if (e in settings) { -return settings[e]; -} -} -`; -return str = str.replace(text, text + newCode), str; -}, -alwaysShowStreamHud(str) { -let index = str.indexOf(",{onShowStreamMenu:"); -if (index < 0) return !1; -if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1; -let commaIndex = str.indexOf(",", index - 10); -return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str; -}, -patchSetCurrentFocus(str) { -let index = str.indexOf(".setCurrentFocus=("); -if (index < 0) return !1; -return index = str.indexOf("{", index) + 1, str = PatcherUtils.insertAt(str, index, "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });"), str; -}, -detectProductDetailPage(str) { -let index = str.indexOf('{location:"ProductDetailPage",'); -if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1; -return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str; -}, -detectBrowserRouterReady(str) { -let index = str.indexOf("{history:this.history,"); -if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 100)), index < 0) return !1; -return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str; -}, -guideAchievementsDefaultLocked(str) { -let index = str.indexOf("FilterButton-module__container"); -if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1; -if (str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250)), index < 0) return !1; -return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str; -}, -disableTouchContextMenu(str) { -let index = str.indexOf('"ContextualCardActions-module__container'); -if (index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index)), index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return ", index, 50)), index < 0) return !1; -return str = PatcherUtils.replaceWith(str, index, "return", "return () => {};"), str; -}, -optimizeGameSlugGenerator(str) { -let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g"; -if (!str.includes(text)) return !1; -return str = str.replace(text, "window.BX_EXPOSED.GameSlugRegexes[0]"), str = str.replace("/ {2,}/g", "window.BX_EXPOSED.GameSlugRegexes[1]"), str = str.replace("/ /g", "window.BX_EXPOSED.GameSlugRegexes[2]"), str; -}, -modifyPreloadedState(str) { -let text = "=window.__PRELOADED_STATE__;"; -if (!str.includes(text)) return !1; -return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str; -}, -homePageBeforeLoad(str) { -return PatcherUtils.patchBeforePageLoad(str, "home"); -}, -productDetailPageBeforeLoad(str) { -return PatcherUtils.patchBeforePageLoad(str, "product-detail"); -}, -streamPageBeforeLoad(str) { -return PatcherUtils.patchBeforePageLoad(str, "stream"); -}, -disableAbsoluteMouse(str) { -let text = "sendAbsoluteMouseCapableMessage(e){"; -if (!str.includes(text)) return !1; -return str = str.replace(text, text + "return;"), str; -}, -changeNotificationsSubscription(str) { -let text = ";buildSubscriptionQueryParamsForNotifications(", index = str.indexOf(text); -if (index < 0) return !1; -index += text.length; -let subsVar = str[index]; -index = str.indexOf("{", index) + 1; -let blockFeatures = getGlobalPref("block.features"), filters = []; -if (blockFeatures.includes("notifications-invites")) filters.push("GameInvite", "PartyInvite"); -if (blockFeatures.includes("friends")) filters.push("Follower"); -if (blockFeatures.includes("notifications-achievements")) filters.push("AchievementUnlock"); -let newCode = ` +if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey];if (e in settings) {return settings[e];}} +`;return str = str.replace(text, text + newCode), str;},alwaysShowStreamHud(str) {let index = str.indexOf(",{onShowStreamMenu:");if (index < 0) return !1;if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1;let commaIndex = str.indexOf(",", index - 10);return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str;},patchSetCurrentFocus(str) {let index = str.indexOf(".setCurrentFocus=(");if (index < 0) return !1;return index = str.indexOf("{", index) + 1, str = PatcherUtils.insertAt(str, index, "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });"), str;},detectProductDetailPage(str) {let index = str.indexOf('{location:"ProductDetailPage",');if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str;},detectBrowserRouterReady(str) {let index = str.indexOf("{history:this.history,");if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 100)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str;},guideAchievementsDefaultLocked(str) {let index = str.indexOf("FilterButton-module__container");if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1;if (str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250)), index < 0) return !1;return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str;},disableTouchContextMenu(str) {let index = str.indexOf('"ContextualCardActions-module__container');if (index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index)), index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return ", index, 50)), index < 0) return !1;return str = PatcherUtils.replaceWith(str, index, "return", "return () => {};"), str;},optimizeGameSlugGenerator(str) {let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g";if (!str.includes(text)) return !1;return str = str.replace(text, "window.BX_EXPOSED.GameSlugRegexes[0]"), str = str.replace("/ {2,}/g", "window.BX_EXPOSED.GameSlugRegexes[1]"), str = str.replace("/ /g", "window.BX_EXPOSED.GameSlugRegexes[2]"), str;},modifyPreloadedState(str) {let text = "=window.__PRELOADED_STATE__;";if (!str.includes(text)) return !1;return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str;},homePageBeforeLoad(str) {return PatcherUtils.patchBeforePageLoad(str, "home");},productDetailPageBeforeLoad(str) {return PatcherUtils.patchBeforePageLoad(str, "product-detail");},streamPageBeforeLoad(str) {return PatcherUtils.patchBeforePageLoad(str, "stream");},disableAbsoluteMouse(str) {let text = "sendAbsoluteMouseCapableMessage(e){";if (!str.includes(text)) return !1;return str = str.replace(text, text + "return;"), str;},changeNotificationsSubscription(str) {let text = ";buildSubscriptionQueryParamsForNotifications(", index = str.indexOf(text);if (index < 0) return !1;index += text.length;let subsVar = str[index];index = str.indexOf("{", index) + 1;let blockFeatures = getGlobalPref("block.features"), filters = [];if (blockFeatures.includes("notifications-invites")) filters.push("GameInvite", "PartyInvite");if (blockFeatures.includes("friends")) filters.push("Follower");if (blockFeatures.includes("notifications-achievements")) filters.push("AchievementUnlock");let newCode = ` let subs = ${subsVar}; subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val)); ${subsVar} = subs; -`; -return str = PatcherUtils.insertAt(str, index, newCode), str; -}, -exposeReactCreateComponent(str) { -let index = str.indexOf(".prototype.isReactComponent={}"); -if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1; -let newCode = "window.BX_EXPOSED.reactCreateElement="; -return str = PatcherUtils.insertAt(str, index - 1, newCode), str; -}, -gameCardCustomIcons(str) { -let initialIndex = str.indexOf("const{supportedInputIcons:"); -if (initialIndex < 0) return !1; -let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge")); -if (returnIndex < 0) return !1; -let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300); -if (arrowIndex < 0) return !1; -let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0)); -if (!paramVar || !supportedInputIconsVar) return !1; -let newCode = renderString(game_card_icons_default, { -param: paramVar, -supportedInputIcons: supportedInputIconsVar -}); -return str = PatcherUtils.insertAt(str, returnIndex, newCode), str; -}, -setImageQuality(str) { -let index = str.indexOf("const{size:{width:"); -if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1; -let paramVar = PatcherUtils.getVariableNameBefore(str, index); -if (!paramVar) return !1; -index = PatcherUtils.indexOf(str, "return", index, 200); -let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`; -return str = PatcherUtils.insertAt(str, index, newCode), str; -}, -setBackgroundImageQuality(str) { -let index = str.indexOf("}?w=${"); -if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1; -return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str; -} -}, PATCH_ORDERS = PatcherUtils.filterPatches([ -...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [ -"enableNativeMkb", -"disableAbsoluteMouse" -] : [], -"exposeReactCreateComponent", -"gameCardCustomIcons", -...getGlobalPref("ui.imageQuality") < 90 ? [ -"setImageQuality" -] : [], -"modifyPreloadedState", -"optimizeGameSlugGenerator", -"detectBrowserRouterReady", -"patchRequestInfoCrash", -"disableStreamGate", -"broadcastPollingMode", -"patchGamepadPolling", -"exposeStreamSession", -"exposeDialogRoutes", -"homePageBeforeLoad", -"productDetailPageBeforeLoad", -"streamPageBeforeLoad", -"guideAchievementsDefaultLocked", -"enableTvRoutes", -"supportLocalCoOp", -"overrideStorageGetSettings", -getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus", -getGlobalPref("ui.layout") !== "default" && "websiteLayout", -getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole", -...STATES.userAgent.capabilities.touch ? [ -"disableTouchContextMenu" -] : [], -...getGlobalPref("block.tracking") ? [ -"disableAiTrack", -"disableTelemetry", -"blockWebRtcStatsCollector", -"disableIndexDbLogging", -"disableTelemetryProvider" -] : [], -...getGlobalPref("xhome.enabled") ? [ -"remotePlayKeepAlive", -"remotePlayDirectConnectUrl", -"remotePlayDisableAchievementToast", -"remotePlayRecentlyUsedTitleIds", -"remotePlayWebTitle", -STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync" -] : [], -...BX_FLAGS.EnableXcloudLogging ? [ -"enableConsoleLogging", -"enableXcloudLogger" -] : [] -]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([ -hideSections.includes("news") && "ignoreNewsSection", -hideSections.includes("friends") && "ignorePlayWithFriendsSection", -hideSections.includes("all-games") && "ignoreAllGamesSection", -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" +`;return str = PatcherUtils.insertAt(str, index, newCode), str;},exposeReactCreateComponent(str) {let index = str.indexOf(".prototype.isReactComponent={}");if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;let newCode = "window.BX_EXPOSED.reactCreateElement=";return str = PatcherUtils.insertAt(str, index - 1, newCode), str;},gameCardCustomIcons(str) {let initialIndex = str.indexOf("const{supportedInputIcons:");if (initialIndex < 0) return !1;let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));if (returnIndex < 0) return !1;let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);if (arrowIndex < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));if (!paramVar || !supportedInputIconsVar) return !1;let newCode = renderString(game_card_icons_default, {param: paramVar,supportedInputIcons: supportedInputIconsVar});return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;},setImageQuality(str) {let index = str.indexOf("const{size:{width:");if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;let paramVar = PatcherUtils.getVariableNameBefore(str, index);if (!paramVar) return !1;index = PatcherUtils.indexOf(str, "return", index, 200);let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;return str = PatcherUtils.insertAt(str, index, newCode), str;},setBackgroundImageQuality(str) {let index = str.indexOf("}?w=${");if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;}}, PATCH_ORDERS = PatcherUtils.filterPatches([...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? ["enableNativeMkb","disableAbsoluteMouse"] : [],"exposeReactCreateComponent","gameCardCustomIcons",...getGlobalPref("ui.imageQuality") < 90 ? ["setImageQuality"] : [],"modifyPreloadedState","optimizeGameSlugGenerator","detectBrowserRouterReady","patchRequestInfoCrash","disableStreamGate","broadcastPollingMode","patchGamepadPolling","exposeStreamSession","exposeDialogRoutes","homePageBeforeLoad","productDetailPageBeforeLoad","streamPageBeforeLoad","guideAchievementsDefaultLocked","enableTvRoutes","supportLocalCoOp","overrideStorageGetSettings",getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",getGlobalPref("ui.layout") !== "default" && "websiteLayout",getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",...STATES.userAgent.capabilities.touch ? ["disableTouchContextMenu"] : [],...getGlobalPref("block.tracking") ? ["disableAiTrack","disableTelemetry","blockWebRtcStatsCollector","disableIndexDbLogging","disableTelemetryProvider"] : [],...getGlobalPref("xhome.enabled") ? ["remotePlayKeepAlive","remotePlayDirectConnectUrl","remotePlayDisableAchievementToast","remotePlayRecentlyUsedTitleIds","remotePlayWebTitle",STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"] : [],...BX_FLAGS.EnableXcloudLogging ? ["enableConsoleLogging","enableXcloudLogger"] : [] +]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([hideSections.includes("news") && "ignoreNewsSection",hideSections.includes("friends") && "ignorePlayWithFriendsSection",hideSections.includes("all-games") && "ignoreAllGamesSection",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" ]), 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; -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, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS); -} -if (modified) { -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(); -} -} -class PatcherCache { -static instance; -static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache); -KEY_CACHE = "BetterXcloud.Patches.Cache"; -KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature"; -CACHE; -constructor() { -this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE); -let pathName = window.location.pathname; -if (pathName.includes("/play/launch/")) Patcher.patchPage("stream"); -else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail"); -else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home"); -PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0)); -} -getSignature() { -let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]'); -if ($link) { -let match = /\/client\.([^\.]+)\.js/.exec($link.href); -match && (webVersion = match[1]); -} else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? ""; -return hashCode(scriptVersion + webVersion + patches); -} -clear() { -window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {}; -} -checkSignature() { -let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature(); -if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear(); -else BxLogger.info(LOG_TAG2, "Signature unchanged"); -} -cleanupPatches(patches) { -return patches.filter((item2) => { -for (let id in this.CACHE) -if (this.CACHE[id].includes(item2)) return !1; -return !0; -}); -} -getPatches(id) { -return this.CACHE[id]; -} -saveToCache(subCache) { -for (let id in subCache) { -let patchNames = subCache[id], data = this.CACHE[id]; -if (!data) this.CACHE[id] = patchNames; -else for (let patchName of patchNames) -if (!data.includes(patchName)) data.push(patchName); -} -window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE)); -} -} -class FullscreenText { -static instance; -static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); -LOG_TAG = "FullscreenText"; -$text; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", { -class: "bx-fullscreen-text bx-gone" -}), document.documentElement.appendChild(this.$text); -} -show(msg) { -document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg; -} -hide() { -document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone"); -} -} -class BaseProfileManagerDialog extends NavigationDialog { -$container; -title; -presetsDb; -allPresets; -currentPresetId = null; -activatedPresetId = null; -$presets; -$header; -$defaultNote; -$content; -$btnRename; -$btnDelete; -constructor(title, presetsDb) { -super(); -this.title = title, this.presetsDb = presetsDb; -} -async renderSummary(presetId) { -return null; -} -updateButtonStates() { -let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0; -this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset); -} -async renderPresetsList() { -if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0]; -renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 }); -} -promptNewName(action, value = "") { -let newName = ""; -while (!newName) { -if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1; -newName = newName.trim(); -} -return newName ? newName : !1; -} -async renderDialog() { -this.$presets = CE("select", { -class: "bx-full-width", -tabindex: -1 -}); -let $select = BxSelectElement.create(this.$presets); -$select.addEventListener("input", (e) => { -this.switchPreset(parseInt($select.value)); -}); -let $header = CE("div", { -class: "bx-dialog-preset-tools", -_nearby: { -orientation: "horizontal", -focus: $select -} -}, $select, this.$btnRename = createButton({ -title: t("rename"), -icon: BxIcon.CURSOR_TEXT, -style: 64, -onClick: async () => { -let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name); -if (!newName) return; -preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh(); -} -}), this.$btnDelete = createButton({ -icon: BxIcon.TRASH, -title: t("delete"), -style: 4 | 64, -onClick: async (e) => { -if (!confirm(t("confirm-delete-preset"))) return; -await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh(); -} -}), createButton({ -icon: BxIcon.NEW, -title: t("new"), -style: 64 | 1, -onClick: async (e) => { -let newName = this.promptNewName(t("new")); -if (!newName) return; -let newId = await this.presetsDb.newPreset(newName, this.presetsDb.BLANK_PRESET_DATA); -this.currentPresetId = newId, await this.refresh(); -} -}), createButton({ -icon: BxIcon.COPY, -title: t("copy"), -style: 64 | 1, -onClick: async (e) => { -let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`); -if (!newName) return; -let newId = await this.presetsDb.newPreset(newName, preset.data); -this.currentPresetId = newId, await this.refresh(); -} -})); -this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, this.title), createButton({ -icon: BxIcon.CLOSE, -style: 64 | 2048 | 8, -onClick: (e) => this.hide() -})), CE("div", !1, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content)); -} -async refresh() { -await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 }); -} -async onBeforeMount(configs = {}) { -await this.renderPresetsList(); -let valid = !1; -if (typeof configs?.id === "number") { -if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0; -} -if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null; -this.refresh(); -} -getDialog() { -return this; -} -getContent() { -if (!this.$container) this.renderDialog(); -return this.$container; -} -focusIfNeeded() { -this.dialogManager.focus(this.$header); -} -} -var SHORTCUT_ACTIONS = { -[t("better-xcloud")]: { -"bx.settings.show": [t("settings"), t("show")] -}, -...STATES.browser.capabilities.mkb ? { -[t("mouse-and-keyboard")]: { -"mkb.toggle": [t("toggle")] -} -} : {}, -[t("controller")]: { -"controller.xbox.press": [t("button-xbox"), t("press")] -}, -...AppInterface ? { -[t("device")]: { -"device.sound.toggle": [t("sound"), t("toggle")], -"device.volume.inc": [t("volume"), t("increase")], -"device.volume.dec": [t("volume"), t("decrease")], -"device.brightness.inc": [t("brightness"), t("increase")], -"device.brightness.dec": [t("brightness"), t("decrease")] -} -} : {}, -[t("stream")]: { -"stream.screenshot.capture": [t("take-screenshot")], -"stream.video.toggle": [t("video"), t("toggle")], -"stream.sound.toggle": [t("sound"), t("toggle")], -...getGlobalPref("audio.volume.booster.enabled") ? { -"stream.volume.inc": [t("volume"), t("increase")], -"stream.volume.dec": [t("volume"), t("decrease")] -} : {}, -"stream.menu.show": [t("menu"), t("show")], -"stream.stats.toggle": [t("stats"), t("show-hide")], -"stream.microphone.toggle": [t("microphone"), t("toggle")] -}, -[t("other")]: { -"ta.open": [t("true-achievements"), t("show")] -} -}; -class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog { -static instance; -static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts"))); -$content; -selectActions = {}; -BUTTONS_ORDER = [ -3, -0, -2, -1, -12, -13, -14, -15, -8, -9, -4, -5, -6, -7, -10, -11 -]; -constructor(title) { -super(title, ControllerShortcutsTable.getInstance()); -let $baseSelect = CE("select", { -class: "bx-full-width", -autocomplete: "off" -}, CE("option", { value: "" }, "---")); -for (let groupLabel in SHORTCUT_ACTIONS) { -let items = SHORTCUT_ACTIONS[groupLabel]; -if (!items) continue; -let $optGroup = CE("optgroup", { label: groupLabel }); -for (let action in items) { -let crumbs = items[action]; -if (!crumbs) continue; -let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label); -$optGroup.appendChild($option); -} -$baseSelect.appendChild($optGroup); -} -let $content = CE("div", { -class: "bx-controller-shortcuts-manager-container" -}), onActionChanged = (e) => { -if (!e.ignoreOnChange) this.updatePreset(); -}, fragment = document.createDocumentFragment(); -fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note"))); -for (let button of this.BUTTONS_ORDER) { -let prompt2 = GamepadKeyName[button][1], $row = CE("div", { -class: "bx-shortcut-row", -_nearby: { -orientation: "horizontal" -} -}), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0)); -$select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, { -focus: $select -}), $row.append($label, $select), fragment.appendChild($row); -} -$content.appendChild(fragment), this.$content = $content; -} -switchPreset(id) { -let preset = this.allPresets.data[id]; -if (!preset) { -this.currentPresetId = 0; -return; -} -this.currentPresetId = id; -let isDefaultPreset = id <= 0, actions = preset.data, button; -for (button in this.selectActions) { -let $select = this.selectActions[button]; -$select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { -ignoreOnChange: !0, -manualTrigger: !0 -}); -} -super.updateButtonStates(); -} -updatePreset() { -let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), button; -for (button in this.selectActions) { -let action = this.selectActions[button].value; -if (!action) continue; -newData.mapping[button] = action; -} -let preset = this.allPresets.data[this.currentPresetId]; -preset.data = newData, this.presetsDb.updatePreset(preset); -} -onBeforeUnmount() { -StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); -} -} -class BxDualNumberStepper extends HTMLInputElement { -controlValues; -controlMin; -controlMinDiff; -controlMax; -steps; -options; -onChange; -$text; -$rangeFrom; -$rangeTo; -$activeRange; -onRangeInput; -setValue; -getValue; -normalizeValue; -static create(key, values, options, onChange) { -options.suffix = options.suffix || "", options.disabled = !!options.disabled; -let $text, $rangeFrom, $rangeTo, self = CE("div", { -class: "bx-dual-number-stepper", -id: `bx_setting_${escapeCssSelector(key)}` -}, $text = CE("span")); -if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self; -return $rangeFrom = CE("input", { -type: "range", -min: self.controlMin, -max: self.controlMax, -step: self.steps, -tabindex: 0 -}), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", !1, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, { -focus: $rangeFrom, -orientation: "vertical" -}), Object.defineProperty(self, "value", { -get() { -return self.controlValues; -}, -set(value) { -let from, to; -if (typeof value === "string") { -let tmp = value.split(","); -from = parseInt(tmp[0]), to = parseInt(tmp[1]); -} else if (Array.isArray(value)) [from, to] = value; -if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]); -} -}), self; -} -static setValues(values) { -let from, to; -if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values); -else from = this.controlMin, to = this.controlMax, values = [from, to]; -this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString(); -let ratio = 100 / (this.controlMax - this.controlMin); -this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%"); -} -static getValues() { -return this.controlValues || [this.controlMin, this.controlMax]; -} -static normalizeValues(values) { -let [from, to] = values; -if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from); -else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to); -return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to]; -} -static onRangeInput(e) { -this.$activeRange = e.target; -let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]); -if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values); -} -static onContextMenu(e) { -e.preventDefault(); -} -static updateTextValue() { -let values = this.controlValues, textContent = null; -if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax); -if (textContent === null) { -let [from, to] = values; -if (from === this.controlMin && to === this.controlMax) textContent = t("default"); -else { -let pad = to.toString().length; -textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`; -} -} -return textContent; -} -} -class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog { -static instance; -static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization"))); -$vibrationIntensity; -$leftTriggerRange; -$rightTriggerRange; -$leftStickDeadzone; -$rightStickDeadzone; -$btnDetect; -selectsMap = {}; -selectsOrder = []; -isDetectingButton = !1; -detectIntervalId = null; -static BUTTONS_ORDER = [ -0, -1, -2, -3, -12, -15, -13, -14, -4, -5, -6, -7, -10, -11, -104, -204, -8, -9, -17 -]; -constructor(title) { -super(title, ControllerCustomizationsTable.getInstance()); -this.render(); -} -render() { -let isControllerFriendly = getGlobalPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => { -if (!e.ignoreOnChange) this.updatePreset(); -}, boundUpdatePreset = this.updatePreset.bind(this); -for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { -if (gamepadKey === 17) continue; -let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0]; -(gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", { -value: gamepadKey, -_dataset: { label: GamepadKeyName[gamepadKey][1] } -}, name)); -} -for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { -let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0); -$clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove(); -let $select = BxSelectElement.create($clonedSelect); -$select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey); -let $row = CE("div", { -class: "bx-controller-key-row", -_nearby: { orientation: "horizontal" } -}, CE("label", { title: buttonName }, buttonPrompt), $select); -$rows.append($row); -} -if (getGlobalPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) { -let $select = this.selectsMap[this.selectsOrder[i]], directions = { -1: i - 2, -3: i + 2, -4: i - 1, -2: i + 1 -}; -for (let dir in directions) { -let idx = directions[dir]; -if (typeof this.selectsOrder[idx] === "undefined") continue; -let $targetSelect = this.selectsMap[this.selectsOrder[idx]]; -setNearby($select, { -[dir]: $targetSelect -}); -} -} -let blankSettings = this.presetsDb.BLANK_PRESET_DATA.settings, params = { -min: 0, -minDiff: 1, -max: 100, -steps: 1 -}; -this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({ -label: t("detect-controller-button"), -classes: ["bx-btn-detect"], -style: 4096 | 64 | 128, -onClick: () => { -this.startDetectingButton(); -} -}), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, { -steps: 10, -suffix: "%", -exactTicks: 20, -customTextValue: (value) => { -return value = parseInt(value), value === 0 ? t("off") : value + "%"; -} -}, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", blankSettings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", blankSettings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", blankSettings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", blankSettings.rightStickDeadzone, params, boundUpdatePreset))); -} -startDetectingButton() { -this.isDetectingButton = !0; -let { $btnDetect } = this; -$btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0; -let count = 4; -$btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => { -if (count -= 1, count === 0) { -this.stopDetectingButton(), $btnDetect.focus(); -return; -} -$btnDetect.textContent = `[${count}] ${t("press-any-button")}`; -}, 1000); -} -stopDetectingButton() { -let { $btnDetect } = this; -$btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null; -} -async onBeforeMount() { -this.stopDetectingButton(), super.onBeforeMount(...arguments); -} -onBeforeUnmount() { -this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount(); -} -handleGamepad(button) { -if (!this.isDetectingButton) return super.handleGamepad(button); -if (button in ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { -this.stopDetectingButton(); -let $select = this.selectsMap[button], $label = $select.previousElementSibling; -if ($label.addEventListener("animationend", () => { -$label.classList.remove("bx-horizontal-shaking"); -}, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getGlobalPref("ui.controllerFriendly")) -this.dialogManager.focus($select); -} -return !0; -} -switchPreset(id) { -let preset = this.allPresets.data[id]; -if (!preset) { -this.currentPresetId = 0; -return; -} -let { -$btnDetect, -$vibrationIntensity, -$leftStickDeadzone, -$rightStickDeadzone, -$leftTriggerRange, -$rightTriggerRange, -selectsMap -} = this, presetData = preset.data; -this.currentPresetId = id; -let isDefaultPreset = id <= 0; -this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset); -let buttonIndex; -for (buttonIndex in selectsMap) { -buttonIndex = buttonIndex; -let $select = selectsMap[buttonIndex]; -if (!$select) continue; -let mappedButton = presetData.mapping[buttonIndex]; -$select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", { -ignoreOnChange: !0, -manualTrigger: !0 -}); -} -presetData.settings = Object.assign({}, this.presetsDb.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange); -} -updatePreset() { -let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), gamepadKey; -for (gamepadKey in this.selectsMap) { -let value = this.selectsMap[gamepadKey].value; -if (!value) continue; -let mapTo = value === "false" ? !1 : parseInt(value); -newData.mapping[gamepadKey] = mapTo; -} -Object.assign(newData.settings, { -vibrationIntensity: parseInt(this.$vibrationIntensity.value), -leftStickDeadzone: this.$leftStickDeadzone.getValue(), -rightStickDeadzone: this.$rightStickDeadzone.getValue(), -leftTriggerRange: this.$leftTriggerRange.getValue(), -rightTriggerRange: this.$rightTriggerRange.getValue() -}); -let preset = this.allPresets.data[this.currentPresetId]; -preset.data = newData, this.presetsDb.updatePreset(preset); -} -async renderSummary(presetId) { -let preset = await this.presetsDb.getPreset(presetId); -if (!preset) return null; -let presetData = preset.data, $content, showNote = !1; -if (Object.keys(presetData.mapping).length > 0) { -$content = CE("div", { class: "bx-controller-customization-summary" }); -for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) { -if (!(gamepadKey in presetData.mapping)) continue; -let mappedKey = presetData.mapping[gamepadKey]; -$content.append(CE("span", { class: "bx-prompt" }, getGamepadPrompt(gamepadKey) + " > " + (mappedKey === !1 ? "🚫" : getGamepadPrompt(mappedKey)))); -} -showNote = !0; -} -let key; -for (key in presetData.settings) { -if (key === "vibrationIntensity") continue; -let value = presetData.settings[key]; -if (Array.isArray(value) && (value[0] !== 0 || value[1] !== 100)) { -showNote = !0; -break; -} -} -let fragment = document.createDocumentFragment(); -if (showNote) { -let $note = CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("controller-customization-input-latency-note")); -fragment.appendChild($note); -} -if ($content) fragment.appendChild($content); -return fragment.childElementCount ? fragment : null; -} -} -class ControllerExtraSettings extends HTMLElement { -currentControllerId; -controllerIds; -$selectControllers; -$selectShortcuts; -$selectCustomization; -$summaryCustomization; -updateLayout; -switchController; -getCurrentControllerId; -saveSettings; -updateCustomizationSummary; -setValue; -static renderSettings() { -let $container = CE("label", { -class: "bx-settings-row bx-controller-extra-settings" -}); -$container.prefKey = "controller.settings", $container.addEventListener("contextmenu", this.boundOnContextMenu), this.settingsManager.setElement("controller.settings", $container), $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container), $container.setValue = ControllerExtraSettings.setValue.bind($container); -let $selectControllers = BxSelectElement.create(CE("select", { -class: "bx-full-width", -autocomplete: "off", -_on: { -input: (e) => { -$container.switchController($selectControllers.value); -} -} -})), $selectShortcuts = BxSelectElement.create(CE("select", { -autocomplete: "off", -_on: { input: $container.saveSettings } -})), $selectCustomization = BxSelectElement.create(CE("select", { -autocomplete: "off", -_on: { -input: async () => { -ControllerExtraSettings.updateCustomizationSummary.call($container), $container.saveSettings(); -} -} -})), $rowCustomization = createSettingRow(t("in-game-controller-customization"), CE("div", { -class: "bx-preset-row", -_nearby: { orientation: "horizontal" } -}, $selectCustomization, createButton({ -title: t("manage"), -icon: BxIcon.MANAGE, -style: 64 | 1 | 512, -onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({ -id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null -}) -})), { multiLines: !0 }); -return $rowCustomization.appendChild($container.$summaryCustomization = CE("div")), $container.append(CE("span", !1, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", { -class: "bx-preset-row", -_nearby: { orientation: "horizontal" } -}, $selectShortcuts, createButton({ -title: t("manage"), -icon: BxIcon.MANAGE, -style: 64 | 1 | 512, -onClick: () => ControllerShortcutsManagerDialog.getInstance().show({ -id: parseInt($container.$selectShortcuts.value) -}) -})), { multiLines: !0 }), $rowCustomization))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => { -$container.updateLayout(); -}), $container; -} -static async updateCustomizationSummary() { -let presetId = parseInt(this.$selectCustomization.value), $summaryContent = await ControllerCustomizationsManagerDialog.getInstance().renderSummary(presetId); -if (removeChildElements(this.$summaryCustomization), $summaryContent) this.$summaryCustomization.appendChild($summaryContent); -} -static async updateLayout() { -if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return; -let $fragment = document.createDocumentFragment(); -removeChildElements(this.$selectControllers); -for (let name of this.controllerIds) { -let $option = CE("option", { value: name }, simplifyGamepadName(name)); -$fragment.appendChild($option); -} -this.$selectControllers.appendChild($fragment); -let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets(); -renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 }); -let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets(); -renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 }); -for (let name of this.controllerIds) { -let $option = CE("option", { value: name }, name); -$fragment.appendChild($option); -} -BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this); -} -static async switchController(id) { -if (this.currentControllerId = id, !this.getCurrentControllerId()) return; -let controllerSetting = STORAGE.Stream.getControllerSetting(this.currentControllerId); -ControllerExtraSettings.updateElements.call(this, controllerSetting); -} -static getCurrentControllerId() { -if (this.currentControllerId) { -if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId; -this.currentControllerId = ""; -} -if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0]; -if (this.currentControllerId) return this.currentControllerId; -return null; -} -static async saveSettings() { -if (!this.getCurrentControllerId()) return; -let controllerSettings = getStreamPref("controller.settings"); -controllerSettings[this.currentControllerId] = { -shortcutPresetId: parseInt(this.$selectShortcuts.value), -customizationPresetId: parseInt(this.$selectCustomization.value) -}, setStreamPref("controller.settings", controllerSettings, "ui"), StreamSettings.refreshControllerSettings(); -} -static setValue(value) { -ControllerExtraSettings.updateElements.call(this, value[this.currentControllerId]); -} -static updateElements(controllerSetting) { -if (!controllerSetting) return; -this.$selectShortcuts.value = controllerSetting.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSetting.customizationPresetId.toString(), ControllerExtraSettings.updateCustomizationSummary.call(this); -} -} -class SuggestionsSetting { -static async renderSuggestions(e) { -let $btnSuggest = e.target.closest("div"); -$btnSuggest.toggleAttribute("bx-open"); -let $content = $btnSuggest.nextElementSibling; -if ($content) { -BxEvent.dispatch($content.querySelector("select"), "input"); -return; -} -let settingTabGroup; -for (settingTabGroup in this.SETTINGS_UI) { -let settingTab = this.SETTINGS_UI[settingTabGroup]; -if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue; -for (let settingTabContent of settingTab.items) { -if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue; -for (let setting of settingTabContent.items) { -let prefKey; -if (typeof setting === "string") prefKey = setting; -else if (typeof setting === "object") prefKey = setting.pref; -if (prefKey) this.settingLabels[prefKey] = settingTabContent.label; -} -} -} -let recommendedDevice = ""; -if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { -if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo); -} -let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; -if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on"); -else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto"); -else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"); -SuggestionsSetting.generateDefaultSuggestedSettings.call(this); -let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality"))); -$select.addEventListener("input", (e2) => { -let profile = $select.value; -removeChildElements($suggestedSettings); -let fragment = document.createDocumentFragment(), note; -if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice }); -else if (profile === "highest") note = "⚠️ " + t("highest-quality-note"); -note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note)); -let settings = this.suggestedSettings[profile]; -for (let key in settings) { -let { storage, definition } = getPrefInfo(key), prefKey; -if (storage === STORAGE.Stream) prefKey = key; -else prefKey = key; -let suggestedValue; -if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]); -else suggestedValue = settings[prefKey]; -let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value; -if (isSameValue) $value = currentValueText; -else { -let suggestedValueText = storage.getValueText(prefKey, suggestedValue); -$value = currentValueText + " ➔ " + suggestedValueText; -} -let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`); -if ($child = CE("div", { -class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}` -}, $checkbox = CE("input", { -type: "checkbox", -tabindex: 0, -checked: !0, -id -}), CE("label", { -for: id -}, CE("div", { -class: "bx-suggest-label" -}, breadcrumb), CE("div", { -class: "bx-suggest-value" -}, $value))), isSameValue) -$checkbox.disabled = !0, $checkbox.checked = !0; -fragment.appendChild($child); -} -$suggestedSettings.appendChild(fragment); -}), BxEvent.dispatch($select, "input"); -let onClickApply = () => { -let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance(); -for (prefKey in settings) { -let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`); -if (!$checkBox.checked || $checkBox.disabled) continue; -let $control = settingsManager.getElement(prefKey); -if (!$control) { -setPref(prefKey, suggestedValue, "direct"); -continue; -} -let { definition: settingDefinition } = getPrefInfo(prefKey); -if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue); -if ("setValue" in $control) $control.setValue(suggestedValue); -else $control.value = suggestedValue; -BxEvent.dispatch($control, "input", { -manualTrigger: !0 -}); -} -BxEvent.dispatch($select, "input"); -}, $btnApply = createButton({ -label: t("apply"), -style: 128 | 64, -onClick: onClickApply -}); -$content = CE("div", { -class: "bx-sub-content-box bx-suggest-box", -_nearby: { -orientation: "vertical" -} -}, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", { -class: "bx-suggest-link bx-focusable", -href: "https://better-xcloud.github.io/guide/android-webview-tweaks/", -target: "_blank", -tabindex: 0 -}, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", { -class: "bx-suggest-link bx-focusable", -href: "https://github.com/redphx/better-xcloud-devices", -target: "_blank", -tabindex: 0 -}, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content); -} -static async getRecommendedSettings(androidInfo) { -function normalize(str) { -return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-"); -} -try { -let { brand, board, model } = androidInfo; -brand = normalize(brand), board = normalize(board), model = normalize(model); -let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {}; -if (json.schema_version !== 2) return null; -let scriptSettings = json.settings.script; -if (scriptSettings._base) { -let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base; -for (let profile of base) -Object.assign(recommended, this.suggestedSettings[profile]); -delete scriptSettings._base; -} -let key; -for (key in scriptSettings) -recommended[key] = scriptSettings[key]; -return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name; -} catch (e) {} -return null; -} -static addDefaultSuggestedSetting(prefKey, value) { -let key; -for (key in this.suggestedSettings) -if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; -} -static generateDefaultSuggestedSettings() { -let key; -for (key in this.suggestedSettings) { -if (key === "default") continue; -let prefKey; -for (prefKey in this.suggestedSettings[key]) -if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default; -} -} -} -class BxKeyBindingButton extends HTMLButtonElement { -title; -isPrompt = !1; -allowedFlags; -keyInfo = null; -bindKey; -unbindKey; -static create(options) { -let $btn = CE("button", { -class: "bx-binding-button bx-focusable", -type: "button" -}); -return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn; -} -static onClick(e) { -KeyBindingDialog.getInstance().show({ -$elm: this -}); -} -static onContextMenu = (e) => { -e.preventDefault(); -let $btn = e.target; -if (!$btn.disabled) $btn.unbindKey.apply($btn); -}; -static bindKey(key, force = !1) { -if (!key) return; -if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) { -if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change"); -} -} -static unbindKey(force = !1) { -this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change"); -} -constructor() { -super(); -} -} -class KeyBindingDialog { -static instance; -static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog); -$dialog; -$wait; -$title; -$inputList; -$overlay; -$currentElm; -countdownIntervalId; -constructor() { -this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", !1, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", !1, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", !1, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); -} -show(options) { -this.$currentElm = options.$elm, this.addEventListeners(); -let allowedFlags = this.$currentElm.allowedFlags; -this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown(); -} -startCountdown() { -this.stopCountdown(); -let count = 9; -this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => { -if (count -= 1, count === 0) { -this.stopCountdown(), this.hide(); -return; -} -this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`; -}, 1000); -} -stopCountdown() { -this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null; -} -hide = () => { -this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"); -}; -addEventListeners() { -let allowedFlags = this.$currentElm.allowedFlags; -if (allowedFlags.includes(1)) window.addEventListener("keyup", this); -if (allowedFlags.includes(4)) window.addEventListener("mousedown", this); -if (allowedFlags.includes(8)) window.addEventListener("wheel", this); -} -clearEventListeners() { -window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this); -} -handleEvent(e) { -let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1; -switch (e.type) { -case "wheel": -if (handled = !0, allowedFlags.includes(8)) valid = !0; -break; -case "mousedown": -if (handled = !0, allowedFlags.includes(4)) valid = !0; -break; -case "keyup": -if (handled = !0, allowedFlags.includes(1)) { -let keyboardEvent = e; -if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) { -let key = keyboardEvent.key; -valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid; -} -} -break; -} -if (handled) { -if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown(); -else this.startCountdown(); -window.setTimeout(this.hide, 200); -} -} -} -class MkbMappingManagerDialog extends BaseProfileManagerDialog { -static instance; -static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller"))); -KEYS_PER_BUTTON = 2; -BUTTONS_ORDER = [ -16, -12, -13, -14, -15, -0, -1, -2, -3, -4, -5, -6, -7, -8, -9, -10, -100, -101, -102, -103, -11, -200, -201, -202, -203 -]; -allKeyElements = []; -$mouseMapTo; -$mouseSensitivityX; -$mouseSensitivityY; -$mouseDeadzone; -$unbindNote; -constructor(title) { -super(title, MkbMappingPresetsTable.getInstance()); -this.render(); -} -onBindingKey = (e) => { -if (e.target.disabled) return; -if (e.button !== 0) return; -}; -parseDataset($btn) { -let dataset = $btn.dataset; -return { -keySlot: parseInt(dataset.keySlot), -buttonIndex: parseInt(dataset.buttonIndex) -}; -} -onKeyChanged = (e) => { -let $current = e.target, keyInfo = $current.keyInfo; -if (keyInfo) { -for (let $elm of this.allKeyElements) -if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0); -} -this.savePreset(); -}; -render() { -let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))); -for (let buttonIndex of this.BUTTONS_ORDER) { -let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment(); -for (let i = 0;i < this.KEYS_PER_BUTTON; i++) -$elm = BxKeyBindingButton.create({ -title: buttonPrompt, -isPrompt: !0, -allowedFlags: [1, 4, 8], -onChanged: this.onKeyChanged -}), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm); -let $keyRow = CE("div", { -class: "bx-mkb-key-row", -_nearby: { orientation: "horizontal" } -}, CE("label", { title: buttonName }, buttonPrompt), $fragment); -$rows.appendChild($keyRow); -} -let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, { -suffix: "%", -exactTicks: 50 -}, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, { -suffix: "%", -exactTicks: 50 -}, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, { -suffix: "%", -exactTicks: 10 -}, savePreset))); -this.$content = CE("div", !1, $rows, $extraSettings); -} -switchPreset(id) { -let preset = this.allPresets.data[id]; -if (!preset) { -this.currentPresetId = 0; -return; -} -let presetData = preset.data; -this.currentPresetId = id; -let isDefaultPreset = id <= 0; -this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset); -for (let $elm of this.allKeyElements) { -let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex]; -if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({ -code: buttonKeys[keySlot] -}, !0); -else $elm.unbindKey(!0); -$elm.disabled = isDefaultPreset; -} -let mouse = presetData.mouse; -this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString(); -} -savePreset() { -let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA); -for (let $elm of this.allKeyElements) { -let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping; -if (!mapping[buttonIndex]) mapping[buttonIndex] = []; -if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot]; -else mapping[buttonIndex][keySlot] = $elm.keyInfo.code; -} -let mouse = presetData.mouse; -mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value); -let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { -id: this.currentPresetId, -name: oldPreset.name, -data: presetData -}; -this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; -} -onBeforeUnmount() { -StreamSettings.refreshMkbSettings(), super.onBeforeUnmount(); -} -} -class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog { -static instance; -static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts"))); -$content; -$unbindNote; -allKeyElements = []; -constructor(title) { -super(title, KeyboardShortcutsTable.getInstance()); -let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" }); -for (let groupLabel in SHORTCUT_ACTIONS) { -let items = SHORTCUT_ACTIONS[groupLabel]; -if (!items) continue; -let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel)); -for (let action in items) { -let crumbs = items[action]; -if (!crumbs) continue; -let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({ -title: label, -isPrompt: !1, -onChanged: this.onKeyChanged, -allowedFlags: [1, 2] -}); -$btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn); -let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn)); -$fieldSet.appendChild($row); -} -if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet); -} -this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows); -} -onKeyChanged = (e) => { -let $current = e.target, keyInfo = $current.keyInfo; -if (keyInfo) for (let $elm of this.allKeyElements) { -if ($elm === $current) continue; -if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0); -} -this.savePreset(); -}; -parseDataset($btn) { -return { -action: $btn.dataset.action -}; -} -switchPreset(id) { -let preset = this.allPresets.data[id]; -if (!preset) { -this.currentPresetId = 0; -return; -} -let presetData = preset.data; -this.currentPresetId = id; -let isDefaultPreset = id <= 0; -this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset); -for (let $elm of this.allKeyElements) { -let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action]; -if (keyInfo) $elm.bindKey(keyInfo, !0); -else $elm.unbindKey(!0); -$elm.disabled = isDefaultPreset; -} -} -savePreset() { -let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA); -for (let $elm of this.allKeyElements) { -let { action } = this.parseDataset($elm), mapping = presetData.mapping; -if ($elm.keyInfo) mapping[action] = $elm.keyInfo; -} -let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = { -id: this.currentPresetId, -name: oldPreset.name, -data: presetData -}; -this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset; -} -onBeforeUnmount() { -StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount(); -} -} -class MkbExtraSettings extends HTMLElement { -$mappingPresets; -$shortcutsPresets; -updateLayout; -saveMkbSettings; -saveShortcutsSettings; -static renderSettings() { -let $container = document.createDocumentFragment(); -$container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container); -let $mappingPresets = BxSelectElement.create(CE("select", { -autocomplete: "off", -_on: { -input: $container.saveMkbSettings -} -})), $shortcutsPresets = BxSelectElement.create(CE("select", { -autocomplete: "off", -_on: { -input: $container.saveShortcutsSettings -} -})); -return $container.append(...getGlobalPref("mkb.enabled") ? [ -createSettingRow(t("virtual-controller"), CE("div", { -class: "bx-preset-row", -_nearby: { -orientation: "horizontal" -} -}, $mappingPresets, createButton({ -title: t("manage"), -icon: BxIcon.MANAGE, -style: 64 | 1 | 512, -onClick: () => MkbMappingManagerDialog.getInstance().show({ -id: parseInt($container.$mappingPresets.value) -}) -})), { -multiLines: !0, -onContextMenu: this.boundOnContextMenu, -pref: "mkb.p1.preset.mappingId" -}), -createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), { -onContextMenu: this.boundOnContextMenu, -pref: "mkb.p1.slot" -}) -] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", { -class: "bx-preset-row", -_nearby: { -orientation: "horizontal" -} -}, $shortcutsPresets, createButton({ -title: t("manage"), -icon: BxIcon.MANAGE, -style: 64 | 1 | 512, -onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({ -id: parseInt($container.$shortcutsPresets.value) -}) -})), { -multiLines: !0, -onContextMenu: this.boundOnContextMenu, -pref: "keyboardShortcuts.preset.inGameId" -})), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => { -$container.updateLayout(); -}), $container; -} -static async updateLayout() { -let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets(); -renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId")); -let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets(); -renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 }); -} -static async saveMkbSettings() { -let presetId = parseInt(this.$mappingPresets.value); -setStreamPref("mkb.p1.preset.mappingId", presetId, "ui"); -} -static async saveShortcutsSettings() { -let presetId = parseInt(this.$shortcutsPresets.value); -setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui"); -} -} -class SettingsDialog extends NavigationDialog { -static instance; -static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog); -LOG_TAG = "SettingsNavigationDialog"; -$container; -$tabs; -$tabContents; -$btnReload; -$btnGlobalReload; -$noteGlobalReload; -$btnSuggestion; -$streamSettingsSelection; -renderFullSettings; -boundOnContextMenu; -suggestedSettings = { -recommended: {}, -default: {}, -lowest: {}, -highest: {} -}; -settingLabels = {}; -settingsManager; -TAB_GLOBAL_ITEMS = [{ -group: "general", -label: t("better-xcloud"), -helpUrl: "https://better-xcloud.github.io/features/", -items: [ -($parent) => { -let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = []; -if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { -let opts = { -label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), -style: 1 | 64 | 128 -}; -if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); -else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; -topButtons.push(createButton(opts)); -} -if (AppInterface) topButtons.push(createButton({ -label: t("app-settings"), -icon: BxIcon.STREAM_SETTINGS, -style: 128 | 64, -onClick: (e) => { -AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); -} -})); -else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ -label: "🔥 " + t("install-android"), -style: 128 | 64, -url: "https://better-xcloud.github.io/android" -})); -this.$btnGlobalReload = createButton({ -label: t("settings-reload"), -classes: ["bx-settings-reload-button", "bx-gone"], -style: 64 | 128, -onClick: (e) => { -this.reloadPage(); -} -}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", { -class: "bx-settings-reload-note" -}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", { -class: "bx-suggest-toggler bx-focusable", -tabindex: 0 -}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); -let $div = CE("div", { -class: "bx-top-buttons", -_nearby: { -orientation: "vertical" -} -}, ...topButtons); -$parent.appendChild($div); -}, -{ -pref: "bx.locale", -multiLines: !0 -}, -"server.bypassRestriction", -"ui.controllerFriendly", -"xhome.enabled" -] -}, { -group: "server", -label: t("server"), -items: [ -{ -pref: "server.region", -multiLines: !0 -}, -{ -pref: "stream.locale", -multiLines: !0 -}, -"server.ipv6.prefer" -] -}, { -group: "stream", -label: t("stream"), -items: [ -"stream.video.resolution", -"stream.video.codecProfile", -"stream.video.maxBitrate", -"audio.volume.booster.enabled", -"screenshot.applyFilters", -"audio.mic.onPlaying", -"game.fortnite.forceConsole", -"stream.video.combineAudio" -] -}, { -requiredVariants: "full", -group: "mkb", -label: t("mouse-and-keyboard"), -items: [ -"nativeMkb.mode", -{ -pref: "nativeMkb.forcedGames", -multiLines: !0, -note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list")) -}, -"mkb.enabled", -"mkb.cursor.hideIdle" -], -...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? { -unsupported: !0, -unsupportedNote: CE("a", { -href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", -target: "_blank" -}, "⚠️ " + t("browser-unsupported-feature")) -} : {} -}, { -requiredVariants: "full", -group: "touch-control", -label: t("touch-controller"), -items: [ -{ -pref: "touchController.mode", -note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list")) -}, -"touchController.autoOff", -"touchController.opacity.default", -"touchController.style.standard", -"touchController.style.custom" -], -...!STATES.userAgent.capabilities.touch ? { -unsupported: !0, -unsupportedNote: "⚠️ " + t("device-unsupported-touch") -} : {} -}, { -group: "ui", -label: t("ui"), -items: [ -"ui.layout", -"ui.imageQuality", -"ui.gameCard.waitTime.show", -"ui.controllerStatus.show", -"ui.streamMenu.simplify", -"ui.splashVideo.skip", -!AppInterface && "ui.hideScrollbar", -"ui.systemMenu.hideHandle", -"ui.feedbackDialog.disabled", -"ui.reduceAnimations", -{ -pref: "ui.hideSections", -multiLines: !0 -}, -{ -pref: "block.features", -multiLines: !0 -} -] -}, { -requiredVariants: "full", -group: "game-bar", -label: t("game-bar"), -items: [ -"gameBar.position" -] -}, { -group: "loading-screen", -label: t("loading-screen"), -items: [ -"loadingScreen.gameArt.show", -"loadingScreen.waitTime.show", -"loadingScreen.rocket" -] -}, { -group: "other", -label: t("other"), -items: [ -"block.tracking" -] -}, { -group: "advanced", -label: t("advanced"), -items: [ -{ -pref: "userAgent.profile", -multiLines: !0, -onCreated: (setting, $control) => { -let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { -type: "text", -placeholder: defaultUserAgent, -autocomplete: "off", -class: "bx-settings-custom-user-agent", -tabindex: 0 -}); -$inpCustomUserAgent.addEventListener("input", (e) => { -let profile = $control.value, custom = e.target.value.trim(); -UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e); -}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, { -orientation: "vertical" -}); -} -} -] -}, { -group: "footer", -items: [ -($parent) => { -try { -let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10); -$parent.appendChild(CE("div", { -class: "bx-settings-app-version" -}, `xCloud website version ${appVersion} (${appDate})`)); -} catch (e) {} -}, -($parent) => { -$parent.appendChild(CE("a", { -class: "bx-donation-link", -href: "https://ko-fi.com/redphx", -target: "_blank", -tabindex: 0 -}, `❤️ ${t("support-better-xcloud")}`)); -}, -($parent) => { -$parent.appendChild(createButton({ -label: t("clear-data"), -style: 8 | 128 | 64, -onClick: (e) => { -if (confirm(t("clear-data-confirm"))) clearAllData(); -} -})); -}, -($parent) => { -$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({ -label: "Debug info", -style: 8 | 128 | 64, -onClick: (e) => { -let $button = e.target.closest("button"); -if (!$button) return; -let $pre = $button.nextElementSibling; -if (!$pre) { -let debugInfo = deepClone(BX_FLAGS.DeviceInfo); -debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", { -class: "bx-focusable bx-gone", -tabindex: 0, -_on: { -click: async (e2) => { -await copyToClipboard(e2.target.innerText); -} -} -}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre); -} -$pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); -} -}))); -} -] -}]; -TAB_DISPLAY_ITEMS = [{ -requiredVariants: "full", -group: "audio", -label: t("audio"), -helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", -items: [{ -pref: "audio.volume", -params: { -disabled: !getGlobalPref("audio.volume.booster.enabled") -}, -onCreated: (setting, $elm) => { -let $range = $elm.querySelector("input[type=range"); -BxEventBus.Stream.on("setting.changed", (payload) => { -let { settingKey } = payload; -if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 }); -}); -} -}] -}, { -group: "video", -label: t("video"), -helpUrl: "https://better-xcloud.github.io/ingame-features/#video", -items: [ -"video.player.type", -"video.maxFps", -"video.player.powerPreference", -"video.processing", -"video.ratio", -"video.position", -"video.processing.sharpness", -"video.saturation", -"video.contrast", -"video.brightness" -] -}]; -TAB_CONTROLLER_ITEMS = [ -{ -group: "controller", -label: t("controller"), -helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", -items: [ -"localCoOp.enabled", -"controller.pollingRate", -($parent) => { -$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this)); -} -] -}, -STATES.userAgent.capabilities.touch && { -group: "touch-control", -label: t("touch-controller"), -items: [{ -label: t("layout"), -content: CE("select", { -disabled: !0 -}, CE("option", !1, t("default"))), -onCreated: (setting, $elm) => { -$elm.addEventListener("input", (e) => { -TouchController.applyCustomLayout($elm.value, 1000); -}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { -let customLayouts = TouchController.getCustomLayouts(); -while ($elm.firstChild) -$elm.removeChild($elm.firstChild); -if ($elm.disabled = !customLayouts, !customLayouts) { -$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input")); -return; -} -let $fragment = document.createDocumentFragment(); -for (let key in customLayouts.layouts) { -let layout = customLayouts.layouts[key], name; -if (layout.author) name = `${layout.name} (${layout.author})`; -else name = layout.name; -let $option = CE("option", { value: key }, name); -$fragment.appendChild($option); -} -$elm.appendChild($fragment), $elm.value = customLayouts.default_layout; -}); -} -}] -}, -STATES.browser.capabilities.deviceVibration && { -group: "device", -label: t("device"), -items: [{ -pref: "deviceVibration.mode", -multiLines: !0, -unsupported: !STATES.browser.capabilities.deviceVibration -}, { -pref: "deviceVibration.intensity", -unsupported: !STATES.browser.capabilities.deviceVibration -}] -} -]; -TAB_MKB_ITEMS = [ -{ -requiredVariants: "full", -group: "mkb", -label: t("mouse-and-keyboard"), -helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", -items: [ -($parent) => { -$parent.appendChild(MkbExtraSettings.renderSettings.apply(this)); -} -] -}, -NativeMkbHandler.isAllowed() && { -requiredVariants: "full", -group: "native-mkb", -label: t("native-mkb"), -items: [ -"nativeMkb.scroll.sensitivityY", -"nativeMkb.scroll.sensitivityX" -] -} -]; -TAB_STATS_ITEMS = [{ -group: "stats", -label: t("stream-stats"), -helpUrl: "https://better-xcloud.github.io/stream-stats/", -items: [ -"stats.showWhenPlaying", -"stats.quickGlance.enabled", -"stats.items", -"stats.position", -"stats.textSize", -"stats.opacity.all", -"stats.opacity.background", -"stats.colors" -] -}]; -SETTINGS_UI = { -global: { -group: "global", -icon: BxIcon.HOME, -items: this.TAB_GLOBAL_ITEMS -}, -stream: { -group: "stream", -icon: BxIcon.DISPLAY, -items: this.TAB_DISPLAY_ITEMS -}, -controller: { -group: "controller", -icon: BxIcon.CONTROLLER, -items: this.TAB_CONTROLLER_ITEMS, -requiredVariants: "full" -}, -mkb: { -group: "mkb", -icon: BxIcon.NATIVE_MKB, -items: this.TAB_MKB_ITEMS, -requiredVariants: "full" -}, -stats: { -group: "stats", -icon: BxIcon.STREAM_STATS, -items: this.TAB_STATS_ITEMS -} -}; -constructor() { -super(); -BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => { -if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); -let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`); -if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; -}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => { -this.$tabContents.dataset.gameId = id.toString(); -}); -} -getDialog() { -return this; -} -getContent() { -return this.$container; -} -onMounted() { -super.onMounted(); -} -isOverlayVisible() { -return !STATES.isPlaying; -} -reloadPage() { -this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); -} -isSupportedVariant(requiredVariants) { -if (typeof requiredVariants === "undefined") return !0; -return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); -} -onTabClicked = (e) => { -let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children); -for ($child of children) -if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child); -else if ($child.dataset.tabGroup) $child.classList.add("bx-gone"); -this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global"); -for (let $child2 of Array.from(this.$tabs.children)) -$child2.classList.remove("bx-active"); -$svg.classList.add("bx-active"); -}; -renderTab(settingTab) { -let $svg = createSvgIcon(settingTab.icon); -return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg; -} -onGlobalSettingChanged = (e) => { -PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); -}; -onContextMenu(e) { -e.preventDefault(); -let $elm = e.target; -$elm instanceof HTMLElement && this.resetHighlightedSetting($elm); -} -renderServerSetting(setting) { -let selectedValue = getGlobalPref("server.region"), continents = { -"america-north": { -label: t("continent-north-america") -}, -"america-south": { -label: t("continent-south-america") -}, -asia: { -label: t("continent-asia") -}, -australia: { -label: t("continent-australia") -}, -europe: { -label: t("continent-europe") -}, -other: { -label: t("other") -} -}, $control = CE("select", { -id: `bx_setting_${escapeCssSelector(setting.pref)}`, -tabindex: 0 -}); -$control.name = $control.id, $control.addEventListener("input", (e) => { -setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e); -}), setting.options = {}; -for (let regionName in STATES.serverRegions) { -let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`; -if (region.isDefault) { -if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; -} -setting.options[value] = label; -let $option = CE("option", { value }, label), continent = continents[region.contintent]; -if (!continent.children) continent.children = []; -continent.children.push($option); -} -let fragment = document.createDocumentFragment(), key; -for (key in continents) { -let continent = continents[key]; -if (!continent.children) continue; -fragment.appendChild(CE("optgroup", { -label: continent.label -}, ...continent.children)); -} -return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; -} -renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { -if (typeof setting === "string") setting = { -pref: setting -}; -let pref = setting.pref, $control; -if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this); -else $control = setting.content; -else if (!setting.unsupported) { -if (pref === "server.region") $control = this.renderServerSetting(setting); -else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => { -let newLocale = e.target.value; -if (getGlobalPref("ui.controllerFriendly")) { -let timeoutId = e.target.timeoutId; -timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => { -Translations.refreshLocale(newLocale), Translations.updateTranslations(); -}, 500); -} else Translations.refreshLocale(newLocale), Translations.updateTranslations(); -this.onGlobalSettingChanged(e); -}); -else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => { -let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value); -UserAgent.updateStorage(value); -let $inp = $control.nextElementSibling; -$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); -}); -else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged); -if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control); -} -let prefDefinition = null; -if (pref) prefDefinition = getPrefInfo(pref).definition; -if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; -let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental; -if (typeof note === "function") note = note(); -if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote(); -if (settingTabContent.label && setting.pref) { -if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest); -} -if (experimental) if (label = "🧪 " + label, !note) note = t("experimental"); -else note = `${t("experimental")}: ${note}`; -let $note; -if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote); -else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note); -let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, { -$note, -multiLines: setting.multiLines, -icon: prefDefinition?.labelIcon, -onContextMenu: this.boundOnContextMenu, -pref -}); -if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`; -$row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); -} -renderSettingsSection(settingTab, sections) { -let $tabContent = CE("div", { -class: "bx-gone", -_dataset: { -tabGroup: settingTab.group -} -}); -for (let section of sections) { -if (!section) continue; -if (section instanceof HTMLElement) { -$tabContent.appendChild(section); -continue; -} -if (!this.isSupportedVariant(section.requiredVariants)) continue; -if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue; -let label = section.label; -if (label === t("better-xcloud")) { -if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)"; -label = createButton({ -label, -url: "https://github.com/redphx/better-xcloud/releases", -style: 4096 | 16 | 64 -}); -} -if (label) { -let $title = CE("h2", { -_nearby: { -orientation: "horizontal" -} -}, CE("span", !1, label), section.helpUrl && createButton({ -icon: BxIcon.QUESTION, -style: 8 | 64, -url: section.helpUrl, -title: t("help") -})); -$tabContent.appendChild($title); -} -if (section.unsupportedNote) { -let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote); -$tabContent.appendChild($note); -} -if (section.unsupported) continue; -if (section.content) { -$tabContent.appendChild(section.content); -continue; -} -section.items = section.items || []; -for (let setting of section.items) { -if (setting === !1) continue; -if (typeof setting === "function") { -setting.apply(this, [$tabContent]); -continue; -} -this.renderSettingRow(settingTab, $tabContent, section, setting); -} -} -return $tabContent; -} -setupDialog() { -let $tabs, $tabContents, $container = CE("div", { -class: "bx-settings-dialog", -_nearby: { -orientation: "horizontal" -} -}, CE("div", { -class: "bx-settings-tabs-container", -_nearby: { -orientation: "vertical", -focus: () => { -return this.dialogManager.focus($tabs); -}, -loop: (direction) => { -if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0; -return !1; -} -} -}, $tabs = CE("div", { -class: "bx-settings-tabs bx-hide-scroll-bar", -_nearby: { -focus: () => this.focusActiveTab() -} -}), CE("div", !1, this.$btnReload = createButton({ -icon: BxIcon.REFRESH, -style: 64 | 32, -onClick: (e) => { -this.reloadPage(); -} -}), createButton({ -icon: BxIcon.CLOSE, -style: 64 | 32, -onClick: (e) => { -this.dialogManager.hide(); -} -}))), CE("div", { -class: "bx-settings-tab-contents", -_nearby: { -orientation: "vertical", -loop: (direction) => { -if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; -return !1; -} -} -}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", { -class: "bx-settings-tab-content", -_nearby: { -orientation: "vertical", -focus: () => this.jumpToSettingGroup("next") -} -}))); -this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => { -if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); -}); -let settingTabGroup; -for (settingTabGroup in this.SETTINGS_UI) { -let settingTab = this.SETTINGS_UI[settingTabGroup]; -if (!settingTab) continue; -if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; -if (settingTab.group !== "global" && !this.renderFullSettings) continue; -let $svg = this.renderTab(settingTab); -$tabs.appendChild($svg); -let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items); -$tabContents.appendChild($tabContent); -} -$tabs.firstElementChild.dispatchEvent(new Event("click")); -} -focusTab(tabId) { -let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); -$tab && $tab.dispatchEvent(new Event("click")); -} -focusIfNeeded() { -this.jumpToSettingGroup("next"); -} -focusActiveTab() { -let $currentTab = this.$tabs.querySelector(".bx-active"); -return $currentTab && $currentTab.focus(), !0; -} -focusVisibleSetting(type = "first") { -let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *")); -if (!controls.length) return !1; -if (type === "last") controls.reverse(); -for (let $control of controls) { -if (!($control instanceof HTMLElement)) continue; -let $focusable = this.dialogManager.findFocusableElement($control); -if ($focusable) { -if (this.dialogManager.focus($focusable)) return !0; -} -} -return !1; -} -focusVisibleTab(type = "first") { -let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)")); -if (!tabs.length) return !1; -if (type === "last") tabs.reverse(); -for (let $tab of tabs) -if (this.dialogManager.focus($tab)) return !0; -return !1; -} -jumpToSettingGroup(direction) { -let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)"); -if (!$tabContent) return !1; -let $header, $focusing = document.activeElement; -if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); -else { -let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0; -while (!0) { -if (!$tmp) break; -if ($tmp.tagName === "H2") { -if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) { -if (++times, direction === "next" || times >= 2) break; -} -} -$tmp = $tmp[siblingProperty]; -} -} -let $target; -if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1); -if ($target) return this.dialogManager.focus($target); -return !1; -} -resetHighlightedSetting($elm) { -let targetGameId = SettingsManager.getInstance().getTargetGameId(); -if (targetGameId < 0) return; -if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0; -let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row"); -if (!$row) return; -let pref = $row.prefKey; -if (!pref) alert("Pref not found: " + $row.id); -if (!isStreamPref(pref)) return; -let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref); -if (deleted) BxEventBus.Stream.emit("setting.changed", { -storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`, -settingKey: pref -}); -return deleted; -} -handleKeyPress(key) { -let handled = !0; -switch (key) { -case "Tab": -this.focusActiveTab(); -break; -case "Home": -this.focusVisibleSetting("first"); -break; -case "End": -this.focusVisibleSetting("last"); -break; -case "PageUp": -this.jumpToSettingGroup("previous"); -break; -case "PageDown": -this.jumpToSettingGroup("next"); -break; -case "KeyQ": -this.resetHighlightedSetting(); -break; -default: -handled = !1; -break; -} -return handled; -} -handleGamepad(button) { -let handled = !0; -switch (button) { -case 1: -let $focusing = document.activeElement; -if ($focusing && this.$tabs.contains($focusing)) this.hide(); -else this.focusActiveTab(); -break; -case 4: -case 5: -this.focusActiveTab(); -break; -case 6: -this.jumpToSettingGroup("previous"); -break; -case 7: -this.jumpToSettingGroup("next"); -break; -case 2: -this.resetHighlightedSetting(); -break; -default: -handled = !1; -break; -} -return handled; -} -} -class ScreenshotManager { -static instance; -static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager); -LOG_TAG = "ScreenshotManager"; -$download; -$canvas; -canvasContext; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", { -alpha: !1, -willReadFrequently: !1 -}); -} -updateCanvasSize(width, height) { -this.$canvas.width = width, this.$canvas.height = height; -} -updateCanvasFilters(filters) { -this.canvasContext.filter = filters; -} -onAnimationEnd(e) { -e.target.classList.remove("bx-taking-screenshot"); -} -takeScreenshot(callback) { -let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas; -if (!streamPlayer || !$canvas) return; -let $player; -if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement(); -else $player = streamPlayer.getPlayerElement("default"); -if (!$player || !$player.isConnected) return; -let $gameStream = $player.closest("#game-stream"); -if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot"); -let canvasContext = this.canvasContext; -if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame(); -if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { -let data = $canvas.toDataURL("image/png").split(";base64,")[1]; -AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); -return; -} -$canvas.toBlob((blob) => { -if (!blob) return; -let now = +new Date, $download = this.$download; -$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); -}, "image/png"); -} -} -class RendererShortcut { -static toggleVisibility() { -let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); -if (!$mediaContainer) { -BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 }); -return; -} -$mediaContainer.classList.toggle("bx-gone"); -let isVisible = !$mediaContainer.classList.contains("bx-gone"); -limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible }); -} -} -class TrueAchievements { -static instance; -static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements); -LOG_TAG = "TrueAchievements"; -$link; -$button; -$hiddenLink; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({ -label: t("true-achievements"), -url: "#", -icon: BxIcon.TRUE_ACHIEVEMENTS, -style: 64 | 8 | 128 | 8192, -onClick: this.onClick -}), this.$button = createButton({ -label: t("true-achievements"), -title: t("true-achievements"), -icon: BxIcon.TRUE_ACHIEVEMENTS, -style: 64, -onClick: this.onClick -}), this.$hiddenLink = CE("a", { -target: "_blank" -}); -} -onClick = (e) => { -e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll(); -let dataset = this.$link.dataset; -this.open(!0, dataset.xboxTitleId, dataset.id); -}; -updateIds(xboxTitleId, id) { -let $link = this.$link, $button = this.$button; -if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; -if (id) $link.dataset.id = id, $button.dataset.id = id; -} -injectAchievementsProgress($elm) { -if (SCRIPT_VARIANT !== "full") return; -let $parent = $elm.parentElement, $div = CE("div", { -class: "bx-guide-home-achievements-progress" -}, $elm), xboxTitleId; -try { -let $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); -if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; -} catch (e) {} -if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId(); -if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); -if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link); -else $div.appendChild(this.$button); -$parent.appendChild($div); -} -injectAchievementDetailPage($parent) { -if (SCRIPT_VARIANT !== "full") return; -let props = getReactProps($parent); -if (!props) return; -try { -let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId; -for (let achiev of achievementList) -if (achiev.name === achievementName) { -id = achiev.id, xboxTitleId = achiev.title.id; -break; -} -if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link); -} catch (e) {} -} -getStreamXboxTitleId() { -return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; -} -open(override, xboxTitleId, id) { -if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId(); -if (AppInterface && AppInterface.openTrueAchievementsLink) { -AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString()); -return; -} -let url = "https://www.trueachievements.com"; -if (xboxTitleId) { -if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`; -} -this.$hiddenLink.href = url, this.$hiddenLink.click(); -} -} -class VirtualControllerShortcut { -static pressXboxButton() { -let streamSession = window.BX_EXPOSED.streamSession; -if (!streamSession) return; -let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, { -Nexus: 1, -VirtualPhysicality: 1024 -}); -streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => { -streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]); -}, 100); -} -} -class ShortcutHandler { -static runAction(action) { -switch (action) { -case "bx.settings.show": -SettingsDialog.getInstance().show(); -break; -case "stream.screenshot.capture": -ScreenshotManager.getInstance().takeScreenshot(); -break; -case "stream.video.toggle": -RendererShortcut.toggleVisibility(); -break; -case "stream.stats.toggle": -StreamStats.getInstance().toggle(); -break; -case "stream.microphone.toggle": -MicrophoneShortcut.toggle(); -break; -case "stream.menu.show": -StreamUiShortcut.showHideStreamMenu(); -break; -case "stream.sound.toggle": -SoundShortcut.muteUnmute(); -break; -case "stream.volume.inc": -SoundShortcut.adjustGainNodeVolume(10); -break; -case "stream.volume.dec": -SoundShortcut.adjustGainNodeVolume(-10); -break; -case "device.brightness.inc": -case "device.brightness.dec": -case "device.sound.toggle": -case "device.volume.inc": -case "device.volume.dec": -AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action); -break; -case "mkb.toggle": -if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle(); -else EmulatedMkbHandler.getInstance()?.toggle(); -break; -case "ta.open": -TrueAchievements.getInstance().open(!1); -break; -case "controller.xbox.press": -VirtualControllerShortcut.pressXboxButton(); -break; -} -} -} -class ControllerShortcut { -static buttonsCache = {}; -static buttonsStatus = {}; -static reset(index) { -ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; -} -static handle(gamepad) { -let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id]; -if (!controllerSettings) return !1; -let actions = controllerSettings.shortcuts; -if (!actions) return !1; -let gamepadIndex = gamepad.index; -ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; -let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button; -for ([index, button] of entries) -if (button.pressed && index !== 16) { -if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) { -let idx = index; -setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0); -} -} -return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; -} -} -var FeatureGates = { -PwaPrompt: !1, -EnableWifiWarnings: !1, -EnableUpdateRequiredPage: !1, -ShowForcedUpdateScreen: !1, -EnableTakControlResizing: !0 -}, nativeMkbMode = getGlobalPref("nativeMkb.mode"); +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;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, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS);}if (modified) {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();}} +class PatcherCache {static instance;static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache);KEY_CACHE = "BetterXcloud.Patches.Cache";KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature";CACHE;constructor() {this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE);let pathName = window.location.pathname;if (pathName.includes("/play/launch/")) Patcher.patchPage("stream");else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail");else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home");PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0));}getSignature() {let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]');if ($link) {let match = /\/client\.([^\.]+)\.js/.exec($link.href);match && (webVersion = match[1]);} else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? "";return hashCode(scriptVersion + webVersion + patches);}clear() {window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {};}checkSignature() {let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature();if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear();else BxLogger.info(LOG_TAG2, "Signature unchanged");}cleanupPatches(patches) {return patches.filter((item2) => {for (let id in this.CACHE)if (this.CACHE[id].includes(item2)) return !1;return !0;});}getPatches(id) {return this.CACHE[id];}saveToCache(subCache) {for (let id in subCache) {let patchNames = subCache[id], data = this.CACHE[id];if (!data) this.CACHE[id] = patchNames;else for (let patchName of patchNames)if (!data.includes(patchName)) data.push(patchName);}window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));}} +class FullscreenText {static instance;static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);LOG_TAG = "FullscreenText";$text;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {class: "bx-fullscreen-text bx-gone"}), document.documentElement.appendChild(this.$text);}show(msg) {document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg;}hide() {document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone");}} +class BaseProfileManagerDialog extends NavigationDialog {$container;title;presetsDb;allPresets;currentPresetId = null;activatedPresetId = null;$presets;$header;$defaultNote;$content;$btnRename;$btnDelete;constructor(title, presetsDb) {super();this.title = title, this.presetsDb = presetsDb;}async renderSummary(presetId) {return null;}updateButtonStates() {let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0;this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset);}async renderPresetsList() {if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0];renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 });}promptNewName(action, value = "") {let newName = "";while (!newName) {if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1;newName = newName.trim();}return newName ? newName : !1;}async renderDialog() {this.$presets = CE("select", {class: "bx-full-width",tabindex: -1});let $select = BxSelectElement.create(this.$presets);$select.addEventListener("input", (e) => {this.switchPreset(parseInt($select.value));});let $header = CE("div", {class: "bx-dialog-preset-tools",_nearby: {orientation: "horizontal",focus: $select}}, $select, this.$btnRename = createButton({title: t("rename"),icon: BxIcon.CURSOR_TEXT,style: 64,onClick: async () => {let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name);if (!newName) return;preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh();}}), this.$btnDelete = createButton({icon: BxIcon.TRASH,title: t("delete"),style: 4 | 64,onClick: async (e) => {if (!confirm(t("confirm-delete-preset"))) return;await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh();}}), createButton({icon: BxIcon.NEW,title: t("new"),style: 64 | 1,onClick: async (e) => {let newName = this.promptNewName(t("new"));if (!newName) return;let newId = await this.presetsDb.newPreset(newName, this.presetsDb.BLANK_PRESET_DATA);this.currentPresetId = newId, await this.refresh();}}), createButton({icon: BxIcon.COPY,title: t("copy"),style: 64 | 1,onClick: async (e) => {let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`);if (!newName) return;let newId = await this.presetsDb.newPreset(newName, preset.data);this.currentPresetId = newId, await this.refresh();}}));this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, this.title), createButton({icon: BxIcon.CLOSE,style: 64 | 2048 | 8,onClick: (e) => this.hide()})), CE("div", !1, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content));}async refresh() {await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 });}async onBeforeMount(configs = {}) {await this.renderPresetsList();let valid = !1;if (typeof configs?.id === "number") {if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0;}if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null;this.refresh();}getDialog() {return this;}getContent() {if (!this.$container) this.renderDialog();return this.$container;}focusIfNeeded() {this.dialogManager.focus(this.$header);}} +var SHORTCUT_ACTIONS = {[t("better-xcloud")]: {"bx.settings.show": [t("settings"), t("show")]},...STATES.browser.capabilities.mkb ? {[t("mouse-and-keyboard")]: {"mkb.toggle": [t("toggle")]}} : {},[t("controller")]: {"controller.xbox.press": [t("button-xbox"), t("press")]},...AppInterface ? {[t("device")]: {"device.sound.toggle": [t("sound"), t("toggle")],"device.volume.inc": [t("volume"), t("increase")],"device.volume.dec": [t("volume"), t("decrease")],"device.brightness.inc": [t("brightness"), t("increase")],"device.brightness.dec": [t("brightness"), t("decrease")]}} : {},[t("stream")]: {"stream.screenshot.capture": [t("take-screenshot")],"stream.video.toggle": [t("video"), t("toggle")],"stream.sound.toggle": [t("sound"), t("toggle")],...getGlobalPref("audio.volume.booster.enabled") ? {"stream.volume.inc": [t("volume"), t("increase")],"stream.volume.dec": [t("volume"), t("decrease")]} : {},"stream.menu.show": [t("menu"), t("show")],"stream.stats.toggle": [t("stats"), t("show-hide")],"stream.microphone.toggle": [t("microphone"), t("toggle")]},[t("other")]: {"ta.open": [t("true-achievements"), t("show")]}}; +class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts")));$content;selectActions = {};BUTTONS_ORDER = [3,0,2,1,12,13,14,15,8,9,4,5,6,7,10,11];constructor(title) {super(title, ControllerShortcutsTable.getInstance());let $baseSelect = CE("select", {class: "bx-full-width",autocomplete: "off"}, CE("option", { value: "" }, "---"));for (let groupLabel in SHORTCUT_ACTIONS) {let items = SHORTCUT_ACTIONS[groupLabel];if (!items) continue;let $optGroup = CE("optgroup", { label: groupLabel });for (let action in items) {let crumbs = items[action];if (!crumbs) continue;let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label);$optGroup.appendChild($option);}$baseSelect.appendChild($optGroup);}let $content = CE("div", {class: "bx-controller-shortcuts-manager-container"}), onActionChanged = (e) => {if (!e.ignoreOnChange) this.updatePreset();}, fragment = document.createDocumentFragment();fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")));for (let button of this.BUTTONS_ORDER) {let prompt2 = GamepadKeyName[button][1], $row = CE("div", {class: "bx-shortcut-row",_nearby: {orientation: "horizontal"}}), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0));$select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, {focus: $select}), $row.append($label, $select), fragment.appendChild($row);}$content.appendChild(fragment), this.$content = $content;}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}this.currentPresetId = id;let isDefaultPreset = id <= 0, actions = preset.data, button;for (button in this.selectActions) {let $select = this.selectActions[button];$select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {ignoreOnChange: !0,manualTrigger: !0});}super.updateButtonStates();}updatePreset() {let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), button;for (button in this.selectActions) {let action = this.selectActions[button].value;if (!action) continue;newData.mapping[button] = action;}let preset = this.allPresets.data[this.currentPresetId];preset.data = newData, this.presetsDb.updatePreset(preset);}onBeforeUnmount() {StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();}} +class BxDualNumberStepper extends HTMLInputElement {controlValues;controlMin;controlMinDiff;controlMax;steps;options;onChange;$text;$rangeFrom;$rangeTo;$activeRange;onRangeInput;setValue;getValue;normalizeValue;static create(key, values, options, onChange) {options.suffix = options.suffix || "", options.disabled = !!options.disabled;let $text, $rangeFrom, $rangeTo, self = CE("div", {class: "bx-dual-number-stepper",id: `bx_setting_${escapeCssSelector(key)}`}, $text = CE("span"));if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self;return $rangeFrom = CE("input", {type: "range",min: self.controlMin,max: self.controlMax,step: self.steps,tabindex: 0}), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", !1, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, {focus: $rangeFrom,orientation: "vertical"}), Object.defineProperty(self, "value", {get() {return self.controlValues;},set(value) {let from, to;if (typeof value === "string") {let tmp = value.split(",");from = parseInt(tmp[0]), to = parseInt(tmp[1]);} else if (Array.isArray(value)) [from, to] = value;if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]);}}), self;}static setValues(values) {let from, to;if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values);else from = this.controlMin, to = this.controlMax, values = [from, to];this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString();let ratio = 100 / (this.controlMax - this.controlMin);this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%");}static getValues() {return this.controlValues || [this.controlMin, this.controlMax];}static normalizeValues(values) {let [from, to] = values;if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from);else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to);return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to];}static onRangeInput(e) {this.$activeRange = e.target;let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]);if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values);}static onContextMenu(e) {e.preventDefault();}static updateTextValue() {let values = this.controlValues, textContent = null;if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax);if (textContent === null) {let [from, to] = values;if (from === this.controlMin && to === this.controlMax) textContent = t("default");else {let pad = to.toString().length;textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`;}}return textContent;}} +class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization")));$vibrationIntensity;$leftTriggerRange;$rightTriggerRange;$leftStickDeadzone;$rightStickDeadzone;$btnDetect;selectsMap = {};selectsOrder = [];isDetectingButton = !1;detectIntervalId = null;static BUTTONS_ORDER = [0,1,2,3,12,15,13,14,4,5,6,7,10,11,104,204,8,9,17];constructor(title) {super(title, ControllerCustomizationsTable.getInstance());this.render();}render() {let isControllerFriendly = getGlobalPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => {if (!e.ignoreOnChange) this.updatePreset();}, boundUpdatePreset = this.updatePreset.bind(this);for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {if (gamepadKey === 17) continue;let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0];(gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", {value: gamepadKey,_dataset: { label: GamepadKeyName[gamepadKey][1] }}, name));}for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0);$clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove();let $select = BxSelectElement.create($clonedSelect);$select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey);let $row = CE("div", {class: "bx-controller-key-row",_nearby: { orientation: "horizontal" }}, CE("label", { title: buttonName }, buttonPrompt), $select);$rows.append($row);}if (getGlobalPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) {let $select = this.selectsMap[this.selectsOrder[i]], directions = {1: i - 2,3: i + 2,4: i - 1,2: i + 1};for (let dir in directions) {let idx = directions[dir];if (typeof this.selectsOrder[idx] === "undefined") continue;let $targetSelect = this.selectsMap[this.selectsOrder[idx]];setNearby($select, {[dir]: $targetSelect});}}let blankSettings = this.presetsDb.BLANK_PRESET_DATA.settings, params = {min: 0,minDiff: 1,max: 100,steps: 1};this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({label: t("detect-controller-button"),classes: ["bx-btn-detect"],style: 4096 | 64 | 128,onClick: () => {this.startDetectingButton();}}), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, {steps: 10,suffix: "%",exactTicks: 20,customTextValue: (value) => {return value = parseInt(value), value === 0 ? t("off") : value + "%";}}, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", blankSettings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", blankSettings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", blankSettings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", blankSettings.rightStickDeadzone, params, boundUpdatePreset)));}startDetectingButton() {this.isDetectingButton = !0;let { $btnDetect } = this;$btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0;let count = 4;$btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => {if (count -= 1, count === 0) {this.stopDetectingButton(), $btnDetect.focus();return;}$btnDetect.textContent = `[${count}] ${t("press-any-button")}`;}, 1000);}stopDetectingButton() {let { $btnDetect } = this;$btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null;}async onBeforeMount() {this.stopDetectingButton(), super.onBeforeMount(...arguments);}onBeforeUnmount() {this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();}handleGamepad(button) {if (!this.isDetectingButton) return super.handleGamepad(button);if (button in ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {this.stopDetectingButton();let $select = this.selectsMap[button], $label = $select.previousElementSibling;if ($label.addEventListener("animationend", () => {$label.classList.remove("bx-horizontal-shaking");}, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getGlobalPref("ui.controllerFriendly"))this.dialogManager.focus($select);}return !0;}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}let {$btnDetect,$vibrationIntensity,$leftStickDeadzone,$rightStickDeadzone,$leftTriggerRange,$rightTriggerRange,selectsMap} = this, presetData = preset.data;this.currentPresetId = id;let isDefaultPreset = id <= 0;this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset);let buttonIndex;for (buttonIndex in selectsMap) {buttonIndex = buttonIndex;let $select = selectsMap[buttonIndex];if (!$select) continue;let mappedButton = presetData.mapping[buttonIndex];$select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {ignoreOnChange: !0,manualTrigger: !0});}presetData.settings = Object.assign({}, this.presetsDb.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange);}updatePreset() {let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), gamepadKey;for (gamepadKey in this.selectsMap) {let value = this.selectsMap[gamepadKey].value;if (!value) continue;let mapTo = value === "false" ? !1 : parseInt(value);newData.mapping[gamepadKey] = mapTo;}Object.assign(newData.settings, {vibrationIntensity: parseInt(this.$vibrationIntensity.value),leftStickDeadzone: this.$leftStickDeadzone.getValue(),rightStickDeadzone: this.$rightStickDeadzone.getValue(),leftTriggerRange: this.$leftTriggerRange.getValue(),rightTriggerRange: this.$rightTriggerRange.getValue()});let preset = this.allPresets.data[this.currentPresetId];preset.data = newData, this.presetsDb.updatePreset(preset);}async renderSummary(presetId) {let preset = await this.presetsDb.getPreset(presetId);if (!preset) return null;let presetData = preset.data, $content, showNote = !1;if (Object.keys(presetData.mapping).length > 0) {$content = CE("div", { class: "bx-controller-customization-summary" });for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {if (!(gamepadKey in presetData.mapping)) continue;let mappedKey = presetData.mapping[gamepadKey];$content.append(CE("span", { class: "bx-prompt" }, getGamepadPrompt(gamepadKey) + " > " + (mappedKey === !1 ? "🚫" : getGamepadPrompt(mappedKey))));}showNote = !0;}let key;for (key in presetData.settings) {if (key === "vibrationIntensity") continue;let value = presetData.settings[key];if (Array.isArray(value) && (value[0] !== 0 || value[1] !== 100)) {showNote = !0;break;}}let fragment = document.createDocumentFragment();if (showNote) {let $note = CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("controller-customization-input-latency-note"));fragment.appendChild($note);}if ($content) fragment.appendChild($content);return fragment.childElementCount ? fragment : null;}} +class ControllerExtraSettings extends HTMLElement {currentControllerId;controllerIds;$selectControllers;$selectShortcuts;$selectCustomization;$summaryCustomization;updateLayout;switchController;getCurrentControllerId;saveSettings;updateCustomizationSummary;setValue;static renderSettings() {let $container = CE("label", {class: "bx-settings-row bx-controller-extra-settings"});$container.prefKey = "controller.settings", $container.addEventListener("contextmenu", this.boundOnContextMenu), this.settingsManager.setElement("controller.settings", $container), $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container), $container.setValue = ControllerExtraSettings.setValue.bind($container);let $selectControllers = BxSelectElement.create(CE("select", {class: "bx-full-width",autocomplete: "off",_on: {input: (e) => {$container.switchController($selectControllers.value);}}})), $selectShortcuts = BxSelectElement.create(CE("select", {autocomplete: "off",_on: { input: $container.saveSettings }})), $selectCustomization = BxSelectElement.create(CE("select", {autocomplete: "off",_on: {input: async () => {ControllerExtraSettings.updateCustomizationSummary.call($container), $container.saveSettings();}}})), $rowCustomization = createSettingRow(t("in-game-controller-customization"), CE("div", {class: "bx-preset-row",_nearby: { orientation: "horizontal" }}, $selectCustomization, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null})})), { multiLines: !0 });return $rowCustomization.appendChild($container.$summaryCustomization = CE("div")), $container.append(CE("span", !1, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", {class: "bx-preset-row",_nearby: { orientation: "horizontal" }}, $selectShortcuts, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => ControllerShortcutsManagerDialog.getInstance().show({id: parseInt($container.$selectShortcuts.value)})})), { multiLines: !0 }), $rowCustomization))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => {$container.updateLayout();}), $container;}static async updateCustomizationSummary() {let presetId = parseInt(this.$selectCustomization.value), $summaryContent = await ControllerCustomizationsManagerDialog.getInstance().renderSummary(presetId);if (removeChildElements(this.$summaryCustomization), $summaryContent) this.$summaryCustomization.appendChild($summaryContent);}static async updateLayout() {if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return;let $fragment = document.createDocumentFragment();removeChildElements(this.$selectControllers);for (let name of this.controllerIds) {let $option = CE("option", { value: name }, simplifyGamepadName(name));$fragment.appendChild($option);}this.$selectControllers.appendChild($fragment);let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 });let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets();renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 });for (let name of this.controllerIds) {let $option = CE("option", { value: name }, name);$fragment.appendChild($option);}BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this);}static async switchController(id) {if (this.currentControllerId = id, !this.getCurrentControllerId()) return;let controllerSetting = STORAGE.Stream.getControllerSetting(this.currentControllerId);ControllerExtraSettings.updateElements.call(this, controllerSetting);}static getCurrentControllerId() {if (this.currentControllerId) {if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId;this.currentControllerId = "";}if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0];if (this.currentControllerId) return this.currentControllerId;return null;}static async saveSettings() {if (!this.getCurrentControllerId()) return;let controllerSettings = getStreamPref("controller.settings");controllerSettings[this.currentControllerId] = {shortcutPresetId: parseInt(this.$selectShortcuts.value),customizationPresetId: parseInt(this.$selectCustomization.value)}, setStreamPref("controller.settings", controllerSettings, "ui"), StreamSettings.refreshControllerSettings();}static setValue(value) {ControllerExtraSettings.updateElements.call(this, value[this.currentControllerId]);}static updateElements(controllerSetting) {if (!controllerSetting) return;this.$selectShortcuts.value = controllerSetting.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSetting.customizationPresetId.toString(), ControllerExtraSettings.updateCustomizationSummary.call(this);}} +class SuggestionsSetting {static async renderSuggestions(e) {let $btnSuggest = e.target.closest("div");$btnSuggest.toggleAttribute("bx-open");let $content = $btnSuggest.nextElementSibling;if ($content) {BxEvent.dispatch($content.querySelector("select"), "input");return;}let settingTabGroup;for (settingTabGroup in this.SETTINGS_UI) {let settingTab = this.SETTINGS_UI[settingTabGroup];if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;for (let settingTabContent of settingTab.items) {if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue;for (let setting of settingTabContent.items) {let prefKey;if (typeof setting === "string") prefKey = setting;else if (typeof setting === "object") prefKey = setting.pref;if (prefKey) this.settingLabels[prefKey] = settingTabContent.label;}}}let recommendedDevice = "";if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);}let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on");else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto");else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off");SuggestionsSetting.generateDefaultSuggestedSettings.call(this);let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality")));$select.addEventListener("input", (e2) => {let profile = $select.value;removeChildElements($suggestedSettings);let fragment = document.createDocumentFragment(), note;if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice });else if (profile === "highest") note = "⚠️ " + t("highest-quality-note");note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note));let settings = this.suggestedSettings[profile];for (let key in settings) {let { storage, definition } = getPrefInfo(key), prefKey;if (storage === STORAGE.Stream) prefKey = key;else prefKey = key;let suggestedValue;if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);else suggestedValue = settings[prefKey];let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value;if (isSameValue) $value = currentValueText;else {let suggestedValueText = storage.getValueText(prefKey, suggestedValue);$value = currentValueText + " ➔ " + suggestedValueText;}let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`);if ($child = CE("div", {class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}`}, $checkbox = CE("input", {type: "checkbox",tabindex: 0,checked: !0,id}), CE("label", {for: id}, CE("div", {class: "bx-suggest-label"}, breadcrumb), CE("div", {class: "bx-suggest-value"}, $value))), isSameValue)$checkbox.disabled = !0, $checkbox.checked = !0;fragment.appendChild($child);}$suggestedSettings.appendChild(fragment);}), BxEvent.dispatch($select, "input");let onClickApply = () => {let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance();for (prefKey in settings) {let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`);if (!$checkBox.checked || $checkBox.disabled) continue;let $control = settingsManager.getElement(prefKey);if (!$control) {setPref(prefKey, suggestedValue, "direct");continue;}let { definition: settingDefinition } = getPrefInfo(prefKey);if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);if ("setValue" in $control) $control.setValue(suggestedValue);else $control.value = suggestedValue;BxEvent.dispatch($control, "input", {manualTrigger: !0});}BxEvent.dispatch($select, "input");}, $btnApply = createButton({label: t("apply"),style: 128 | 64,onClick: onClickApply});$content = CE("div", {class: "bx-sub-content-box bx-suggest-box",_nearby: {orientation: "vertical"}}, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", {class: "bx-suggest-link bx-focusable",href: "https://better-xcloud.github.io/guide/android-webview-tweaks/",target: "_blank",tabindex: 0}, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", {class: "bx-suggest-link bx-focusable",href: "https://github.com/redphx/better-xcloud-devices",target: "_blank",tabindex: 0}, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);}static async getRecommendedSettings(androidInfo) {function normalize(str) {return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");}try {let { brand, board, model } = androidInfo;brand = normalize(brand), board = normalize(board), model = normalize(model);let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {};if (json.schema_version !== 2) return null;let scriptSettings = json.settings.script;if (scriptSettings._base) {let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base;for (let profile of base)Object.assign(recommended, this.suggestedSettings[profile]);delete scriptSettings._base;}let key;for (key in scriptSettings)recommended[key] = scriptSettings[key];return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name;} catch (e) {}return null;}static addDefaultSuggestedSetting(prefKey, value) {let key;for (key in this.suggestedSettings)if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value;}static generateDefaultSuggestedSettings() {let key;for (key in this.suggestedSettings) {if (key === "default") continue;let prefKey;for (prefKey in this.suggestedSettings[key])if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default;}}} +class BxKeyBindingButton extends HTMLButtonElement {title;isPrompt = !1;allowedFlags;keyInfo = null;bindKey;unbindKey;static create(options) {let $btn = CE("button", {class: "bx-binding-button bx-focusable",type: "button"});return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn;}static onClick(e) {KeyBindingDialog.getInstance().show({$elm: this});}static onContextMenu = (e) => {e.preventDefault();let $btn = e.target;if (!$btn.disabled) $btn.unbindKey.apply($btn);};static bindKey(key, force = !1) {if (!key) return;if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) {if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change");}}static unbindKey(force = !1) {this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change");}constructor() {super();}} +class KeyBindingDialog {static instance;static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog);$dialog;$wait;$title;$inputList;$overlay;$currentElm;countdownIntervalId;constructor() {this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", !1, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", !1, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", !1, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog);}show(options) {this.$currentElm = options.$elm, this.addEventListeners();let allowedFlags = this.$currentElm.allowedFlags;this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown();}startCountdown() {this.stopCountdown();let count = 9;this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => {if (count -= 1, count === 0) {this.stopCountdown(), this.hide();return;}this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`;}, 1000);}stopCountdown() {this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null;}hide = () => {this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone");};addEventListeners() {let allowedFlags = this.$currentElm.allowedFlags;if (allowedFlags.includes(1)) window.addEventListener("keyup", this);if (allowedFlags.includes(4)) window.addEventListener("mousedown", this);if (allowedFlags.includes(8)) window.addEventListener("wheel", this);}clearEventListeners() {window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this);}handleEvent(e) {let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1;switch (e.type) {case "wheel":if (handled = !0, allowedFlags.includes(8)) valid = !0;break;case "mousedown":if (handled = !0, allowedFlags.includes(4)) valid = !0;break;case "keyup":if (handled = !0, allowedFlags.includes(1)) {let keyboardEvent = e;if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) {let key = keyboardEvent.key;valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid;}}break;}if (handled) {if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown();else this.startCountdown();window.setTimeout(this.hide, 200);}}} +class MkbMappingManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller")));KEYS_PER_BUTTON = 2;BUTTONS_ORDER = [16,12,13,14,15,0,1,2,3,4,5,6,7,8,9,10,100,101,102,103,11,200,201,202,203];allKeyElements = [];$mouseMapTo;$mouseSensitivityX;$mouseSensitivityY;$mouseDeadzone;$unbindNote;constructor(title) {super(title, MkbMappingPresetsTable.getInstance());this.render();}onBindingKey = (e) => {if (e.target.disabled) return;if (e.button !== 0) return;};parseDataset($btn) {let dataset = $btn.dataset;return {keySlot: parseInt(dataset.keySlot),buttonIndex: parseInt(dataset.buttonIndex)};}onKeyChanged = (e) => {let $current = e.target, keyInfo = $current.keyInfo;if (keyInfo) {for (let $elm of this.allKeyElements)if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0);}this.savePreset();};render() {let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")));for (let buttonIndex of this.BUTTONS_ORDER) {let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment();for (let i = 0;i < this.KEYS_PER_BUTTON; i++)$elm = BxKeyBindingButton.create({title: buttonPrompt,isPrompt: !0,allowedFlags: [1, 4, 8],onChanged: this.onKeyChanged}), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm);let $keyRow = CE("div", {class: "bx-mkb-key-row",_nearby: { orientation: "horizontal" }}, CE("label", { title: buttonName }, buttonPrompt), $fragment);$rows.appendChild($keyRow);}let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, {suffix: "%",exactTicks: 50}, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, {suffix: "%",exactTicks: 50}, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, {suffix: "%",exactTicks: 10}, savePreset)));this.$content = CE("div", !1, $rows, $extraSettings);}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}let presetData = preset.data;this.currentPresetId = id;let isDefaultPreset = id <= 0;this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);for (let $elm of this.allKeyElements) {let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex];if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({code: buttonKeys[keySlot]}, !0);else $elm.unbindKey(!0);$elm.disabled = isDefaultPreset;}let mouse = presetData.mouse;this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();}savePreset() {let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);for (let $elm of this.allKeyElements) {let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping;if (!mapping[buttonIndex]) mapping[buttonIndex] = [];if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot];else mapping[buttonIndex][keySlot] = $elm.keyInfo.code;}let mouse = presetData.mouse;mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {id: this.currentPresetId,name: oldPreset.name,data: presetData};this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;}onBeforeUnmount() {StreamSettings.refreshMkbSettings(), super.onBeforeUnmount();}} +class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog {static instance;static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts")));$content;$unbindNote;allKeyElements = [];constructor(title) {super(title, KeyboardShortcutsTable.getInstance());let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" });for (let groupLabel in SHORTCUT_ACTIONS) {let items = SHORTCUT_ACTIONS[groupLabel];if (!items) continue;let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel));for (let action in items) {let crumbs = items[action];if (!crumbs) continue;let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({title: label,isPrompt: !1,onChanged: this.onKeyChanged,allowedFlags: [1, 2]});$btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn);let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn));$fieldSet.appendChild($row);}if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet);}this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows);}onKeyChanged = (e) => {let $current = e.target, keyInfo = $current.keyInfo;if (keyInfo) for (let $elm of this.allKeyElements) {if ($elm === $current) continue;if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0);}this.savePreset();};parseDataset($btn) {return {action: $btn.dataset.action};}switchPreset(id) {let preset = this.allPresets.data[id];if (!preset) {this.currentPresetId = 0;return;}let presetData = preset.data;this.currentPresetId = id;let isDefaultPreset = id <= 0;this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);for (let $elm of this.allKeyElements) {let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action];if (keyInfo) $elm.bindKey(keyInfo, !0);else $elm.unbindKey(!0);$elm.disabled = isDefaultPreset;}}savePreset() {let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);for (let $elm of this.allKeyElements) {let { action } = this.parseDataset($elm), mapping = presetData.mapping;if ($elm.keyInfo) mapping[action] = $elm.keyInfo;}let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {id: this.currentPresetId,name: oldPreset.name,data: presetData};this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;}onBeforeUnmount() {StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount();}} +class MkbExtraSettings extends HTMLElement {$mappingPresets;$shortcutsPresets;updateLayout;saveMkbSettings;saveShortcutsSettings;static renderSettings() {let $container = document.createDocumentFragment();$container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);let $mappingPresets = BxSelectElement.create(CE("select", {autocomplete: "off",_on: {input: $container.saveMkbSettings}})), $shortcutsPresets = BxSelectElement.create(CE("select", {autocomplete: "off",_on: {input: $container.saveShortcutsSettings}}));return $container.append(...getGlobalPref("mkb.enabled") ? [createSettingRow(t("virtual-controller"), CE("div", {class: "bx-preset-row",_nearby: {orientation: "horizontal"}}, $mappingPresets, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => MkbMappingManagerDialog.getInstance().show({id: parseInt($container.$mappingPresets.value)})})), {multiLines: !0,onContextMenu: this.boundOnContextMenu,pref: "mkb.p1.preset.mappingId"}),createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), {onContextMenu: this.boundOnContextMenu,pref: "mkb.p1.slot"})] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", {class: "bx-preset-row",_nearby: {orientation: "horizontal"}}, $shortcutsPresets, createButton({title: t("manage"),icon: BxIcon.MANAGE,style: 64 | 1 | 512,onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({id: parseInt($container.$shortcutsPresets.value)})})), {multiLines: !0,onContextMenu: this.boundOnContextMenu,pref: "keyboardShortcuts.preset.inGameId"})), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => {$container.updateLayout();}), $container;}static async updateLayout() {let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId"));let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 });}static async saveMkbSettings() {let presetId = parseInt(this.$mappingPresets.value);setStreamPref("mkb.p1.preset.mappingId", presetId, "ui");}static async saveShortcutsSettings() {let presetId = parseInt(this.$shortcutsPresets.value);setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui");}} +class SettingsDialog extends NavigationDialog {static instance;static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);LOG_TAG = "SettingsNavigationDialog";$container;$tabs;$tabContents;$btnReload;$btnGlobalReload;$noteGlobalReload;$btnSuggestion;$streamSettingsSelection;renderFullSettings;boundOnContextMenu;suggestedSettings = {recommended: {},default: {},lowest: {},highest: {}};settingLabels = {};settingsManager;TAB_GLOBAL_ITEMS = [{group: "general",label: t("better-xcloud"),helpUrl: "https://better-xcloud.github.io/features/",items: [($parent) => {let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {let opts = {label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),style: 1 | 64 | 128};if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";topButtons.push(createButton(opts));}if (AppInterface) topButtons.push(createButton({label: t("app-settings"),icon: BxIcon.STREAM_SETTINGS,style: 128 | 64,onClick: (e) => {AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();}}));else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({label: "🔥 " + t("install-android"),style: 128 | 64,url: "https://better-xcloud.github.io/android"}));this.$btnGlobalReload = createButton({label: t("settings-reload"),classes: ["bx-settings-reload-button", "bx-gone"],style: 64 | 128,onClick: (e) => {this.reloadPage();}}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {class: "bx-settings-reload-note"}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {class: "bx-suggest-toggler bx-focusable",tabindex: 0}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);let $div = CE("div", {class: "bx-top-buttons",_nearby: {orientation: "vertical"}}, ...topButtons);$parent.appendChild($div);},{pref: "bx.locale",multiLines: !0},"server.bypassRestriction","ui.controllerFriendly","xhome.enabled"]}, {group: "server",label: t("server"),items: [{pref: "server.region",multiLines: !0},{pref: "stream.locale",multiLines: !0},"server.ipv6.prefer"]}, {group: "stream",label: t("stream"),items: ["stream.video.resolution","stream.video.codecProfile","stream.video.maxBitrate","audio.volume.booster.enabled","screenshot.applyFilters","audio.mic.onPlaying","game.fortnite.forceConsole","stream.video.combineAudio"]}, {requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),items: ["nativeMkb.mode",{pref: "nativeMkb.forcedGames",multiLines: !0,note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))},"mkb.enabled","mkb.cursor.hideIdle"],...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {unsupported: !0,unsupportedNote: CE("a", {href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",target: "_blank"}, "⚠️ " + t("browser-unsupported-feature"))} : {}}, {requiredVariants: "full",group: "touch-control",label: t("touch-controller"),items: [{pref: "touchController.mode",note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))},"touchController.autoOff","touchController.opacity.default","touchController.style.standard","touchController.style.custom"],...!STATES.userAgent.capabilities.touch ? {unsupported: !0,unsupportedNote: "⚠️ " + t("device-unsupported-touch")} : {}}, {group: "ui",label: t("ui"),items: ["ui.layout","ui.imageQuality","ui.gameCard.waitTime.show","ui.controllerStatus.show","ui.streamMenu.simplify","ui.splashVideo.skip",!AppInterface && "ui.hideScrollbar","ui.systemMenu.hideHandle","ui.feedbackDialog.disabled","ui.reduceAnimations",{pref: "ui.hideSections",multiLines: !0},{pref: "block.features",multiLines: !0}]}, {requiredVariants: "full",group: "game-bar",label: t("game-bar"),items: ["gameBar.position"]}, {group: "loading-screen",label: t("loading-screen"),items: ["loadingScreen.gameArt.show","loadingScreen.waitTime.show","loadingScreen.rocket"]}, {group: "other",label: t("other"),items: ["block.tracking"]}, {group: "advanced",label: t("advanced"),items: [{pref: "userAgent.profile",multiLines: !0,onCreated: (setting, $control) => {let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {type: "text",placeholder: defaultUserAgent,autocomplete: "off",class: "bx-settings-custom-user-agent",tabindex: 0});$inpCustomUserAgent.addEventListener("input", (e) => {let profile = $control.value, custom = e.target.value.trim();UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {orientation: "vertical"});}}]}, {group: "footer",items: [($parent) => {try {let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);$parent.appendChild(CE("div", {class: "bx-settings-app-version"}, `xCloud website version ${appVersion} (${appDate})`));} catch (e) {}},($parent) => {$parent.appendChild(CE("a", {class: "bx-donation-link",href: "https://ko-fi.com/redphx",target: "_blank",tabindex: 0}, `❤️ ${t("support-better-xcloud")}`));},($parent) => {$parent.appendChild(createButton({label: t("clear-data"),style: 8 | 128 | 64,onClick: (e) => {if (confirm(t("clear-data-confirm"))) clearAllData();}}));},($parent) => {$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({label: "Debug info",style: 8 | 128 | 64,onClick: (e) => {let $button = e.target.closest("button");if (!$button) return;let $pre = $button.nextElementSibling;if (!$pre) {let debugInfo = deepClone(BX_FLAGS.DeviceInfo);debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {class: "bx-focusable bx-gone",tabindex: 0,_on: {click: async (e2) => {await copyToClipboard(e2.target.innerText);}}}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);}$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();}})));}]}];TAB_DISPLAY_ITEMS = [{requiredVariants: "full",group: "audio",label: t("audio"),helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",items: [{pref: "audio.volume",params: {disabled: !getGlobalPref("audio.volume.booster.enabled")},onCreated: (setting, $elm) => {let $range = $elm.querySelector("input[type=range");BxEventBus.Stream.on("setting.changed", (payload) => {let { settingKey } = payload;if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });});}}]}, {group: "video",label: t("video"),helpUrl: "https://better-xcloud.github.io/ingame-features/#video",items: ["video.player.type","video.maxFps","video.player.powerPreference","video.processing","video.ratio","video.position","video.processing.sharpness","video.saturation","video.contrast","video.brightness"]}];TAB_CONTROLLER_ITEMS = [{group: "controller",label: t("controller"),helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",items: ["localCoOp.enabled","controller.pollingRate",($parent) => {$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));}]},STATES.userAgent.capabilities.touch && {group: "touch-control",label: t("touch-controller"),items: [{label: t("layout"),content: CE("select", {disabled: !0}, CE("option", !1, t("default"))),onCreated: (setting, $elm) => {$elm.addEventListener("input", (e) => {TouchController.applyCustomLayout($elm.value, 1000);}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {let customLayouts = TouchController.getCustomLayouts();while ($elm.firstChild)$elm.removeChild($elm.firstChild);if ($elm.disabled = !customLayouts, !customLayouts) {$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));return;}let $fragment = document.createDocumentFragment();for (let key in customLayouts.layouts) {let layout = customLayouts.layouts[key], name;if (layout.author) name = `${layout.name} (${layout.author})`;else name = layout.name;let $option = CE("option", { value: key }, name);$fragment.appendChild($option);}$elm.appendChild($fragment), $elm.value = customLayouts.default_layout;});}}]},STATES.browser.capabilities.deviceVibration && {group: "device",label: t("device"),items: [{pref: "deviceVibration.mode",multiLines: !0,unsupported: !STATES.browser.capabilities.deviceVibration}, {pref: "deviceVibration.intensity",unsupported: !STATES.browser.capabilities.deviceVibration}]}];TAB_MKB_ITEMS = [{requiredVariants: "full",group: "mkb",label: t("mouse-and-keyboard"),helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",items: [($parent) => {$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));}]},NativeMkbHandler.isAllowed() && {requiredVariants: "full",group: "native-mkb",label: t("native-mkb"),items: ["nativeMkb.scroll.sensitivityY","nativeMkb.scroll.sensitivityX"]}];TAB_STATS_ITEMS = [{group: "stats",label: t("stream-stats"),helpUrl: "https://better-xcloud.github.io/stream-stats/",items: ["stats.showWhenPlaying","stats.quickGlance.enabled","stats.items","stats.position","stats.textSize","stats.opacity.all","stats.opacity.background","stats.colors"]}];SETTINGS_UI = {global: {group: "global",icon: BxIcon.HOME,items: this.TAB_GLOBAL_ITEMS},stream: {group: "stream",icon: BxIcon.DISPLAY,items: this.TAB_DISPLAY_ITEMS},controller: {group: "controller",icon: BxIcon.CONTROLLER,items: this.TAB_CONTROLLER_ITEMS,requiredVariants: "full"},mkb: {group: "mkb",icon: BxIcon.NATIVE_MKB,items: this.TAB_MKB_ITEMS,requiredVariants: "full"},stats: {group: "stats",icon: BxIcon.STREAM_STATS,items: this.TAB_STATS_ITEMS}};constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {this.$tabContents.dataset.gameId = id.toString();});}getDialog() {return this;}getContent() {return this.$container;}onMounted() {super.onMounted();}isOverlayVisible() {return !STATES.isPlaying;}reloadPage() {this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();}isSupportedVariant(requiredVariants) {if (typeof requiredVariants === "undefined") return !0;return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);}onTabClicked = (e) => {let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);for ($child of children)if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");for (let $child2 of Array.from(this.$tabs.children))$child2.classList.remove("bx-active");$svg.classList.add("bx-active");};renderTab(settingTab) {let $svg = createSvgIcon(settingTab.icon);return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;}onGlobalSettingChanged = (e) => {PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");};onContextMenu(e) {e.preventDefault();let $elm = e.target;$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);}renderServerSetting(setting) {let selectedValue = getGlobalPref("server.region"), continents = {"america-north": {label: t("continent-north-america")},"america-south": {label: t("continent-south-america")},asia: {label: t("continent-asia")},australia: {label: t("continent-australia")},europe: {label: t("continent-europe")},other: {label: t("other")}}, $control = CE("select", {id: `bx_setting_${escapeCssSelector(setting.pref)}`,tabindex: 0});$control.name = $control.id, $control.addEventListener("input", (e) => {setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);}), setting.options = {};for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;if (region.isDefault) {if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";}setting.options[value] = label;let $option = CE("option", { value }, label), continent = continents[region.contintent];if (!continent.children) continent.children = [];continent.children.push($option);}let fragment = document.createDocumentFragment(), key;for (key in continents) {let continent = continents[key];if (!continent.children) continue;fragment.appendChild(CE("optgroup", {label: continent.label}, ...continent.children));}return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;}renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {if (typeof setting === "string") setting = {pref: setting};let pref = setting.pref, $control;if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);else $control = setting.content;else if (!setting.unsupported) {if (pref === "server.region") $control = this.renderServerSetting(setting);else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {let newLocale = e.target.value;if (getGlobalPref("ui.controllerFriendly")) {let timeoutId = e.target.timeoutId;timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {Translations.refreshLocale(newLocale), Translations.updateTranslations();}, 500);} else Translations.refreshLocale(newLocale), Translations.updateTranslations();this.onGlobalSettingChanged(e);});else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);UserAgent.updateStorage(value);let $inp = $control.nextElementSibling;$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);});else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);}let prefDefinition = null;if (pref) prefDefinition = getPrefInfo(pref).definition;if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;if (typeof note === "function") note = note();if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();if (settingTabContent.label && setting.pref) {if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);}if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");else note = `${t("experimental")}: ${note}`;let $note;if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {$note,multiLines: setting.multiLines,icon: prefDefinition?.labelIcon,onContextMenu: this.boundOnContextMenu,pref});if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;$row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);}renderSettingsSection(settingTab, sections) {let $tabContent = CE("div", {class: "bx-gone",_dataset: {tabGroup: settingTab.group}});for (let section of sections) {if (!section) continue;if (section instanceof HTMLElement) {$tabContent.appendChild(section);continue;}if (!this.isSupportedVariant(section.requiredVariants)) continue;if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;let label = section.label;if (label === t("better-xcloud")) {if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";label = createButton({label,url: "https://github.com/redphx/better-xcloud/releases",style: 4096 | 16 | 64});}if (label) {let $title = CE("h2", {_nearby: {orientation: "horizontal"}}, CE("span", !1, label), section.helpUrl && createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: section.helpUrl,title: t("help")}));$tabContent.appendChild($title);}if (section.unsupportedNote) {let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);$tabContent.appendChild($note);}if (section.unsupported) continue;if (section.content) {$tabContent.appendChild(section.content);continue;}section.items = section.items || [];for (let setting of section.items) {if (setting === !1) continue;if (typeof setting === "function") {setting.apply(this, [$tabContent]);continue;}this.renderSettingRow(settingTab, $tabContent, section, setting);}}return $tabContent;}setupDialog() {let $tabs, $tabContents, $container = CE("div", {class: "bx-settings-dialog",_nearby: {orientation: "horizontal"}}, CE("div", {class: "bx-settings-tabs-container",_nearby: {orientation: "vertical",focus: () => {return this.dialogManager.focus($tabs);},loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;return !1;}}}, $tabs = CE("div", {class: "bx-settings-tabs bx-hide-scroll-bar",_nearby: {focus: () => this.focusActiveTab()}}), CE("div", !1, this.$btnReload = createButton({icon: BxIcon.REFRESH,style: 64 | 32,onClick: (e) => {this.reloadPage();}}), createButton({icon: BxIcon.CLOSE,style: 64 | 32,onClick: (e) => {this.dialogManager.hide();}}))), CE("div", {class: "bx-settings-tab-contents",_nearby: {orientation: "vertical",loop: (direction) => {if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;return !1;}}}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {class: "bx-settings-tab-content",_nearby: {orientation: "vertical",focus: () => this.jumpToSettingGroup("next")}})));this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();});let settingTabGroup;for (settingTabGroup in this.SETTINGS_UI) {let settingTab = this.SETTINGS_UI[settingTabGroup];if (!settingTab) continue;if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;if (settingTab.group !== "global" && !this.renderFullSettings) continue;let $svg = this.renderTab(settingTab);$tabs.appendChild($svg);let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);$tabContents.appendChild($tabContent);}$tabs.firstElementChild.dispatchEvent(new Event("click"));}focusTab(tabId) {let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);$tab && $tab.dispatchEvent(new Event("click"));}focusIfNeeded() {this.jumpToSettingGroup("next");}focusActiveTab() {let $currentTab = this.$tabs.querySelector(".bx-active");return $currentTab && $currentTab.focus(), !0;}focusVisibleSetting(type = "first") {let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));if (!controls.length) return !1;if (type === "last") controls.reverse();for (let $control of controls) {if (!($control instanceof HTMLElement)) continue;let $focusable = this.dialogManager.findFocusableElement($control);if ($focusable) {if (this.dialogManager.focus($focusable)) return !0;}}return !1;}focusVisibleTab(type = "first") {let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));if (!tabs.length) return !1;if (type === "last") tabs.reverse();for (let $tab of tabs)if (this.dialogManager.focus($tab)) return !0;return !1;}jumpToSettingGroup(direction) {let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");if (!$tabContent) return !1;let $header, $focusing = document.activeElement;if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");else {let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;while (!0) {if (!$tmp) break;if ($tmp.tagName === "H2") {if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {if (++times, direction === "next" || times >= 2) break;}}$tmp = $tmp[siblingProperty];}}let $target;if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);if ($target) return this.dialogManager.focus($target);return !1;}resetHighlightedSetting($elm) {let targetGameId = SettingsManager.getInstance().getTargetGameId();if (targetGameId < 0) return;if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");if (!$row) return;let pref = $row.prefKey;if (!pref) alert("Pref not found: " + $row.id);if (!isStreamPref(pref)) return;let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref);if (deleted) BxEventBus.Stream.emit("setting.changed", {storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,settingKey: pref});return deleted;}handleKeyPress(key) {let handled = !0;switch (key) {case "Tab":this.focusActiveTab();break;case "Home":this.focusVisibleSetting("first");break;case "End":this.focusVisibleSetting("last");break;case "PageUp":this.jumpToSettingGroup("previous");break;case "PageDown":this.jumpToSettingGroup("next");break;case "KeyQ":this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}handleGamepad(button) {let handled = !0;switch (button) {case 1:let $focusing = document.activeElement;if ($focusing && this.$tabs.contains($focusing)) this.hide();else this.focusActiveTab();break;case 4:case 5:this.focusActiveTab();break;case 6:this.jumpToSettingGroup("previous");break;case 7:this.jumpToSettingGroup("next");break;case 2:this.resetHighlightedSetting();break;default:handled = !1;break;}return handled;}} +class ScreenshotManager {static instance;static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager);LOG_TAG = "ScreenshotManager";$download;$canvas;canvasContext;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", {alpha: !1,willReadFrequently: !1});}updateCanvasSize(width, height) {this.$canvas.width = width, this.$canvas.height = height;}updateCanvasFilters(filters) {this.canvasContext.filter = filters;}onAnimationEnd(e) {e.target.classList.remove("bx-taking-screenshot");}takeScreenshot(callback) {let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas;if (!streamPlayer || !$canvas) return;let $player;if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement();else $player = streamPlayer.getPlayerElement("default");if (!$player || !$player.isConnected) return;let $gameStream = $player.closest("#game-stream");if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");let canvasContext = this.canvasContext;if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {let data = $canvas.toDataURL("image/png").split(";base64,")[1];AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();return;}$canvas.toBlob((blob) => {if (!blob) return;let now = +new Date, $download = this.$download;$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();}, "image/png");}} +class RendererShortcut {static toggleVisibility() {let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');if (!$mediaContainer) {BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 });return;}$mediaContainer.classList.toggle("bx-gone");let isVisible = !$mediaContainer.classList.contains("bx-gone");limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible });}} +class TrueAchievements {static instance;static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements);LOG_TAG = "TrueAchievements";$link;$button;$hiddenLink;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({label: t("true-achievements"),url: "#",icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64 | 8 | 128 | 8192,onClick: this.onClick}), this.$button = createButton({label: t("true-achievements"),title: t("true-achievements"),icon: BxIcon.TRUE_ACHIEVEMENTS,style: 64,onClick: this.onClick}), this.$hiddenLink = CE("a", {target: "_blank"});}onClick = (e) => {e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll();let dataset = this.$link.dataset;this.open(!0, dataset.xboxTitleId, dataset.id);};updateIds(xboxTitleId, id) {let $link = this.$link, $button = this.$button;if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId;if (id) $link.dataset.id = id, $button.dataset.id = id;}injectAchievementsProgress($elm) {if (SCRIPT_VARIANT !== "full") return;let $parent = $elm.parentElement, $div = CE("div", {class: "bx-guide-home-achievements-progress"}, $elm), xboxTitleId;try {let $container = $parent.closest("div[class*=AchievementsPreview-module__container]");if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId;} catch (e) {}if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId();if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString();if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link);else $div.appendChild(this.$button);$parent.appendChild($div);}injectAchievementDetailPage($parent) {if (SCRIPT_VARIANT !== "full") return;let props = getReactProps($parent);if (!props) return;try {let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId;for (let achiev of achievementList)if (achiev.name === achievementName) {id = achiev.id, xboxTitleId = achiev.title.id;break;}if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link);} catch (e) {}}getStreamXboxTitleId() {return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;}open(override, xboxTitleId, id) {if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId();if (AppInterface && AppInterface.openTrueAchievementsLink) {AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString());return;}let url = "https://www.trueachievements.com";if (xboxTitleId) {if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`;}this.$hiddenLink.href = url, this.$hiddenLink.click();}} +class VirtualControllerShortcut {static pressXboxButton() {let streamSession = window.BX_EXPOSED.streamSession;if (!streamSession) return;let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, {Nexus: 1,VirtualPhysicality: 1024});streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => {streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]);}, 100);}} +class ShortcutHandler {static runAction(action) {switch (action) {case "bx.settings.show":SettingsDialog.getInstance().show();break;case "stream.screenshot.capture":ScreenshotManager.getInstance().takeScreenshot();break;case "stream.video.toggle":RendererShortcut.toggleVisibility();break;case "stream.stats.toggle":StreamStats.getInstance().toggle();break;case "stream.microphone.toggle":MicrophoneShortcut.toggle();break;case "stream.menu.show":StreamUiShortcut.showHideStreamMenu();break;case "stream.sound.toggle":SoundShortcut.muteUnmute();break;case "stream.volume.inc":SoundShortcut.adjustGainNodeVolume(10);break;case "stream.volume.dec":SoundShortcut.adjustGainNodeVolume(-10);break;case "device.brightness.inc":case "device.brightness.dec":case "device.sound.toggle":case "device.volume.inc":case "device.volume.dec":AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);break;case "mkb.toggle":if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle();else EmulatedMkbHandler.getInstance()?.toggle();break;case "ta.open":TrueAchievements.getInstance().open(!1);break;case "controller.xbox.press":VirtualControllerShortcut.pressXboxButton();break;}}} +class ControllerShortcut {static buttonsCache = {};static buttonsStatus = {};static reset(index) {ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = [];}static handle(gamepad) {let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];if (!controllerSettings) return !1;let actions = controllerSettings.shortcuts;if (!actions) return !1;let gamepadIndex = gamepad.index;ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = [];let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button;for ([index, button] of entries)if (button.pressed && index !== 16) {if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {let idx = index;setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0);}}return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed;}} +var FeatureGates = {PwaPrompt: !1,EnableWifiWarnings: !1,EnableUpdateRequiredPage: !1,ShowForcedUpdateScreen: !1,EnableTakControlResizing: !0}, nativeMkbMode = getGlobalPref("nativeMkb.mode"); if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on"; var blockFeatures = getGlobalPref("block.features"); if (blockFeatures.includes("chat")) FeatureGates.EnableGuideChatTab = !1; if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers = !1; if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); -class LocalCoOpManager { -static instance; -static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager); -supportedIds; -constructor() { -BxEventBus.Script.once("list.localCoOp.updated", (e) => { -this.supportedIds = e.ids; -}), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds); -} -isSupported(productId) { -return this.supportedIds.has(productId); -} -} -var BxExposed = { -getTitleInfo: () => STATES.currentStream.titleInfo, -modifyPreloadedState: (state) => { -let LOG_TAG3 = "PreloadState"; -try { -state.appContext.requestInfo.userAgent = window.navigator.userAgent; -} catch (e) { -BxLogger.error(LOG_TAG3, e); -} -try { -for (let exp in FeatureGates) -state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp]; -} catch (e) { -BxLogger.error(LOG_TAG3, e); -} -try { -let sigls = state.xcloud.sigls; -if (STATES.userAgent.capabilities.touch) { -let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470"; -if (siglId in sigls) { -let allGames = sigls[siglId].data.products; -customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); -} else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId); -} -} catch (e) { -BxLogger.error(LOG_TAG3, e); -} -try { -let sigls = state.xcloud.sigls; -if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles); -} catch (e) { -BxLogger.error(LOG_TAG3, e); -} -try { -let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud; -if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play"; -} catch (e) { -BxLogger.error(LOG_TAG3, e); -} -return state; -}, -modifyTitleInfo: function(titleInfo) { -titleInfo = deepClone(titleInfo); -let supportedInputTypes = titleInfo.details.supportedInputTypes; -if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); -if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); -if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { -let touchControllerAvailability = getGlobalPref("touchController.mode"); -if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) { -let gamepads = window.navigator.getGamepads(), gamepadFound = !1; -for (let gamepad of gamepads) -if (gamepad && gamepad.connected) { -gamepadFound = !0; -break; -} -gamepadFound && (touchControllerAvailability = "off"); -} -if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = []; -if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch"); -} -return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo; -}, -setupGainNode: ($media, audioStream) => { -if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => { -$media.muted = !0, $media.pause(); -}); -else $media.muted = !0, $media.addEventListener("playing", (e) => { -$media.muted = !0; -}); -try { -let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain(); -source.connect(gainNode).connect(audioCtx.destination); -} catch (e) { -BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null; -} -}, -handleControllerShortcut: ControllerShortcut.handle, -resetControllerShortcut: ControllerShortcut.reset, -overrideSettings: { -Tv_settings: { -hasCompletedOnboarding: !0 -} -}, -disableGamepadPolling: !1, -backButtonPressed: () => { -let navigationDialogManager = NavigationDialogManager.getInstance(); -if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; -let dict = { -bubbles: !0, -cancelable: !0, -key: "XF86Back", -code: "XF86Back", -keyCode: 4, -which: 4 -}; -return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1; -}, -GameSlugRegexes: [ -/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, -/ {2,}/g, -/ /g -], -toggleLocalCoOp(enable) {}, -beforePageLoad: (page) => { -BxLogger.info("beforePageLoad", page), Patcher.patchPage(page); -}, -localCoOpManager: LocalCoOpManager.getInstance(), -reactCreateElement: function(...args) {}, -createReactLocalCoOpIcon: (attrs) => { -let reactCE = window.BX_EXPOSED.reactCreateElement; -return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" }))); -} -}; -function localRedirect(path) { -let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); -if (!$pageContent) return; -let $anchor = CE("a", { -href: url, -class: "bx-hidden bx-offscreen" -}, ""); -$anchor.addEventListener("click", (e) => { -window.setTimeout(() => { -$pageContent.removeChild($anchor); -}, 1000); -}), $pageContent.appendChild($anchor), $anchor.click(); -} +class LocalCoOpManager {static instance;static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager);supportedIds;constructor() {BxEventBus.Script.once("list.localCoOp.updated", (e) => {this.supportedIds = e.ids;}), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds);}isSupported(productId) {return this.supportedIds.has(productId);}} +var BxExposed = {getTitleInfo: () => STATES.currentStream.titleInfo,modifyPreloadedState: (state) => {let LOG_TAG3 = "PreloadState";try {state.appContext.requestInfo.userAgent = window.navigator.userAgent;} catch (e) {BxLogger.error(LOG_TAG3, e);}try {for (let exp in FeatureGates)state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (STATES.userAgent.capabilities.touch) {let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470";if (siglId in sigls) {let allGames = sigls[siglId].data.products;customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);} else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId);}} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let sigls = state.xcloud.sigls;if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);} catch (e) {BxLogger.error(LOG_TAG3, e);}try {let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";} catch (e) {BxLogger.error(LOG_TAG3, e);}return state;},modifyTitleInfo: function(titleInfo) {titleInfo = deepClone(titleInfo);let supportedInputTypes = titleInfo.details.supportedInputTypes;if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB");if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB");if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) {let touchControllerAvailability = getGlobalPref("touchController.mode");if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) {let gamepads = window.navigator.getGamepads(), gamepadFound = !1;for (let gamepad of gamepads)if (gamepad && gamepad.connected) {gamepadFound = !0;break;}gamepadFound && (touchControllerAvailability = "off");}if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = [];if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch");}return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo;},setupGainNode: ($media, audioStream) => {if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0, $media.pause();});else $media.muted = !0, $media.addEventListener("playing", (e) => {$media.muted = !0;});try {let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();source.connect(gainNode).connect(audioCtx.destination);} catch (e) {BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;}},handleControllerShortcut: ControllerShortcut.handle,resetControllerShortcut: ControllerShortcut.reset,overrideSettings: {Tv_settings: {hasCompletedOnboarding: !0}},disableGamepadPolling: !1,backButtonPressed: () => {let navigationDialogManager = NavigationDialogManager.getInstance();if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;let dict = {bubbles: !0,cancelable: !0,key: "XF86Back",code: "XF86Back",keyCode: 4,which: 4};return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;},GameSlugRegexes: [/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g,/ {2,}/g,/ /g],toggleLocalCoOp(enable) {},beforePageLoad: (page) => {BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);},localCoOpManager: LocalCoOpManager.getInstance(),reactCreateElement: function(...args) {},createReactLocalCoOpIcon: (attrs) => {let reactCE = window.BX_EXPOSED.reactCreateElement;return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" })));}}; +function localRedirect(path) {let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");if (!$pageContent) return;let $anchor = CE("a", {href: url,class: "bx-hidden bx-offscreen"}, "");$anchor.addEventListener("click", (e) => {window.setTimeout(() => {$pageContent.removeChild($anchor);}, 1000);}), $pageContent.appendChild($anchor), $anchor.click();} window.localRedirect = localRedirect; -function getPreferredServerRegion(shortName = !1) { -let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions; -if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName; -else return preferredRegion; -for (let regionName in serverRegions) { -let region = serverRegions[regionName]; -if (!region.isDefault) continue; -if (shortName && region.shortName) return region.shortName; -else return regionName; -} -return null; -} -class HeaderSection { -static instance; -static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection); -LOG_TAG = "HeaderSection"; -$btnRemotePlay; -$btnSettings; -$buttonsWrapper; -observer; -timeoutId; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({ -classes: ["bx-header-remote-play-button", "bx-gone"], -icon: BxIcon.REMOTE_PLAY, -title: t("remote-play"), -style: 8 | 64 | 2048, -onClick: (e) => RemotePlayManager.getInstance()?.togglePopup() -}), this.$btnSettings = createButton({ -classes: ["bx-header-settings-button"], -label: "???", -style: 16 | 32 | 64 | 256, -onClick: (e) => SettingsDialog.getInstance().show() -}), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings); -} -injectSettingsButton($parent) { -if (!$parent) return; -let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings; -if (isElementVisible(this.$buttonsWrapper)) return; -if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true"); -$parent.appendChild(this.$buttonsWrapper); -} -checkHeader = () => { -let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); -if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); -$target && this.injectSettingsButton($target); -}; -watchHeader() { -let $root = document.querySelector("#PageContent header") || document.querySelector("#root"); -if (!$root) return; -this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => { -this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000); -}), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader(); -} -showRemotePlayButton() { -this.$btnRemotePlay?.classList.remove("bx-gone"); -} -static watchHeader() { -HeaderSection.getInstance().watchHeader(); -} -} -class RemotePlayDialog extends NavigationDialog { -static instance; -static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog); -LOG_TAG = "RemotePlayNavigationDialog"; -STATE_LABELS = { -On: t("powered-on"), -Off: t("powered-off"), -ConnectedStandby: t("standby"), -Unknown: t("unknown") -}; -$container; -constructor() { -super(); -BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog(); -} -setupDialog() { -let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p")); -$resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => { -let value = e.target.value; -$settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui"); -}), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { -manualTrigger: !0 -}); -let $qualitySettings = CE("div", { -class: "bx-remote-play-settings" -}, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions)); -$fragment.appendChild($qualitySettings); -let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); -for (let con of consoles) { -let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({ -classes: ["bx-remote-play-connect-button"], -label: t("console-connect"), -style: 1 | 64, -onClick: (e) => manager.play(con.serverId) -})); -$fragment.appendChild($child); -} -$fragment.appendChild(CE("div", { -class: "bx-remote-play-buttons", -_nearby: { -orientation: "horizontal" -} -}, createButton({ -icon: BxIcon.QUESTION, -style: 8 | 64, -url: "https://better-xcloud.github.io/remote-play", -label: t("help") -}), createButton({ -style: 8 | 64, -label: t("close"), -onClick: (e) => this.hide() -}))), this.$container = $fragment; -} -getDialog() { -return this; -} -getContent() { -return this.$container; -} -focusIfNeeded() { -let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); -$btnConnect && $btnConnect.focus(); -} -} -class RemotePlayManager { -static instance; -static getInstance() { -if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager; -else RemotePlayManager.instance = null; -return RemotePlayManager.instance; -} -LOG_TAG = "RemotePlayManager"; -isInitialized = !1; -XCLOUD_TOKEN; -XHOME_TOKEN; -consoles; -regions = []; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"); -} -initialize() { -if (this.isInitialized) return; -this.isInitialized = !0, this.requestXhomeToken(() => { -this.getConsolesList(() => { -BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); -}); -}); -} -getXcloudToken() { -return this.XCLOUD_TOKEN; -} -setXcloudToken(token) { -this.XCLOUD_TOKEN = token; -} -getXhomeToken() { -return this.XHOME_TOKEN; -} -getConsoles() { -return this.consoles; -} -requestXhomeToken(callback) { -if (this.XHOME_TOKEN) { -callback(); -return; -} -let GSSV_TOKEN; -try { -GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token; -} catch (e) { -for (let i = 0;i < localStorage.length; i++) { -let key = localStorage.key(i); -if (!key.startsWith("Auth.User.")) continue; -let json = JSON.parse(localStorage.getItem(key)); -for (let token of json.tokens) { -if (!token.relyingParty.includes("gssv.xboxlive.com")) continue; -GSSV_TOKEN = token.tokenData.token; -break; -} -break; -} -} -let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { -method: "POST", -body: JSON.stringify({ -offeringId: "xhome", -token: GSSV_TOKEN -}), -headers: { -"Content-Type": "application/json; charset=utf-8" -} -}); -fetch(request).then((resp) => resp.json()).then((json) => { -this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback(); -}); -} -async getConsolesList(callback) { -if (this.consoles) { -callback(); -return; -} -let options = { -method: "GET", -headers: { -Authorization: `Bearer ${this.XHOME_TOKEN}` -} -}; -for (let region of this.regions) -try { -let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json(); -if (json.results.length === 0) continue; -this.consoles = json.results, STATES.remotePlay.server = region.baseUri; -break; -} catch (e) {} -if (!STATES.remotePlay.server) this.consoles = []; -callback(); -} -play(serverId, resolution) { -if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui"); -STATES.remotePlay.config = { -serverId -}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); -} -togglePopup(force = null) { -if (!this.isReady()) { -Toast.show(t("getting-consoles-list")); -return; -} -if (this.consoles.length === 0) { -Toast.show(t("no-consoles-found"), "", { instant: !0 }); -return; -} -RemotePlayDialog.getInstance().show(); -} -static detect() { -if (!getGlobalPref("xhome.enabled")) return; -if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play"); -else window.BX_REMOTE_PLAY_CONFIG = null; -} -isReady() { -return this.consoles !== null; -} -} -class XhomeInterceptor { -static consoleAddrs = {}; -static async handleLogin(request) { -try { -let obj = await request.clone().json(); -obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { -method: "POST", -body: JSON.stringify(obj), -headers: { -"Content-Type": "application/json" -} -}); -} catch (e) { -alert(e), console.log(e); -} -return NATIVE_FETCH(request); -} -static async handleConfiguration(request) { -BxEventBus.Stream.emit("state.starting", {}); -let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [ -["ipAddress", "port"], -["ipV4Address", "ipV4Port"], -["ipV6Address", "ipV6Port"] -]; -XhomeInterceptor.consoleAddrs = {}; -for (let pair of pairs) { -let [keyAddr, keyPort] = pair; -if (keyAddr && keyPort && serverDetails[keyAddr]) { -let port = serverDetails[keyPort], ports = new Set; -port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports); -} -} -return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; -} -static async handleInputConfigs(request, opts) { -let response = await NATIVE_FETCH(request); -if (getGlobalPref("touchController.mode") !== "all") return response; -let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; -TouchController.setXboxTitleId(xboxTitleId); -let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0; -if (!hasTouchSupport) { -let supportedInputTypes = inputConfigs.supportedInputTypes; -hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay"); -} -if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { -data: null -}); -else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId); -return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; -} -static async handleTitles(request) { -let clone = request.clone(), headers = {}; -for (let pair of clone.headers.entries()) -headers[pair[0]] = pair[1]; -headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`; -let index = request.url.indexOf(".xboxlive.com"); -return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), { -method: clone.method, -body: await clone.text(), -headers -}), NATIVE_FETCH(request); -} -static async handlePlay(request) { -BxEventBus.Stream.emit("state.loading", {}); -let body = await request.clone().json(), newRequest = new Request(request, { -body: JSON.stringify(body) -}); -return NATIVE_FETCH(newRequest); -} -static async handle(request) { -TouchController.disable(); -let clone = request.clone(), headers = {}; -for (let pair of clone.headers.entries()) -headers[pair[0]] = pair[1]; -headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`; -let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution")); -headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)); -let opts = { -method: clone.method, -headers -}; -if (clone.method === "POST") opts.body = await clone.text(); -let url = request.url; -if (!url.includes("/servers/home")) { -let parsed = new URL(url); -url = STATES.remotePlay.server + parsed.pathname; -} -if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request); -else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request); -else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts); -else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request); -else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request); -else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs); -return await NATIVE_FETCH(request); -} -} -class LoadingScreen { -static $bgStyle; -static $waitTimeBox; -static waitTimeInterval = null; -static orgWebTitle; -static secondsToString(seconds) { -let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0"); -return mDisplay + sDisplay; -} -static setup() { -let titleInfo = STATES.currentStream.titleInfo; -if (!titleInfo) return; -if (!LoadingScreen.$bgStyle) { -let $bgStyle = CE("style"); -document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; -} -if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket(); -} -static hideRocket() { -let $bgStyle = LoadingScreen.$bgStyle; -$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}"; -} -static setBackground(imageUrl) { -let $bgStyle = LoadingScreen.$bgStyle; -imageUrl = imageUrl + "?w=1920"; -let imageQuality = getGlobalPref("ui.imageQuality"); -if (imageQuality !== 90) imageUrl += "&q=" + imageQuality; -$bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`; -let bg = new Image; -bg.onload = (e) => { -$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; -}, bg.src = imageUrl; -} -static setupWaitTime(waitTime) { -if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket(); -let secondsLeft = waitTime, $countDown, $estimated; -LoadingScreen.orgWebTitle = document.title; -let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; -endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds); -let endDateStr = endDate.toISOString().slice(0, 19); -endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`; -let $waitTimeBox = LoadingScreen.$waitTimeBox; -if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox; -else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown"); -$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => { -if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null; -}, 1000); -} -static hide() { -if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) { -let $rocketBg = document.querySelector('#game-stream rect[width="800"]'); -$rocketBg && $rocketBg.addEventListener("transitionend", (e) => { -LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}"; -}), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}'; -} -setTimeout(LoadingScreen.reset, 2000); -} -static reset() { -LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null; -} -} -class GuideMenu { -static instance; -static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu); -$renderedButtons; -closeGuideMenu() { -if (window.BX_EXPOSED.dialogRoutes) { -window.BX_EXPOSED.dialogRoutes.closeAll(); -return; -} -let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); -$btnClose && $btnClose.click(); -} -renderButtons() { -if (this.$renderedButtons) return this.$renderedButtons; -let buttons = { -scriptSettings: createButton({ -label: t("better-xcloud"), -icon: BxIcon.BETTER_XCLOUD, -style: 128 | 64 | 1, -onClick: () => { -BxEventBus.Script.once("dialog.dismissed", () => { -setTimeout(() => SettingsDialog.getInstance().show(), 50); -}), this.closeGuideMenu(); -} -}), -closeApp: AppInterface && createButton({ -icon: BxIcon.POWER, -label: t("close-app"), -title: t("close-app"), -style: 128 | 64 | 4, -onClick: (e) => { -AppInterface.closeApp(); -}, -attributes: { -"data-state": "normal" -} -}), -reloadPage: createButton({ -icon: BxIcon.REFRESH, -label: t("reload-page"), -title: t("reload-page"), -style: 128 | 64, -onClick: () => { -if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); -else window.location.reload(); -} -}), -backToHome: createButton({ -icon: BxIcon.HOME, -label: t("back-to-home"), -title: t("back-to-home"), -style: 128 | 64, -onClick: () => { -this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)); -}, -attributes: { -"data-state": "playing" -} -}) -}, buttonsLayout = [ -buttons.scriptSettings, -[ -buttons.backToHome, -buttons.reloadPage, -buttons.closeApp -] -], $div = CE("div", { -class: "bx-guide-home-buttons" -}); -if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv"; -for (let $button of buttonsLayout) { -if (!$button) continue; -if ($button instanceof HTMLElement) $div.appendChild($button); -else if (Array.isArray($button)) { -let $wrapper = CE("div", {}); -for (let $child of $button) -$child && $wrapper.appendChild($child); -$div.appendChild($wrapper); -} -} -return this.$renderedButtons = $div, $div; -} -injectHome($root, isPlaying = !1) { -{ -let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); -if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress); -} -let $target = null; -if (isPlaying) { -$target = $root.querySelector("a[class*=QuitGameButton]"); -let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); -$btnXcloudHome && ($btnXcloudHome.style.display = "none"); -} else { -let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); -if ($dividers) $target = $dividers[$dividers.length - 1]; -} -if (!$target) return !1; -let $buttons = this.renderButtons(); -$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); -} -onShown = async (e) => { -if (e.where === "home") { -let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); -$root && this.injectHome($root, STATES.isPlaying); -} -}; -addEventListeners() { -window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown); -} -observe($addedElm) { -let className = $addedElm.className; -if (!className) className = $addedElm.firstElementChild?.className ?? ""; -if (!className || className.startsWith("bx-")) return; -if (className.includes("AchievementsButton-module__progressBarContainer")) { -TrueAchievements.getInstance().injectAchievementsProgress($addedElm); -return; -} -if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; -{ -let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); -if ($achievDetailPage) { -TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage); -return; -} -} -let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true"); -if ($selectedTab) { -let $elm = $selectedTab, index; -for (index = 0;$elm = $elm?.previousElementSibling; index++) -; -if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" }); -} -} -} -class StreamBadges { -static instance; -static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges); -LOG_TAG = "StreamBadges"; -serverInfo = {}; -badges = { -playtime: { -name: t("playtime"), -icon: BxIcon.PLAYTIME, -color: "#ff004d" -}, -battery: { -name: t("battery"), -icon: BxIcon.BATTERY, -color: "#00b543" -}, -download: { -name: t("download"), -icon: BxIcon.DOWNLOAD, -color: "#29adff" -}, -upload: { -name: t("upload"), -icon: BxIcon.UPLOAD, -color: "#ff77a8" -}, -server: { -name: t("server"), -icon: BxIcon.SERVER, -color: "#ff6c24" -}, -video: { -name: t("video"), -icon: BxIcon.DISPLAY, -color: "#742f29" -}, -audio: { -name: t("audio"), -icon: BxIcon.AUDIO, -color: "#5f574f" -} -}; -$container; -intervalId; -REFRESH_INTERVAL = 3000; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"); -} -setRegion(region) { -this.serverInfo.server = { -region -}; -} -renderBadge(name, value) { -let badgeInfo = this.badges[name], $badge; -if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge; -if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery"); -return this.badges[name].$element = $badge, $badge; -} -updateBadges = async (forceUpdate = !1) => { -if (!this.$container || !forceUpdate && !this.$container.isConnected) { -this.stop(); -return; -} -let statsCollector = StreamStatsCollector.getInstance(); -await statsCollector.collect(); -let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = { -download: dl.toString(), -upload: ul.toString(), -playtime: play.toString(), -battery: batt.toString() -}, name; -for (name in badges) { -let value = badges[name]; -if (value === null) continue; -let $elm = this.badges[name].$element; -if (!$elm) continue; -if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone"); -else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone"); -} -}; -async start() { -await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL); -} -stop() { -this.intervalId && clearInterval(this.intervalId), this.intervalId = null; -} -destroy() { -this.serverInfo = {}, delete this.$container; -} -async render() { -if (this.$container) return this.start(), this.$container; -await this.getServerStats(); -let batteryLevel = ""; -if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; -let BADGES = [ -["playtime", "1m"], -["battery", batteryLevel], -["download", humanFileSize(0)], -["upload", humanFileSize(0)], -this.badges.server.$element ?? ["server", "?"], -this.serverInfo.video ? this.badges.video.$element : ["video", "?"], -this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"] -], $container = CE("div", { class: "bx-badges" }); -for (let item2 of BADGES) { -if (!item2) continue; -let $badge; -if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2); -else $badge = item2; -$container.appendChild($badge); -} -return this.$container = $container, await this.start(), $container; -} -async getServerStats() { -let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId; -if (stats.forEach((stat) => { -if (stat.type === "codec") { -let mimeType = stat.mimeType.split("/")[0]; -if (mimeType === "video") allVideoCodecs[stat.id] = stat; -else if (mimeType === "audio") allAudioCodecs[stat.id] = stat; -} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) { -if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight; -else if (stat.kind === "audio") audioCodecId = stat.codecId; -} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId; -else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId; -else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address; -}), videoCodecId) { -let videoStat = allVideoCodecs[videoCodecId], video = { -width: videoWidth, -height: videoHeight, -codec: videoStat.mimeType.substring(6) -}; -if (video.codec === "H264") { -let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine); -match && (video.profile = match[1]); -} -let text = videoHeight + "p"; -if (text && (text += "/"), text += video.codec, video.profile) { -let profile = video.profile, quality = profile; -if (profile.startsWith("4d")) quality = t("visual-quality-high"); -else if (profile.startsWith("42e")) quality = t("visual-quality-normal"); -else if (profile.startsWith("420")) quality = t("visual-quality-low"); -text += ` (${quality})`; -} -this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video; -} -if (audioCodecId) { -let audioStat = allAudioCodecs[audioCodecId], audio = { -codec: audioStat.mimeType.substring(6), -bitrate: audioStat.clockRate -}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`; -this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio; -} -if (candidatePairId) { -BxLogger.info("candidate", candidatePairId, allCandidatePairs); -let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server; -if (server && server.region) text += server.region; -text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); -} -} -static setupEvents() {} -} -class XcloudInterceptor { -static SERVER_EXTRA_INFO = { -EastUS: ["🇺🇸", "america-north"], -EastUS2: ["🇺🇸", "america-north"], -NorthCentralUs: ["🇺🇸", "america-north"], -SouthCentralUS: ["🇺🇸", "america-north"], -WestUS: ["🇺🇸", "america-north"], -WestUS2: ["🇺🇸", "america-north"], -MexicoCentral: ["🇲🇽", "america-north"], -BrazilSouth: ["🇧🇷", "america-south"], -JapanEast: ["🇯🇵", "asia"], -KoreaCentral: ["🇰🇷", "asia"], -AustraliaEast: ["🇦🇺", "australia"], -AustraliaSouthEast: ["🇦🇺", "australia"], -SwedenCentral: ["🇸🇪", "europe"], -UKSouth: ["🇬🇧", "europe"], -WestEurope: ["🇪🇺", "europe"] -}; -static async handleLogin(request, init) { -let bypassServer = getGlobalPref("server.bypassRestriction"); -if (bypassServer !== "off") { -let ip = BypassServerIps[bypassServer]; -ip && request.headers.set("X-Forwarded-For", ip); -} -let response = await NATIVE_FETCH(request, init); -if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response; -let obj = await response.clone().json(); -RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); -let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region; -for (region of obj.offeringSettings.regions) { -let { name: regionName, name: shortName } = region; -if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); -let match = serverRegex.exec(region.baseUri); -if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1]; -else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName); -region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); -} -BxEventBus.Script.emit("xcloud.server.ready", {}); -let preferredRegion = getPreferredServerRegion(); -if (preferredRegion && preferredRegion in STATES.serverRegions) { -let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); -tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; -} -return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; -} -static async handlePlay(request, init) { -BxEventBus.Stream.emit("state.loading", {}); -let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0]; -for (let regionName in STATES.serverRegions) { -let region = STATES.serverRegions[regionName]; -if (region && parsedUrl.origin === region.baseUri) { -badgeRegion = regionName; -break; -} -} -StreamBadges.getInstance().setRegion(badgeRegion); -let clone = request.clone(), body = await clone.json(), headers = {}; -for (let pair of clone.headers.entries()) -headers[pair[0]] = pair[1]; -if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { -let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION); -headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName; -} -if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; -let newRequest = new Request(request, { -body: JSON.stringify(body), -headers -}); -return NATIVE_FETCH(newRequest); -} -static async handleWaitTime(request, init) { -let response = await NATIVE_FETCH(request, init); -if (getGlobalPref("loadingScreen.waitTime.show")) { -let json = await response.clone().json(); -if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); -} -return response; -} -static async handleConfiguration(request, init) { -if (request.method !== "GET") return NATIVE_FETCH(request, init); -if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); -else TouchController.enable(); -let response = await NATIVE_FETCH(request, init), text = await response.clone().text(); -if (!text.length) return response; -BxEventBus.Stream.emit("state.starting", {}); -let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; -overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; -let overrideMkb = null; -if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; -if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1; -if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { -enableMouseInput: overrideMkb, -enableKeyboardInput: overrideMkb -}); -if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10; -if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0; -return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; -} -static async handle(request, init) { -let url = typeof request === "string" ? request : request.url; -if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init); -else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init); -else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init); -else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init); -else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request); -return NATIVE_FETCH(request, init); -} -} -function clearApplicationInsightsBuffers() { -window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer"); -} -function clearDbLogs(dbName, table) { -let request = window.indexedDB.open(dbName); -request.onsuccess = (e) => { -let db = e.target.result; -try { -let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); -objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`); -} catch (ex) {} -}; -} -function clearAllLogs() { -clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); -} -function updateIceCandidates(candidates, options) { -let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; -for (let item2 of candidates) { -if (item2.candidate == "a=end-of-candidates") continue; -let groups = pattern.exec(item2.candidate).groups; -lst.push(groups); -} -if (options.preferIpv6Server) lst.sort((a, b) => { -let firstIp = a.ip, secondIp = b.ip; -return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; -}); -let newCandidates = [], foundation = 1, newCandidate = (candidate) => { -return { -candidate, -messageType: "iceCandidate", -sdpMLineIndex: "0", -sdpMid: "0" -}; -}; -if (lst.forEach((item2) => { -item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation; -}), options.consoleAddrs) -for (let ip in options.consoleAddrs) -for (let port of options.consoleAddrs[ip]) -newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`)); -return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates; -} -async function patchIceCandidates(request, consoleAddrs) { -let response = await NATIVE_FETCH(request), text = await response.clone().text(); -if (!text.length) return response; -let options = { -preferIpv6Server: getGlobalPref("server.ipv6.prefer"), -consoleAddrs -}, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse); -return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; -} -function interceptHttpRequests() { -let BLOCKED_URLS = []; -if (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net"); -let blockFeatures2 = getGlobalPref("block.features"); -if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox"); -if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations"); -if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/"); -let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send; -xhrPrototype.open = function(method, url) { -return this._url = url, nativeXhrOpen.apply(this, arguments); -}, xhrPrototype.send = function(...arg) { -for (let url of BLOCKED_URLS) -if (this._url.startsWith(url)) { -if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); -return BxLogger.warning("Blocked URL", url), !1; -} -return nativeXhrSend.apply(this, arguments); -}; -let gamepassAllGames = [], IGNORED_DOMAINS = [ -"accounts.xboxlive.com", -"chat.xboxlive.com", -"notificationinbox.xboxlive.com", -"peoplehub.xboxlive.com", -"peoplehub-public.xboxlive.com", -"rta.xboxlive.com", -"userpresence.xboxlive.com", -"xblmessaging.xboxlive.com", -"consent.config.office.com", -"arc.msn.com", -"browser.events.data.microsoft.com", -"dc.services.visualstudio.com", -"2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" -]; -window.BX_FETCH = window.fetch = async (request, init) => { -let url = typeof request === "string" ? request : request.url; -for (let blocked of BLOCKED_URLS) -if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', { -status: 200, -statusText: "200 OK" -}); -try { -let domain = new URL(url).hostname; -if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init); -} catch (e) { -return NATIVE_FETCH(request, init); -} -if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { -let response = await NATIVE_FETCH(request, init), json = await response.json(); -if (json && json.exp && json.exp.treatments) for (let key in FeatureGates) -json.exp.treatments[key] = FeatureGates[key]; -return response.json = () => Promise.resolve(json), response; -} catch (e) { -return console.log(e), NATIVE_FETCH(request, init); -} -if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { -let response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); -if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++) -gamepassAllGames.push(obj[i].id); -else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try { -let customList = TouchController.getCustomList(); -customList = customList.filter((id) => gamepassAllGames.includes(id)); -let newCustomList = customList.map((item2) => ({ id: item2 })); -obj.push(...newCustomList); -} catch (e) { -console.log(e); -} -return response.json = () => Promise.resolve(obj), response; -} -if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) { -let response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); -try { -let newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 })); -obj.push(...newCustomList); -} catch (e) { -console.log(e); -} -return response.json = () => Promise.resolve(obj), response; -} -let requestType; -if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome"; -else requestType = "xcloud"; -if (requestType === "xhome") return XhomeInterceptor.handle(request); -return XcloudInterceptor.handle(request, init); -}; -} -function generateMsDeviceInfo(osName) { -return { -appInfo: { -env: { -clientAppId: window.location.host, -clientAppType: "browser", -clientAppVersion: "26.1.97", -clientSdkVersion: "10.3.7", -httpEnvironment: "prod", -sdkInstallId: "" -} -}, -dev: { -os: { name: osName, ver: "22631.2715", platform: "desktop" }, -hw: { make: "Microsoft", model: "unknown", sdktype: "web" }, -browser: { browserName: "chrome", browserVersion: "130.0" }, -displayInfo: { -dimensions: { widthInPixels: 1920, heightInPixels: 1080 }, -pixelDensity: { dpiX: 1, dpiY: 1 } -} -} -}; -} -function getOsNameFromResolution(resolution) { -let osName; -switch (resolution) { -case "1080p-hq": -osName = "tizen"; -break; -case "1080p": -osName = "windows"; -break; -default: -osName = "android"; -break; -} -return osName; -} -function addCss() { -let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = []; -if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); -if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]"); -if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]"); -if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])'); -if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])'); -if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); -if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; -if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; -if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}"; -if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}"; -else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}"; -if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; -let $style = CE("style", !1, css); -document.documentElement.appendChild($style); -} -function preloadFonts() { -let $link = CE("link", { -rel: "preload", -href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf", -as: "font", -type: "font/otf", -crossorigin: "" -}); -document.querySelector("head")?.appendChild($link); -} -class MouseCursorHider { -static instance; -static getInstance() { -if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider; -else MouseCursorHider.instance = null; -return MouseCursorHider.instance; -} -timeoutId; -isCursorVisible = !0; -show() { -document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0; -} -hide() { -document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1; -} -onMouseMove = (e) => { -!this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000); -}; -start() { -this.show(), document.addEventListener("mousemove", this.onMouseMove); -} -stop() { -this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show(); -} -} -function patchHistoryMethod(type) { -let orig = window.history[type]; -return function(...args) { -return BxEvent.dispatch(window, BxEvent.POPSTATE, { -arguments: args -}), orig.apply(this, arguments); -}; -} -function onHistoryChanged(e) { -if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; -window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {}); -} -function setCodecPreferences(sdp, preferredCodec) { -let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || []; -for (let match of matches) { -let id = match[1]; -if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); -} -if (!preferredCodecIds.length) return sdp; -let lines = sdp.split(`\r -`); -for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { -let line = lines[lineIndex]; -if (!line.startsWith("m=video")) continue; -let tmp = line.trim().split(" "), ids = tmp.slice(3); -ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); -break; -} -return lines.join(`\r -`); -} -function patchSdpBitrate(sdp, video, audio) { -let lines = sdp.split(`\r -`), mediaSet = new Set; -!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); -let bitrate = { -video, -audio -}; -for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) { -let media = "", line = lines[lineNumber]; -if (!line.startsWith("m=")) continue; -for (let m of mediaSet) -if (line.startsWith(`m=${m}`)) { -media = m, mediaSet.delete(media); -break; -} -if (!media) continue; -let bLine = `b=AS:${bitrate[media]}`; -while (lineNumber++, lineNumber < lines.length) { -if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue; -if (line.startsWith("b=AS:")) { -lines[lineNumber] = bLine; -break; -} -if (line.startsWith("m=")) { -lines.splice(lineNumber, 0, bLine); -break; -} -} -} -return lines.join(`\r -`); -} +function getPreferredServerRegion(shortName = !1) {let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions;if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;else return preferredRegion;for (let regionName in serverRegions) {let region = serverRegions[regionName];if (!region.isDefault) continue;if (shortName && region.shortName) return region.shortName;else return regionName;}return null;} +class HeaderSection {static instance;static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);LOG_TAG = "HeaderSection";$btnRemotePlay;$btnSettings;$buttonsWrapper;observer;timeoutId;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({classes: ["bx-header-remote-play-button", "bx-gone"],icon: BxIcon.REMOTE_PLAY,title: t("remote-play"),style: 8 | 64 | 2048,onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()}), this.$btnSettings = createButton({classes: ["bx-header-settings-button"],label: "???",style: 16 | 32 | 64 | 256,onClick: (e) => SettingsDialog.getInstance().show()}), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);}injectSettingsButton($parent) {if (!$parent) return;let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings;if (isElementVisible(this.$buttonsWrapper)) return;if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");$parent.appendChild(this.$buttonsWrapper);}checkHeader = () => {let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");$target && this.injectSettingsButton($target);};watchHeader() {let $root = document.querySelector("#PageContent header") || document.querySelector("#root");if (!$root) return;this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);}), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();}showRemotePlayButton() {this.$btnRemotePlay?.classList.remove("bx-gone");}static watchHeader() {HeaderSection.getInstance().watchHeader();}} +class RemotePlayDialog extends NavigationDialog {static instance;static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog);LOG_TAG = "RemotePlayNavigationDialog";STATE_LABELS = {On: t("powered-on"),Off: t("powered-off"),ConnectedStandby: t("standby"),Unknown: t("unknown")};$container;constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();}setupDialog() {let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p"));$resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => {let value = e.target.value;$settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui");}), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", {manualTrigger: !0});let $qualitySettings = CE("div", {class: "bx-remote-play-settings"}, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions));$fragment.appendChild($qualitySettings);let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles();for (let con of consoles) {let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({classes: ["bx-remote-play-connect-button"],label: t("console-connect"),style: 1 | 64,onClick: (e) => manager.play(con.serverId)}));$fragment.appendChild($child);}$fragment.appendChild(CE("div", {class: "bx-remote-play-buttons",_nearby: {orientation: "horizontal"}}, createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: "https://better-xcloud.github.io/remote-play",label: t("help")}), createButton({style: 8 | 64,label: t("close"),onClick: (e) => this.hide()}))), this.$container = $fragment;}getDialog() {return this;}getContent() {return this.$container;}focusIfNeeded() {let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button");$btnConnect && $btnConnect.focus();}} +class RemotePlayManager {static instance;static getInstance() {if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager;else RemotePlayManager.instance = null;return RemotePlayManager.instance;}LOG_TAG = "RemotePlayManager";isInitialized = !1;XCLOUD_TOKEN;XHOME_TOKEN;consoles;regions = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}initialize() {if (this.isInitialized) return;this.isInitialized = !0, this.requestXhomeToken(() => {this.getConsolesList(() => {BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);});});}getXcloudToken() {return this.XCLOUD_TOKEN;}setXcloudToken(token) {this.XCLOUD_TOKEN = token;}getXhomeToken() {return this.XHOME_TOKEN;}getConsoles() {return this.consoles;}requestXhomeToken(callback) {if (this.XHOME_TOKEN) {callback();return;}let GSSV_TOKEN;try {GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token;} catch (e) {for (let i = 0;i < localStorage.length; i++) {let key = localStorage.key(i);if (!key.startsWith("Auth.User.")) continue;let json = JSON.parse(localStorage.getItem(key));for (let token of json.tokens) {if (!token.relyingParty.includes("gssv.xboxlive.com")) continue;GSSV_TOKEN = token.tokenData.token;break;}break;}}let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify({offeringId: "xhome",token: GSSV_TOKEN}),headers: {"Content-Type": "application/json; charset=utf-8"}});fetch(request).then((resp) => resp.json()).then((json) => {this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback();});}async getConsolesList(callback) {if (this.consoles) {callback();return;}let options = {method: "GET",headers: {Authorization: `Bearer ${this.XHOME_TOKEN}`}};for (let region of this.regions)try {let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();if (json.results.length === 0) continue;this.consoles = json.results, STATES.remotePlay.server = region.baseUri;break;} catch (e) {}if (!STATES.remotePlay.server) this.consoles = [];callback();}play(serverId, resolution) {if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui");STATES.remotePlay.config = {serverId}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play");}togglePopup(force = null) {if (!this.isReady()) {Toast.show(t("getting-consoles-list"));return;}if (this.consoles.length === 0) {Toast.show(t("no-consoles-found"), "", { instant: !0 });return;}RemotePlayDialog.getInstance().show();}static detect() {if (!getGlobalPref("xhome.enabled")) return;if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");else window.BX_REMOTE_PLAY_CONFIG = null;}isReady() {return this.consoles !== null;}} +class XhomeInterceptor {static consoleAddrs = {};static async handleLogin(request) {try {let obj = await request.clone().json();obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify(obj),headers: {"Content-Type": "application/json"}});} catch (e) {alert(e), console.log(e);}return NATIVE_FETCH(request);}static async handleConfiguration(request) {BxEventBus.Stream.emit("state.starting", {});let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [["ipAddress", "port"],["ipV4Address", "ipV4Port"],["ipV6Address", "ipV6Port"]];XhomeInterceptor.consoleAddrs = {};for (let pair of pairs) {let [keyAddr, keyPort] = pair;if (keyAddr && keyPort && serverDetails[keyAddr]) {let port = serverDetails[keyPort], ports = new Set;port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);}}return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleInputConfigs(request, opts) {let response = await NATIVE_FETCH(request);if (getGlobalPref("touchController.mode") !== "all") return response;let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];TouchController.setXboxTitleId(xboxTitleId);let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;if (!hasTouchSupport) {let supportedInputTypes = inputConfigs.supportedInputTypes;hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");}if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {data: null});else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleTitles(request) {let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;let index = request.url.indexOf(".xboxlive.com");return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {method: clone.method,body: await clone.text(),headers}), NATIVE_FETCH(request);}static async handlePlay(request) {BxEventBus.Stream.emit("state.loading", {});let body = await request.clone().json(), newRequest = new Request(request, {body: JSON.stringify(body)});return NATIVE_FETCH(newRequest);}static async handle(request) {TouchController.disable();let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));let opts = {method: clone.method,headers};if (clone.method === "POST") opts.body = await clone.text();let url = request.url;if (!url.includes("/servers/home")) {let parsed = new URL(url);url = STATES.remotePlay.server + parsed.pathname;}if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request);else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request);else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts);else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request);else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);return await NATIVE_FETCH(request);}} +class LoadingScreen {static $bgStyle;static $waitTimeBox;static waitTimeInterval = null;static orgWebTitle;static secondsToString(seconds) {let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");return mDisplay + sDisplay;}static setup() {let titleInfo = STATES.currentStream.titleInfo;if (!titleInfo) return;if (!LoadingScreen.$bgStyle) {let $bgStyle = CE("style");document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;}if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();}static hideRocket() {let $bgStyle = LoadingScreen.$bgStyle;$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";}static setBackground(imageUrl) {let $bgStyle = LoadingScreen.$bgStyle;imageUrl = imageUrl + "?w=1920";let imageQuality = getGlobalPref("ui.imageQuality");if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;$bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;let bg = new Image;bg.onload = (e) => {$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';}, bg.src = imageUrl;}static setupWaitTime(waitTime) {if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();let secondsLeft = waitTime, $countDown, $estimated;LoadingScreen.orgWebTitle = document.title;let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);let endDateStr = endDate.toISOString().slice(0, 19);endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;let $waitTimeBox = LoadingScreen.$waitTimeBox;if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}, 1000);}static hide() {if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {let $rocketBg = document.querySelector('#game-stream rect[width="800"]');$rocketBg && $rocketBg.addEventListener("transitionend", (e) => {LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";}), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';}setTimeout(LoadingScreen.reset, 2000);}static reset() {LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}} +class GuideMenu {static instance;static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);$renderedButtons;closeGuideMenu() {if (window.BX_EXPOSED.dialogRoutes) {window.BX_EXPOSED.dialogRoutes.closeAll();return;}let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");$btnClose && $btnClose.click();}renderButtons() {if (this.$renderedButtons) return this.$renderedButtons;let buttons = {scriptSettings: createButton({label: t("better-xcloud"),icon: BxIcon.BETTER_XCLOUD,style: 128 | 64 | 1,onClick: () => {BxEventBus.Script.once("dialog.dismissed", () => {setTimeout(() => SettingsDialog.getInstance().show(), 50);}), this.closeGuideMenu();}}),closeApp: AppInterface && createButton({icon: BxIcon.POWER,label: t("close-app"),title: t("close-app"),style: 128 | 64 | 4,onClick: (e) => {AppInterface.closeApp();},attributes: {"data-state": "normal"}}),reloadPage: createButton({icon: BxIcon.REFRESH,label: t("reload-page"),title: t("reload-page"),style: 128 | 64,onClick: () => {if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();else window.location.reload();}}),backToHome: createButton({icon: BxIcon.HOME,label: t("back-to-home"),title: t("back-to-home"),style: 128 | 64,onClick: () => {this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));},attributes: {"data-state": "playing"}})}, buttonsLayout = [buttons.scriptSettings,[buttons.backToHome,buttons.reloadPage,buttons.closeApp]], $div = CE("div", {class: "bx-guide-home-buttons"});if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";for (let $button of buttonsLayout) {if (!$button) continue;if ($button instanceof HTMLElement) $div.appendChild($button);else if (Array.isArray($button)) {let $wrapper = CE("div", {});for (let $child of $button)$child && $wrapper.appendChild($child);$div.appendChild($wrapper);}}return this.$renderedButtons = $div, $div;}injectHome($root, isPlaying = !1) {{let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]");if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress);}let $target = null;if (isPlaying) {$target = $root.querySelector("a[class*=QuitGameButton]");let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");$btnXcloudHome && ($btnXcloudHome.style.display = "none");} else {let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");if ($dividers) $target = $dividers[$dividers.length - 1];}if (!$target) return !1;let $buttons = this.renderButtons();$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);}onShown = async (e) => {if (e.where === "home") {let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");$root && this.injectHome($root, STATES.isPlaying);}};addEventListeners() {window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);}observe($addedElm) {let className = $addedElm.className;if (!className) className = $addedElm.firstElementChild?.className ?? "";if (!className || className.startsWith("bx-")) return;if (className.includes("AchievementsButton-module__progressBarContainer")) {TrueAchievements.getInstance().injectAchievementsProgress($addedElm);return;}if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;{let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]");if ($achievDetailPage) {TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage);return;}}let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");if ($selectedTab) {let $elm = $selectedTab, index;for (index = 0;$elm = $elm?.previousElementSibling; index++);if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });}}} +class StreamBadges {static instance;static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);LOG_TAG = "StreamBadges";serverInfo = {};badges = {playtime: {name: t("playtime"),icon: BxIcon.PLAYTIME,color: "#ff004d"},battery: {name: t("battery"),icon: BxIcon.BATTERY,color: "#00b543"},download: {name: t("download"),icon: BxIcon.DOWNLOAD,color: "#29adff"},upload: {name: t("upload"),icon: BxIcon.UPLOAD,color: "#ff77a8"},server: {name: t("server"),icon: BxIcon.SERVER,color: "#ff6c24"},video: {name: t("video"),icon: BxIcon.DISPLAY,color: "#742f29"},audio: {name: t("audio"),icon: BxIcon.AUDIO,color: "#5f574f"}};$container;intervalId;REFRESH_INTERVAL = 3000;constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}setRegion(region) {this.serverInfo.server = {region};}renderBadge(name, value) {let badgeInfo = this.badges[name], $badge;if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");return this.badges[name].$element = $badge, $badge;}updateBadges = async (forceUpdate = !1) => {if (!this.$container || !forceUpdate && !this.$container.isConnected) {this.stop();return;}let statsCollector = StreamStatsCollector.getInstance();await statsCollector.collect();let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {download: dl.toString(),upload: ul.toString(),playtime: play.toString(),battery: batt.toString()}, name;for (name in badges) {let value = badges[name];if (value === null) continue;let $elm = this.badges[name].$element;if (!$elm) continue;if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");}};async start() {await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);}stop() {this.intervalId && clearInterval(this.intervalId), this.intervalId = null;}destroy() {this.serverInfo = {}, delete this.$container;}async render() {if (this.$container) return this.start(), this.$container;await this.getServerStats();let batteryLevel = "";if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";let BADGES = [["playtime", "1m"],["battery", batteryLevel],["download", humanFileSize(0)],["upload", humanFileSize(0)],this.badges.server.$element ?? ["server", "?"],this.serverInfo.video ? this.badges.video.$element : ["video", "?"],this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]], $container = CE("div", { class: "bx-badges" });for (let item2 of BADGES) {if (!item2) continue;let $badge;if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2);else $badge = item2;$container.appendChild($badge);}return this.$container = $container, await this.start(), $container;}async getServerStats() {let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;if (stats.forEach((stat) => {if (stat.type === "codec") {let mimeType = stat.mimeType.split("/")[0];if (mimeType === "video") allVideoCodecs[stat.id] = stat;else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;else if (stat.kind === "audio") audioCodecId = stat.codecId;} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;}), videoCodecId) {let videoStat = allVideoCodecs[videoCodecId], video = {width: videoWidth,height: videoHeight,codec: videoStat.mimeType.substring(6)};if (video.codec === "H264") {let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);match && (video.profile = match[1]);}let text = videoHeight + "p";if (text && (text += "/"), text += video.codec, video.profile) {let profile = video.profile, quality = profile;if (profile.startsWith("4d")) quality = t("visual-quality-high");else if (profile.startsWith("42e")) quality = t("visual-quality-normal");else if (profile.startsWith("420")) quality = t("visual-quality-low");text += ` (${quality})`;}this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;}if (audioCodecId) {let audioStat = allAudioCodecs[audioCodecId], audio = {codec: audioStat.mimeType.substring(6),bitrate: audioStat.clockRate}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;}if (candidatePairId) {BxLogger.info("candidate", candidatePairId, allCandidatePairs);let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;if (server && server.region) text += server.region;text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);}}static setupEvents() {}} +class XcloudInterceptor {static SERVER_EXTRA_INFO = {EastUS: ["🇺🇸", "america-north"],EastUS2: ["🇺🇸", "america-north"],NorthCentralUs: ["🇺🇸", "america-north"],SouthCentralUS: ["🇺🇸", "america-north"],WestUS: ["🇺🇸", "america-north"],WestUS2: ["🇺🇸", "america-north"],MexicoCentral: ["🇲🇽", "america-north"],BrazilSouth: ["🇧🇷", "america-south"],JapanEast: ["🇯🇵", "asia"],KoreaCentral: ["🇰🇷", "asia"],AustraliaEast: ["🇦🇺", "australia"],AustraliaSouthEast: ["🇦🇺", "australia"],SwedenCentral: ["🇸🇪", "europe"],UKSouth: ["🇬🇧", "europe"],WestEurope: ["🇪🇺", "europe"]};static async handleLogin(request, init) {let bypassServer = getGlobalPref("server.bypassRestriction");if (bypassServer !== "off") {let ip = BypassServerIps[bypassServer];ip && request.headers.set("X-Forwarded-For", ip);}let response = await NATIVE_FETCH(request, init);if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;let obj = await response.clone().json();RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;for (region of obj.offeringSettings.regions) {let { name: regionName, name: shortName } = region;if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);let match = serverRegex.exec(region.baseUri);if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);}BxEventBus.Script.emit("xcloud.server.ready", {});let preferredRegion = getPreferredServerRegion();if (preferredRegion && preferredRegion in STATES.serverRegions) {let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;}return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;}static async handlePlay(request, init) {BxEventBus.Stream.emit("state.loading", {});let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName];if (region && parsedUrl.origin === region.baseUri) {badgeRegion = regionName;break;}}StreamBadges.getInstance().setRegion(badgeRegion);let clone = request.clone(), body = await clone.json(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;}if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;let newRequest = new Request(request, {body: JSON.stringify(body),headers});return NATIVE_FETCH(newRequest);}static async handleWaitTime(request, init) {let response = await NATIVE_FETCH(request, init);if (getGlobalPref("loadingScreen.waitTime.show")) {let json = await response.clone().json();if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);}return response;}static async handleConfiguration(request, init) {if (request.method !== "GET") return NATIVE_FETCH(request, init);if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();else TouchController.enable();let response = await NATIVE_FETCH(request, init), text = await response.clone().text();if (!text.length) return response;BxEventBus.Stream.emit("state.starting", {});let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;let overrideMkb = null;if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {enableMouseInput: overrideMkb,enableKeyboardInput: overrideMkb});if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handle(request, init) {let url = typeof request === "string" ? request : request.url;if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);return NATIVE_FETCH(request, init);}} +function clearApplicationInsightsBuffers() {window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");} +function clearDbLogs(dbName, table) {let request = window.indexedDB.open(dbName);request.onsuccess = (e) => {let db = e.target.result;try {let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);} catch (ex) {}};} +function clearAllLogs() {clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");} +function updateIceCandidates(candidates, options) {let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = [];for (let item2 of candidates) {if (item2.candidate == "a=end-of-candidates") continue;let groups = pattern.exec(item2.candidate).groups;lst.push(groups);}if (options.preferIpv6Server) lst.sort((a, b) => {let firstIp = a.ip, secondIp = b.ip;return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;});let newCandidates = [], foundation = 1, newCandidate = (candidate) => {return {candidate,messageType: "iceCandidate",sdpMLineIndex: "0",sdpMid: "0"};};if (lst.forEach((item2) => {item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation;}), options.consoleAddrs)for (let ip in options.consoleAddrs)for (let port of options.consoleAddrs[ip])newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates;} +async function patchIceCandidates(request, consoleAddrs) {let response = await NATIVE_FETCH(request), text = await response.clone().text();if (!text.length) return response;let options = {preferIpv6Server: getGlobalPref("server.ipv6.prefer"),consoleAddrs}, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse);return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;} +function interceptHttpRequests() {let BLOCKED_URLS = [];if (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net");let blockFeatures2 = getGlobalPref("block.features");if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox");if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations");if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/");let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;xhrPrototype.open = function(method, url) {return this._url = url, nativeXhrOpen.apply(this, arguments);}, xhrPrototype.send = function(...arg) {for (let url of BLOCKED_URLS)if (this._url.startsWith(url)) {if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000);return BxLogger.warning("Blocked URL", url), !1;}return nativeXhrSend.apply(this, arguments);};let gamepassAllGames = [], IGNORED_DOMAINS = ["accounts.xboxlive.com","chat.xboxlive.com","notificationinbox.xboxlive.com","peoplehub.xboxlive.com","peoplehub-public.xboxlive.com","rta.xboxlive.com","userpresence.xboxlive.com","xblmessaging.xboxlive.com","consent.config.office.com","arc.msn.com","browser.events.data.microsoft.com","dc.services.visualstudio.com","2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"];window.BX_FETCH = window.fetch = async (request, init) => {let url = typeof request === "string" ? request : request.url;for (let blocked of BLOCKED_URLS)if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', {status: 200,statusText: "200 OK"});try {let domain = new URL(url).hostname;if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);} catch (e) {return NATIVE_FETCH(request, init);}if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {let response = await NATIVE_FETCH(request, init), json = await response.json();if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)json.exp.treatments[key] = FeatureGates[key];return response.json = () => Promise.resolve(json), response;} catch (e) {return console.log(e), NATIVE_FETCH(request, init);}if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++)gamepassAllGames.push(obj[i].id);else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try {let customList = TouchController.getCustomList();customList = customList.filter((id) => gamepassAllGames.includes(id));let newCustomList = customList.map((item2) => ({ id: item2 }));obj.push(...newCustomList);} catch (e) {console.log(e);}return response.json = () => Promise.resolve(obj), response;}if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) {let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();try {let newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 }));obj.push(...newCustomList);} catch (e) {console.log(e);}return response.json = () => Promise.resolve(obj), response;}let requestType;if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome";else requestType = "xcloud";if (requestType === "xhome") return XhomeInterceptor.handle(request);return XcloudInterceptor.handle(request, init);};} +function generateMsDeviceInfo(osName) {return {appInfo: {env: {clientAppId: window.location.host,clientAppType: "browser",clientAppVersion: "26.1.97",clientSdkVersion: "10.3.7",httpEnvironment: "prod",sdkInstallId: ""}},dev: {os: { name: osName, ver: "22631.2715", platform: "desktop" },hw: { make: "Microsoft", model: "unknown", sdktype: "web" },browser: { browserName: "chrome", browserVersion: "130.0" },displayInfo: {dimensions: { widthInPixels: 1920, heightInPixels: 1080 },pixelDensity: { dpiX: 1, dpiY: 1 }}}};} +function getOsNameFromResolution(resolution) {let osName;switch (resolution) {case "1080p-hq":osName = "tizen";break;case "1080p":osName = "windows";break;default:osName = "android";break;}return osName;} +function addCss() {let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = [];if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]");if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]");if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]");if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]");if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }";if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}";if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}";if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}";else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}";if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}";let $style = CE("style", !1, css);document.documentElement.appendChild($style);} +function preloadFonts() {let $link = CE("link", {rel: "preload",href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",as: "font",type: "font/otf",crossorigin: ""});document.querySelector("head")?.appendChild($link);} +class MouseCursorHider {static instance;static getInstance() {if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider;else MouseCursorHider.instance = null;return MouseCursorHider.instance;}timeoutId;isCursorVisible = !0;show() {document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0;}hide() {document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1;}onMouseMove = (e) => {!this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000);};start() {this.show(), document.addEventListener("mousemove", this.onMouseMove);}stop() {this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show();}} +function patchHistoryMethod(type) {let orig = window.history[type];return function(...args) {return BxEvent.dispatch(window, BxEvent.POPSTATE, {arguments: args}), orig.apply(this, arguments);};} +function onHistoryChanged(e) {if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});} +function setCodecPreferences(sdp, preferredCodec) {let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];for (let match of matches) {let id = match[1];if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);}if (!preferredCodecIds.length) return sdp;let lines = sdp.split(`\r +`);for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {let line = lines[lineIndex];if (!line.startsWith("m=video")) continue;let tmp = line.trim().split(" "), ids = tmp.slice(3);ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");break;}return lines.join(`\r +`);} +function patchSdpBitrate(sdp, video, audio) {let lines = sdp.split(`\r +`), mediaSet = new Set;!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");let bitrate = {video,audio};for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {let media = "", line = lines[lineNumber];if (!line.startsWith("m=")) continue;for (let m of mediaSet)if (line.startsWith(`m=${m}`)) {media = m, mediaSet.delete(media);break;}if (!media) continue;let bLine = `b=AS:${bitrate[media]}`;while (lineNumber++, lineNumber < lines.length) {if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue;if (line.startsWith("b=AS:")) {lines[lineNumber] = bLine;break;}if (line.startsWith("m=")) {lines.splice(lineNumber, 0, bLine);break;}}}return lines.join(`\r +`);} var clarity_boost_default = `#version 300 es in vec4 position; void main() { -gl_Position = position; -}`; +gl_Position = position;}`; var clarity_boost_default2 = `#version 300 es precision mediump float; uniform sampler2D data; @@ -9135,8 +283,7 @@ vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb; if (filterId == FILTER_UNSHARP_MASKING) { vec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0; gaussianBlur /= 16.0; -return e + (e - gaussianBlur) * sharpenFactor / 3.0; -} +return e + (e - gaussianBlur) * sharpenFactor / 3.0;} vec3 minRgb = min(min(min(d, e), min(f, b)), h); minRgb += min(min(a, c), min(g, i)); vec3 maxRgb = max(max(max(d, e), max(f, b)), h); @@ -9148,8 +295,7 @@ vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK)); vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0); vec3 window = b + d + f + h; vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0); -return mix(e, outColor, sharpenFactor / 2.0); -} +return mix(e, outColor, sharpenFactor / 2.0);} void main() { vec2 uv = gl_FragCoord.xy / iResolution.xy; vec3 color = texture(data, uv).rgb; @@ -9157,1103 +303,56 @@ color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color; color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color; color = contrast * (color - 0.5) + 0.5; color = brightness * color; -fragColor = vec4(color, 1.0); -}`; -class WebGL2Player { -LOG_TAG = "WebGL2Player"; -$video; -$canvas; -gl = null; -resources = []; -program = null; -stopped = !1; -options = { -filterId: 1, -sharpenFactor: 0, -brightness: 0, -contrast: 0, -saturation: 0 -}; -targetFps = 60; -frameInterval = 0; -lastFrameTime = 0; -animFrameId = null; -constructor($video) { -BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; -let $canvas = document.createElement("canvas"); -$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas); -} -setFilter(filterId, update = !0) { -this.options.filterId = filterId, update && this.updateCanvas(); -} -setSharpness(sharpness, update = !0) { -this.options.sharpenFactor = sharpness, update && this.updateCanvas(); -} -setBrightness(brightness, update = !0) { -this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas(); -} -setContrast(contrast, update = !0) { -this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas(); -} -setSaturation(saturation, update = !0) { -this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas(); -} -setTargetFps(target) { -this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0; -} -getCanvas() { -return this.$canvas; -} -updateCanvas() { -let gl = this.gl, program = this.program; -gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation); -} -forceDrawFrame() { -let gl = this.gl; -gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); -} -setupRendering() { -let frameCallback; -if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { -let $video = this.$video; -frameCallback = $video.requestVideoFrameCallback.bind($video); -} else frameCallback = requestAnimationFrame; -let animate = () => { -if (this.stopped) return; -this.animFrameId = frameCallback(animate); -let draw = !0; -if (this.targetFps === 0) draw = !1; -else if (this.targetFps < 60) { -let currentTime = performance.now(); -if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1; -else this.lastFrameTime = currentTime; -} -if (draw) { -let gl = this.gl; -gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6); -} -}; -this.animFrameId = frameCallback(animate); -} -setupShaders() { -BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference")); -let gl = this.$canvas.getContext("webgl2", { -isBx: !0, -antialias: !0, -alpha: !1, -powerPreference: getStreamPref("video.player.powerPreference") -}); -this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); -let vShader = gl.createShader(gl.VERTEX_SHADER); -gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); -let fShader = gl.createShader(gl.FRAGMENT_SHADER); -gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); -let program = gl.createProgram(); -if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`); -this.updateCanvas(); -let buffer = gl.createBuffer(); -this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0); -let texture = gl.createTexture(); -this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0); -} -resume() { -this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering(); -} -stop() { -if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) { -if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId); -else cancelAnimationFrame(this.animFrameId); -this.animFrameId = null; -} -} -destroy() { -BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); -let gl = this.gl; -if (gl) { -gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null); -for (let resource of this.resources) -if (resource instanceof WebGLProgram) gl.deleteProgram(resource); -else if (resource instanceof WebGLShader) gl.deleteShader(resource); -else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); -else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); -this.gl = null; -} -if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas); -this.$canvas.width = 1, this.$canvas.height = 1; -} -} -class StreamPlayer { -$video; -playerType = "default"; -options = {}; -webGL2Player = null; -$videoCss = null; -$usmMatrix = null; -constructor($video, type, options) { -this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type); -} -setupVideoElements() { -if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return; -let $fragment = document.createDocumentFragment(); -this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); -let $svg = CE("svg", { -id: "bx-video-filters", -xmlns: "http://www.w3.org/2000/svg", -class: "bx-gone" -}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", { -id: "bx-filter-usm", -xmlns: "http://www.w3.org/2000/svg" -}, this.$usmMatrix = CE("feConvolveMatrix", { -id: "bx-filter-usm-matrix", -order: "3", -xmlns: "http://www.w3.org/2000/svg" -})))); -$fragment.appendChild($svg), document.documentElement.appendChild($fragment); -} -getVideoPlayerFilterStyle() { -let filters = [], sharpness = this.options.sharpness || 0; -if (this.options.processing === "usm" && sharpness != 0) { -let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`; -this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)"); -} -let saturation = this.options.saturation || 100; -if (saturation != 100) filters.push(`saturate(${saturation}%)`); -let contrast = this.options.contrast || 100; -if (contrast != 100) filters.push(`contrast(${contrast}%)`); -let brightness = this.options.brightness || 100; -if (brightness != 100) filters.push(`brightness(${brightness}%)`); -return filters.join(" "); -} -resizePlayer() { -let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas; -if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); -let targetWidth, targetHeight, targetObjectFit; -if (PREF_RATIO.includes(":")) { -let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect(); -if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio; -else width = parentRect.width, height = width / videoRatio; -width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(); -let $parent = $video.parentElement, position = getStreamPref("video.position"); -if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") { -let padding = Math.floor((window.innerHeight - height) / 4); -if (padding > 0) { -if (position === "bottom-half") padding *= 3; -$parent.style.paddingTop = padding + "px"; -} -} -targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill"; -} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString(); -if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize")); -if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); -} -setPlayerType(type, refreshPlayer = !1) { -if (this.playerType !== type) { -let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone"; -if (type === "webgl2") { -if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); -else this.webGL2Player.resume(); -this.$videoCss.textContent = "", this.$video.classList.add(videoClass); -} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass); -} -this.playerType = type, refreshPlayer && this.refreshPlayer(); -} -setOptions(options, refreshPlayer = !1) { -this.options = options, refreshPlayer && this.refreshPlayer(); -} -updateOptions(options, refreshPlayer = !1) { -this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer(); -} -getPlayerElement(playerType) { -if (typeof playerType === "undefined") playerType = this.playerType; -if (playerType === "webgl2") return this.webGL2Player?.getCanvas(); -return this.$video; -} -getWebGL2Player() { -return this.webGL2Player; -} -refreshPlayer() { -if (this.playerType === "webgl2") { -let options = this.options, webGL2Player = this.webGL2Player; -if (options.processing === "usm") webGL2Player.setFilter(1); -else webGL2Player.setFilter(2); -ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100); -} else { -let filters = this.getVideoPlayerFilterStyle(), videoCss = ""; -if (filters) videoCss += `filter: ${filters} !important;`; -if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters); -let css = ""; -if (videoCss) css = `#game-stream video { ${videoCss} }`; -this.$videoCss.textContent = css; -} -this.resizePlayer(); -} -reloadPlayer() { -this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1); -} -cleanUpWebGL2Player() { -this.webGL2Player?.destroy(), this.webGL2Player = null; -} -destroy() { -this.cleanUpWebGL2Player(); -} -} -function patchVideoApi() { -let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() { -if (this.style.visibility = "visible", !this.videoWidth) return; -let playerOptions = { -processing: getStreamPref("video.processing"), -sharpness: getStreamPref("video.processing.sharpness"), -saturation: getStreamPref("video.saturation"), -contrast: getStreamPref("video.contrast"), -brightness: getStreamPref("video.brightness") -}; -STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", { -$video: this -}); -}, nativePlay = HTMLMediaElement.prototype.play; -HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() { -if (this.className && this.className.startsWith("XboxSplashVideo")) { -if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {}); -return nativePlay.apply(this); -} -let $parent = this.parentElement; -if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); -return nativePlay.apply(this); -}; -} -function patchRtcCodecs() { -if (getGlobalPref("stream.video.codecProfile") === "default") return; -if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; -} -function patchRtcPeerConnection() { -let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; -RTCPeerConnection.prototype.createDataChannel = function() { -let dataChannel = nativeCreateDataChannel.apply(this, arguments); -return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel; -}; -let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile"); -if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) { -let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; -RTCPeerConnection.prototype.setLocalDescription = function(description) { -if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); -try { -if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); -} catch (e) { -BxLogger.error("setLocalDescription", e); -} -return nativeSetLocalDescription.apply(this, arguments); -}; -} -let OrgRTCPeerConnection = window.RTCPeerConnection; -window.RTCPeerConnection = function() { -let conn = new OrgRTCPeerConnection; -return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { -BxLogger.info("connectionstatechange", conn.connectionState); -}), conn; -}; -} -function patchAudioContext() { -let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; -window.AudioContext = function(options) { -if (options && options.latencyHint) options.latencyHint = 0; -let ctx = new OrgAudioContext(options); -return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { -let gainNode = nativeCreateGain.apply(this); -return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; -}, STATES.currentStream.audioContext = ctx, ctx; -}; -} -function patchMeControl() { -let overrideConfigs = { -enableAADTelemetry: !1, -enableTelemetry: !1, -telEvs: "", -oneDSUrl: "" -}, MSA = { -MeControl: { -API: { -setDisplayMode: () => {}, -setMobileState: () => {}, -addEventListener: () => {}, -removeEventListener: () => {} -} -} -}, MeControl = {}, MsaHandler = { -get(target, prop, receiver) { -return target[prop]; -}, -set(obj, prop, value) { -if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs); -return obj[prop] = value, !0; -} -}, MeControlHandler = { -get(target, prop, receiver) { -return target[prop]; -}, -set(obj, prop, value) { -if (prop === "Config") value = Object.assign(value, overrideConfigs); -return obj[prop] = value, !0; -} -}; -window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler); -} -function disableAdobeAudienceManager() { -Object.defineProperty(window, "adobe", { -get() { -return Object.freeze({}); -} -}); -} -function patchCanvasContext() { -let nativeGetContext = HTMLCanvasElement.prototype.getContext; -HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) { -if (contextType.includes("webgl")) { -if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) { -if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power"; -} -} -return nativeGetContext.apply(this, [contextType, contextAttributes]); -}; -} -function patchPointerLockApi() { -Object.defineProperty(document, "fullscreenElement", { -configurable: !0, -get() { -return document.documentElement; -} -}), HTMLElement.prototype.requestFullscreen = function(options) { -return Promise.resolve(); -}; -let pointerLockElement = null; -Object.defineProperty(document, "pointerLockElement", { -configurable: !0, -get() { -return pointerLockElement; -} -}), HTMLElement.prototype.requestPointerLock = function() { -pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED)); -}, Document.prototype.exitPointerLock = function() { -pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED)); -}; -} -class BaseGameBarAction { -constructor() {} -reset() {} -onClick(e) { -BxEventBus.Stream.emit("gameBar.activated", {}); -} -render() { -return this.$content; -} -} -class ScreenshotAction extends BaseGameBarAction { -$content; -constructor() { -super(); -this.$content = createButton({ -style: 8, -icon: BxIcon.SCREENSHOT, -title: t("take-screenshot"), -onClick: this.onClick -}); -} -onClick = (e) => { -super.onClick(e), ScreenshotManager.getInstance().takeScreenshot(); -}; -} -class TouchControlAction extends BaseGameBarAction { -$content; -constructor() { -super(); -let $btnEnable = createButton({ -style: 8, -icon: BxIcon.TOUCH_CONTROL_ENABLE, -title: t("show-touch-controller"), -onClick: this.onClick -}), $btnDisable = createButton({ -style: 8, -icon: BxIcon.TOUCH_CONTROL_DISABLE, -title: t("hide-touch-controller"), -onClick: this.onClick, -classes: ["bx-activated"] -}); -this.$content = CE("div", !1, $btnEnable, $btnDisable); -} -onClick = (e) => { -super.onClick(e); -let isVisible = TouchController.toggleVisibility(); -this.$content.dataset.activated = (!isVisible).toString(); -}; -reset() { -this.$content.dataset.activated = "false"; -} -} -class MicrophoneAction extends BaseGameBarAction { -$content; -constructor() { -super(); -let $btnDefault = createButton({ -style: 8, -icon: BxIcon.MICROPHONE, -onClick: this.onClick, -classes: ["bx-activated"] -}), $btnMuted = createButton({ -style: 8, -icon: BxIcon.MICROPHONE_MUTED, -onClick: this.onClick -}); -this.$content = CE("div", !1, $btnMuted, $btnDefault), BxEventBus.Stream.on("microphone.state.changed", (payload) => { -let enabled = payload.state === "Enabled"; -this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); -}); -} -onClick = (e) => { -super.onClick(e); -let enabled = MicrophoneShortcut.toggle(!1); -this.$content.dataset.activated = enabled.toString(); -}; -reset() { -this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false"; -} -} -class TrueAchievementsAction extends BaseGameBarAction { -$content; -constructor() { -super(); -this.$content = createButton({ -style: 8, -icon: BxIcon.TRUE_ACHIEVEMENTS, -onClick: this.onClick -}); -} -onClick = (e) => { -super.onClick(e), TrueAchievements.getInstance().open(!1); -}; -} -class SpeakerAction extends BaseGameBarAction { -$content; -constructor() { -super(); -let $btnEnable = createButton({ -style: 8, -icon: BxIcon.AUDIO, -onClick: this.onClick -}), $btnMuted = createButton({ -style: 8, -icon: BxIcon.SPEAKER_MUTED, -onClick: this.onClick, -classes: ["bx-activated"] -}); -this.$content = CE("div", !1, $btnEnable, $btnMuted), BxEventBus.Stream.on("speaker.state.changed", (payload) => { -let enabled = payload.state === 0; -this.$content.dataset.activated = (!enabled).toString(); -}); -} -onClick = (e) => { -super.onClick(e), SoundShortcut.muteUnmute(); -}; -reset() { -this.$content.dataset.activated = "false"; -} -} -class RendererAction extends BaseGameBarAction { -$content; -constructor() { -super(); -let $btnDefault = createButton({ -style: 8, -icon: BxIcon.EYE, -onClick: this.onClick -}), $btnActivated = createButton({ -style: 8, -icon: BxIcon.EYE_SLASH, -onClick: this.onClick, -classes: ["bx-activated"] -}); -this.$content = CE("div", !1, $btnDefault, $btnActivated), BxEventBus.Stream.on("video.visibility.changed", (payload) => { -this.$content.dataset.activated = (!payload.isVisible).toString(); -}); -} -onClick = (e) => { -super.onClick(e), RendererShortcut.toggleVisibility(); -}; -reset() { -this.$content.dataset.activated = "false"; -} -} -class GameBar { -static instance; -static getInstance() { -if (typeof GameBar.instance === "undefined") if (getGlobalPref("gameBar.position") !== "off") GameBar.instance = new GameBar; -else GameBar.instance = null; -return GameBar.instance; -} -LOG_TAG = "GameBar"; -static VISIBLE_DURATION = 2000; -$gameBar; -$container; -timeoutId = null; -actions = []; -constructor() { -BxLogger.info(this.LOG_TAG, "constructor()"); -let $container, position = getGlobalPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT)); -if (this.actions = [ -new ScreenshotAction, -...STATES.userAgent.capabilities.touch && getGlobalPref("touchController.mode") !== "off" ? [new TouchControlAction] : [], -new SpeakerAction, -new RendererAction, -new MicrophoneAction, -new TrueAchievementsAction -], position === "bottom-right") -this.actions.reverse(); -for (let action of this.actions) -$container.appendChild(action.render()); -$gameBar.addEventListener("click", (e) => { -if (e.target !== $gameBar) return; -$container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); -}), BxEventBus.Stream.on("gameBar.activated", this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => { -$container.classList.replace("bx-hide", "bx-offscreen"); -}), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { -if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable(); -}).bind(this)); -} -beginHideTimeout = () => { -this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => { -this.timeoutId = null, this.hideBar(); -}, GameBar.VISIBLE_DURATION); -}; -clearHideTimeout = () => { -this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null; -}; -enable() { -this.$gameBar.classList.remove("bx-gone"); -} -disable() { -this.hideBar(), this.$gameBar.classList.add("bx-gone"); -} -showBar() { -this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout(); -} -hideBar = () => { -this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide"); -}; -reset() { -for (let action of this.actions) -action.reset(); -} -} -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 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; -} -} +fragColor = vec4(color, 1.0);}`; +class WebGL2Player {LOG_TAG = "WebGL2Player";$video;$canvas;gl = null;resources = [];program = null;stopped = !1;options = {filterId: 1,sharpenFactor: 0,brightness: 0,contrast: 0,saturation: 0};targetFps = 60;frameInterval = 0;lastFrameTime = 0;animFrameId = null;constructor($video) {BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;let $canvas = document.createElement("canvas");$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);}setFilter(filterId, update = !0) {this.options.filterId = filterId, update && this.updateCanvas();}setSharpness(sharpness, update = !0) {this.options.sharpenFactor = sharpness, update && this.updateCanvas();}setBrightness(brightness, update = !0) {this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();}setContrast(contrast, update = !0) {this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();}setSaturation(saturation, update = !0) {this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();}setTargetFps(target) {this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;}getCanvas() {return this.$canvas;}updateCanvas() {let gl = this.gl, program = this.program;gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);}forceDrawFrame() {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}setupRendering() {let frameCallback;if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {let $video = this.$video;frameCallback = $video.requestVideoFrameCallback.bind($video);} else frameCallback = requestAnimationFrame;let animate = () => {if (this.stopped) return;this.animFrameId = frameCallback(animate);let draw = !0;if (this.targetFps === 0) draw = !1;else if (this.targetFps < 60) {let currentTime = performance.now();if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;else this.lastFrameTime = currentTime;}if (draw) {let gl = this.gl;gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);}};this.animFrameId = frameCallback(animate);}setupShaders() {BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));let gl = this.$canvas.getContext("webgl2", {isBx: !0,antialias: !0,alpha: !1,powerPreference: getStreamPref("video.player.powerPreference")});this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);let vShader = gl.createShader(gl.VERTEX_SHADER);gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);let fShader = gl.createShader(gl.FRAGMENT_SHADER);gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);let program = gl.createProgram();if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);this.updateCanvas();let buffer = gl.createBuffer();this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);let texture = gl.createTexture();this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);}resume() {this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();}stop() {if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);else cancelAnimationFrame(this.animFrameId);this.animFrameId = null;}}destroy() {BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();let gl = this.gl;if (gl) {gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);for (let resource of this.resources)if (resource instanceof WebGLProgram) gl.deleteProgram(resource);else if (resource instanceof WebGLShader) gl.deleteShader(resource);else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);this.gl = null;}if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);this.$canvas.width = 1, this.$canvas.height = 1;}} +class StreamPlayer {$video;playerType = "default";options = {};webGL2Player = null;$videoCss = null;$usmMatrix = null;constructor($video, type, options) {this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);}setupVideoElements() {if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;let $fragment = document.createDocumentFragment();this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);let $svg = CE("svg", {id: "bx-video-filters",xmlns: "http://www.w3.org/2000/svg",class: "bx-gone"}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {id: "bx-filter-usm",xmlns: "http://www.w3.org/2000/svg"}, this.$usmMatrix = CE("feConvolveMatrix", {id: "bx-filter-usm-matrix",order: "3",xmlns: "http://www.w3.org/2000/svg"}))));$fragment.appendChild($svg), document.documentElement.appendChild($fragment);}getVideoPlayerFilterStyle() {let filters = [], sharpness = this.options.sharpness || 0;if (this.options.processing === "usm" && sharpness != 0) {let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");}let saturation = this.options.saturation || 100;if (saturation != 100) filters.push(`saturate(${saturation}%)`);let contrast = this.options.contrast || 100;if (contrast != 100) filters.push(`contrast(${contrast}%)`);let brightness = this.options.brightness || 100;if (brightness != 100) filters.push(`brightness(${brightness}%)`);return filters.join(" ");}resizePlayer() {let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();let targetWidth, targetHeight, targetObjectFit;if (PREF_RATIO.includes(":")) {let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;else width = parentRect.width, height = width / videoRatio;width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();let $parent = $video.parentElement, position = getStreamPref("video.position");if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {let padding = Math.floor((window.innerHeight - height) / 4);if (padding > 0) {if (position === "bottom-half") padding *= 3;$parent.style.paddingTop = padding + "px";}}targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();}setPlayerType(type, refreshPlayer = !1) {if (this.playerType !== type) {let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";if (type === "webgl2") {if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);else this.webGL2Player.resume();this.$videoCss.textContent = "", this.$video.classList.add(videoClass);} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);}this.playerType = type, refreshPlayer && this.refreshPlayer();}setOptions(options, refreshPlayer = !1) {this.options = options, refreshPlayer && this.refreshPlayer();}updateOptions(options, refreshPlayer = !1) {this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();}getPlayerElement(playerType) {if (typeof playerType === "undefined") playerType = this.playerType;if (playerType === "webgl2") return this.webGL2Player?.getCanvas();return this.$video;}getWebGL2Player() {return this.webGL2Player;}refreshPlayer() {if (this.playerType === "webgl2") {let options = this.options, webGL2Player = this.webGL2Player;if (options.processing === "usm") webGL2Player.setFilter(1);else webGL2Player.setFilter(2);ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);} else {let filters = this.getVideoPlayerFilterStyle(), videoCss = "";if (filters) videoCss += `filter: ${filters} !important;`;if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);let css = "";if (videoCss) css = `#game-stream video { ${videoCss} }`;this.$videoCss.textContent = css;}this.resizePlayer();}reloadPlayer() {this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);}cleanUpWebGL2Player() {this.webGL2Player?.destroy(), this.webGL2Player = null;}destroy() {this.cleanUpWebGL2Player();}} +function patchVideoApi() {let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {if (this.style.visibility = "visible", !this.videoWidth) return;let playerOptions = {processing: getStreamPref("video.processing"),sharpness: getStreamPref("video.processing.sharpness"),saturation: getStreamPref("video.saturation"),contrast: getStreamPref("video.contrast"),brightness: getStreamPref("video.brightness")};STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {$video: this});}, nativePlay = HTMLMediaElement.prototype.play;HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {if (this.className && this.className.startsWith("XboxSplashVideo")) {if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});return nativePlay.apply(this);}let $parent = this.parentElement;if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });return nativePlay.apply(this);};} +function patchRtcCodecs() {if (getGlobalPref("stream.video.codecProfile") === "default") return;if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;} +function patchRtcPeerConnection() {let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;RTCPeerConnection.prototype.createDataChannel = function() {let dataChannel = nativeCreateDataChannel.apply(this, arguments);return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;};let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile");if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;RTCPeerConnection.prototype.setLocalDescription = function(description) {if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);try {if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));} catch (e) {BxLogger.error("setLocalDescription", e);}return nativeSetLocalDescription.apply(this, arguments);};}let OrgRTCPeerConnection = window.RTCPeerConnection;window.RTCPeerConnection = function() {let conn = new OrgRTCPeerConnection;return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {BxLogger.info("connectionstatechange", conn.connectionState);}), conn;};} +function patchAudioContext() {let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;window.AudioContext = function(options) {if (options && options.latencyHint) options.latencyHint = 0;let ctx = new OrgAudioContext(options);return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {let gainNode = nativeCreateGain.apply(this);return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;}, STATES.currentStream.audioContext = ctx, ctx;};} +function patchMeControl() {let overrideConfigs = {enableAADTelemetry: !1,enableTelemetry: !1,telEvs: "",oneDSUrl: ""}, MSA = {MeControl: {API: {setDisplayMode: () => {},setMobileState: () => {},addEventListener: () => {},removeEventListener: () => {}}}}, MeControl = {}, MsaHandler = {get(target, prop, receiver) {return target[prop];},set(obj, prop, value) {if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs);return obj[prop] = value, !0;}}, MeControlHandler = {get(target, prop, receiver) {return target[prop];},set(obj, prop, value) {if (prop === "Config") value = Object.assign(value, overrideConfigs);return obj[prop] = value, !0;}};window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);} +function disableAdobeAudienceManager() {Object.defineProperty(window, "adobe", {get() {return Object.freeze({});}});} +function patchCanvasContext() {let nativeGetContext = HTMLCanvasElement.prototype.getContext;HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {if (contextType.includes("webgl")) {if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power";}}return nativeGetContext.apply(this, [contextType, contextAttributes]);};} +function patchPointerLockApi() {Object.defineProperty(document, "fullscreenElement", {configurable: !0,get() {return document.documentElement;}}), HTMLElement.prototype.requestFullscreen = function(options) {return Promise.resolve();};let pointerLockElement = null;Object.defineProperty(document, "pointerLockElement", {configurable: !0,get() {return pointerLockElement;}}), HTMLElement.prototype.requestPointerLock = function() {pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED));}, Document.prototype.exitPointerLock = function() {pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED));};} +class BaseGameBarAction {constructor() {}reset() {}onClick(e) {BxEventBus.Stream.emit("gameBar.activated", {});}render() {return this.$content;}} +class ScreenshotAction extends BaseGameBarAction {$content;constructor() {super();this.$content = createButton({style: 8,icon: BxIcon.SCREENSHOT,title: t("take-screenshot"),onClick: this.onClick});}onClick = (e) => {super.onClick(e), ScreenshotManager.getInstance().takeScreenshot();};} +class TouchControlAction extends BaseGameBarAction {$content;constructor() {super();let $btnEnable = createButton({style: 8,icon: BxIcon.TOUCH_CONTROL_ENABLE,title: t("show-touch-controller"),onClick: this.onClick}), $btnDisable = createButton({style: 8,icon: BxIcon.TOUCH_CONTROL_DISABLE,title: t("hide-touch-controller"),onClick: this.onClick,classes: ["bx-activated"]});this.$content = CE("div", !1, $btnEnable, $btnDisable);}onClick = (e) => {super.onClick(e);let isVisible = TouchController.toggleVisibility();this.$content.dataset.activated = (!isVisible).toString();};reset() {this.$content.dataset.activated = "false";}} +class MicrophoneAction extends BaseGameBarAction {$content;constructor() {super();let $btnDefault = createButton({style: 8,icon: BxIcon.MICROPHONE,onClick: this.onClick,classes: ["bx-activated"]}), $btnMuted = createButton({style: 8,icon: BxIcon.MICROPHONE_MUTED,onClick: this.onClick});this.$content = CE("div", !1, $btnMuted, $btnDefault), BxEventBus.Stream.on("microphone.state.changed", (payload) => {let enabled = payload.state === "Enabled";this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone");});}onClick = (e) => {super.onClick(e);let enabled = MicrophoneShortcut.toggle(!1);this.$content.dataset.activated = enabled.toString();};reset() {this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false";}} +class TrueAchievementsAction extends BaseGameBarAction {$content;constructor() {super();this.$content = createButton({style: 8,icon: BxIcon.TRUE_ACHIEVEMENTS,onClick: this.onClick});}onClick = (e) => {super.onClick(e), TrueAchievements.getInstance().open(!1);};} +class SpeakerAction extends BaseGameBarAction {$content;constructor() {super();let $btnEnable = createButton({style: 8,icon: BxIcon.AUDIO,onClick: this.onClick}), $btnMuted = createButton({style: 8,icon: BxIcon.SPEAKER_MUTED,onClick: this.onClick,classes: ["bx-activated"]});this.$content = CE("div", !1, $btnEnable, $btnMuted), BxEventBus.Stream.on("speaker.state.changed", (payload) => {let enabled = payload.state === 0;this.$content.dataset.activated = (!enabled).toString();});}onClick = (e) => {super.onClick(e), SoundShortcut.muteUnmute();};reset() {this.$content.dataset.activated = "false";}} +class RendererAction extends BaseGameBarAction {$content;constructor() {super();let $btnDefault = createButton({style: 8,icon: BxIcon.EYE,onClick: this.onClick}), $btnActivated = createButton({style: 8,icon: BxIcon.EYE_SLASH,onClick: this.onClick,classes: ["bx-activated"]});this.$content = CE("div", !1, $btnDefault, $btnActivated), BxEventBus.Stream.on("video.visibility.changed", (payload) => {this.$content.dataset.activated = (!payload.isVisible).toString();});}onClick = (e) => {super.onClick(e), RendererShortcut.toggleVisibility();};reset() {this.$content.dataset.activated = "false";}} +class GameBar {static instance;static getInstance() {if (typeof GameBar.instance === "undefined") if (getGlobalPref("gameBar.position") !== "off") GameBar.instance = new GameBar;else GameBar.instance = null;return GameBar.instance;}LOG_TAG = "GameBar";static VISIBLE_DURATION = 2000;$gameBar;$container;timeoutId = null;actions = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()");let $container, position = getGlobalPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT));if (this.actions = [new ScreenshotAction,...STATES.userAgent.capabilities.touch && getGlobalPref("touchController.mode") !== "off" ? [new TouchControlAction] : [],new SpeakerAction,new RendererAction,new MicrophoneAction,new TrueAchievementsAction], position === "bottom-right")this.actions.reverse();for (let action of this.actions)$container.appendChild(action.render());$gameBar.addEventListener("click", (e) => {if (e.target !== $gameBar) return;$container.classList.contains("bx-show") ? this.hideBar() : this.showBar();}), BxEventBus.Stream.on("gameBar.activated", this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => {$container.classList.replace("bx-hide", "bx-offscreen");}), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => {if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable();}).bind(this));}beginHideTimeout = () => {this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => {this.timeoutId = null, this.hideBar();}, GameBar.VISIBLE_DURATION);};clearHideTimeout = () => {this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null;};enable() {this.$gameBar.classList.remove("bx-gone");}disable() {this.hideBar(), this.$gameBar.classList.add("bx-gone");}showBar() {this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout();}hideBar = () => {this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide");};reset() {for (let action of this.actions)action.reset();}} +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 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;}} 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"); -} +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); -if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") { -window.stop(); -let css = ""; -css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}'; -let isSafari = UserAgent.isSafari(), $secondaryAction; -if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading")); -else $secondaryAction = CE("a", { -href: "https://better-xcloud.github.io/troubleshooting", -target: "_blank" -}, "🤓 " + t("how-to-fix")); -let $fragment = document.createDocumentFragment(); -throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", { -class: "bx-reload-overlay" -}, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari"); -} -window.addEventListener("load", (e) => { -window.setTimeout(() => { -if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0); -}, 3000); -}); -document.addEventListener("readystatechange", (e) => { -if (document.readyState !== "interactive") return; -if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize(); -else window.setTimeout(HeaderSection.watchHeader, 2000); -if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) { -let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); -$parent && ($parent.style.display = "none"); -} -preloadFonts(); -}); +if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") {window.stop();let css = "";css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}';let isSafari = UserAgent.isSafari(), $secondaryAction;if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading"));else $secondaryAction = CE("a", {href: "https://better-xcloud.github.io/troubleshooting",target: "_blank"}, "🤓 " + t("how-to-fix"));let $fragment = document.createDocumentFragment();throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", {class: "bx-reload-overlay"}, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari");} +window.addEventListener("load", (e) => {window.setTimeout(() => {if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0);}, 3000);}); +document.addEventListener("readystatechange", (e) => {if (document.readyState !== "interactive") return;if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize();else window.setTimeout(HeaderSection.watchHeader, 2000);if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");$parent && ($parent.style.display = "none");}preloadFonts();}); window.BX_EXPOSED = BxExposed; window.addEventListener(BxEvent.POPSTATE, onHistoryChanged); window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); -BxEventBus.Script.once("xcloud.server.unavailable", () => { -if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); -}); -BxEventBus.Script.on("xcloud.server.ready", () => { -STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); -}); -BxEventBus.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"; -}); +BxEventBus.Script.once("xcloud.server.unavailable", () => {if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();}); +BxEventBus.Script.on("xcloud.server.ready", () => {STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);}); +BxEventBus.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.error", () => { -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.streamPlayer?.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.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", {});}); +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.streamPlayer?.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(); -}); -function main() { -if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") { -let customList = getGlobalPref("nativeMkb.forcedGames"); -BX_FLAGS.ForceNativeMkbTitles.push(...customList); -} -if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager(); -if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect(); -if (getGlobalPref("touchController.mode") === "all") TouchController.setup(); -if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); -if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); -} +window.addEventListener("pagehide", (e) => {BxEventBus.Stream.emit("state.stopped", {});}); +window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {ScreenshotManager.getInstance().takeScreenshot();}); +function main() {if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {let customList = getGlobalPref("nativeMkb.forcedGames");BX_FLAGS.ForceNativeMkbTitles.push(...customList);}if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();if (getGlobalPref("touchController.mode") === "all") TouchController.setup();if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));} main();