From 82ee00b4aea07e57a9407ed3f0a50ad2ea12257e Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Mon, 14 Oct 2024 17:17:32 +0700 Subject: [PATCH] Update dist --- dist/better-xcloud.lite.user.js | 10584 +++++++++++---------- dist/better-xcloud.user.js | 14856 +++++++++++++++--------------- 2 files changed, 12678 insertions(+), 12762 deletions(-) diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js index a54a7f2..fe71c76 100644 --- a/dist/better-xcloud.lite.user.js +++ b/dist/better-xcloud.lite.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud (Lite) // @namespace https://github.com/redphx -// @version 5.8.4 +// @version 5.8.5-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -12,5622 +12,5594 @@ // ==/UserScript== "use strict"; class BxLogger { - static info = (tag, ...args) => BxLogger.log("#008746", tag, ...args); - static warning = (tag, ...args) => 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); - } + static info = (tag, ...args) => BxLogger.log("#008746", tag, ...args); + static warning = (tag, ...args) => 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" - } + 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; + 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 SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "123.0.0.0"; if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { - const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); - if (match) CHROMIUM_VERSION = match[1]; + const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); + if (match) CHROMIUM_VERSION = match[1]; } class UserAgent { - static STORAGE_KEY = "better_xcloud_user_agent"; - 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} SmartTV`, - "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) { - const 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) { - const 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; - const userAgent = UserAgent.getDefault().toLowerCase(); - let result = userAgent.includes("safari") && !userAgent.includes("chrom"); - return this.#isSafari = result, result; - } - static isSafariMobile() { - if (this.#isSafariMobile !== null) return this.#isSafariMobile; - const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); - return this.#isSafariMobile = result, result; - } - static isMobile() { - if (this.#isMobile !== null) return this.#isMobile; - const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); - return this.#isMobile = result, result; - } - static spoof() { - const 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 - }); + static STORAGE_KEY = "better_xcloud_user_agent"; + 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} SmartTV`, + "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) { + const 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) { + const 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; + const userAgent = UserAgent.getDefault().toLowerCase(); + let result = userAgent.includes("safari") && !userAgent.includes("chrom"); + return this.#isSafari = result, result; + } + static isSafariMobile() { + if (this.#isSafariMobile !== null) return this.#isSafariMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); + return this.#isSafariMobile = result, result; + } + static isMobile() { + if (this.#isMobile !== null) return this.#isMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); + return this.#isMobile = result, result; + } + static spoof() { + const 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 + }); + } } function deepClone(obj) { - if ("structuredClone" in window) return structuredClone(obj); - if (!obj) return {}; - return JSON.parse(JSON.stringify(obj)); + if ("structuredClone" in window) return structuredClone(obj); + if (!obj) return {}; + return JSON.parse(JSON.stringify(obj)); } -var SCRIPT_VERSION = "5.8.4", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "5.8.5-beta", SCRIPT_VARIANT = "lite", 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, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { - supportedRegion: !0, - serverRegions: {}, - selectedRegion: {}, - gsToken: "", - isSignedIn: !1, - isPlaying: !1, - appContext: {}, - browser: { - capabilities: { - touch: browserHasTouchSupport, - batteryApi: "getBattery" in window.navigator - } - }, - userAgent: { - isTv, - capabilities: { - touch: userAgentHasTouchSupport, - mkb: supportMkb - } - }, - currentStream: {}, - remotePlay: {}, - pointerServerPort: 9269 + supportedRegion: !0, + serverRegions: {}, + selectedRegion: {}, + gsToken: "", + isSignedIn: !1, + isPlaying: !1, + appContext: {}, + browser: { + capabilities: { + touch: browserHasTouchSupport, + batteryApi: "getBattery" in window.navigator + } + }, + userAgent: { + isTv, + capabilities: { + touch: userAgentHasTouchSupport, + mkb: supportMkb + } + }, + currentStream: {}, + remotePlay: {}, + pointerServerPort: 9269 }, STORAGE = {}; var BxEvent; ((BxEvent) => { - BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", 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.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", 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_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", 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; - } - const 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", eventName, data); + BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", 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.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", 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_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", 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; } - BxEvent.dispatch = dispatch; + const 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", eventName, data); + } + BxEvent.dispatch = dispatch; })(BxEvent ||= {}); window.BxEvent = BxEvent; class NavigationUtils { - static setNearby($elm, nearby) { - $elm.nearby = $elm.nearby || {}; - let key; - for (key in nearby) - $elm.nearby[key] = nearby[key]; - } + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } } var setNearby = NavigationUtils.setNearby; function createElement(elmName, props = {}, ..._) { - let $elm; - const hasNs = "xmlns" in props; - if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; - else $elm = document.createElement(elmName); - if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; - for (let key in props) { - if ($elm.hasOwnProperty(key)) continue; - if (hasNs) $elm.setAttributeNS(null, key, props[key]); - else if (key === "on") for (let eventName in props[key]) - $elm.addEventListener(eventName, props[key][eventName]); - else $elm.setAttribute(key, props[key]); - } - for (let i = 2, size = arguments.length;i < size; i++) { - const arg = arguments[i]; - if (arg instanceof Node) $elm.appendChild(arg); - else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); - } - return $elm; + let $elm; + const hasNs = "xmlns" in props; + if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; + else $elm = document.createElement(elmName); + if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + if (hasNs) $elm.setAttributeNS(null, key, props[key]); + else if (key === "on") for (let eventName in props[key]) + $elm.addEventListener(eventName, props[key][eventName]); + else $elm.setAttribute(key, props[key]); + } + for (let i = 2, size = arguments.length;i < size; i++) { + const arg = arguments[i]; + if (arg instanceof Node) $elm.appendChild(arg); + else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); + } + return $elm; } function isElementVisible($elm) { - const rect = $elm.getBoundingClientRect(); - return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; + const rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; } function removeChildElements($parent) { - while ($parent.firstElementChild) - $parent.firstElementChild.remove(); + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); } function humanFileSize(size) { - const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; + const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + 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; - const output = []; - return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + const output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); } var ButtonStyleClass = { - 1: "bx-primary", - 2: "bx-danger", - 4: "bx-ghost", - 8: "bx-frosted", - 16: "bx-drop-shadow", - 32: "bx-focusable", - 64: "bx-full-width", - 128: "bx-full-height", - 256: "bx-tall", - 512: "bx-circular", - 1024: "bx-normal-case", - 2048: "bx-normal-link" + 1: "bx-primary", + 2: "bx-danger", + 4: "bx-ghost", + 8: "bx-frosted", + 16: "bx-drop-shadow", + 32: "bx-focusable", + 64: "bx-full-width", + 128: "bx-full-height", + 256: "bx-tall", + 512: "bx-circular", + 1024: "bx-normal-case", + 2048: "bx-normal-link" }, CE = createElement, svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement, createSvgIcon = (icon) => { - return svgParser(icon.toString()); + return svgParser(icon.toString()); }, ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)), createButton = (options) => { - let $btn; - if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; - else $btn = CE("button", { class: "bx-button", type: "button" }); - const style = options.style || 0; - style && ButtonStyleIndices.forEach((index) => { - style & index && $btn.classList.add(ButtonStyleClass[index]); - }), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; - for (let key in options.attributes) - if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); - return $btn; + let $btn; + if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; + else $btn = CE("button", { class: "bx-button", type: "button" }); + const style = options.style || 0; + style && ButtonStyleIndices.forEach((index) => { + style & index && $btn.classList.add(ButtonStyleClass[index]); + }), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; }, CTN = document.createTextNode.bind(document); window.BX_CE = createElement; var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; 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": "中文(繁體)" + "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 = { - activate: "Activate", - activated: "Activated", - active: "Active", - advanced: "Advanced", - "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", - "back-to-home": "Back to home", - "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", - battery: "Battery", - "battery-saving": "Battery saving", - "better-xcloud": "Better xCloud", - "bitrate-audio-maximum": "Maximum audio bitrate", - "bitrate-video-maximum": "Maximum video bitrate", - "bottom-left": "Bottom-left", - "bottom-right": "Bottom-right", - brazil: "Brazil", - brightness: "Brightness", - "browser-unsupported-feature": "Your browser doesn't support this feature", - "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", - "clarity-boost": "Clarity boost", - "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", - clear: "Clear", - 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", - contrast: "Contrast", - controller: "Controller", - "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", - delete: "Delete", - 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-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", - "fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile", - "fortnite-force-console-version": "Fortnite: force console version", - "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", - import: "Import", - increase: "Increase", - "install-android": "Better xCloud app for Android", - japan: "Japan", - jitter: "Jitter", - "keyboard-shortcuts": "Keyboard shortcuts", - korea: "Korea", - language: "Language", - large: "Large", - layout: "Layout", - "left-stick": "Left stick", - "load-failed-message": "Failed to run Better xCloud", - "loading-screen": "Loading screen", - "local-co-op": "Local co-op", - "lowest-quality": "Lowest quality", - "map-mouse-to": "Map mouse to", - "max-fps": "Max FPS", - "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": "Using this feature when playing online could be viewed as cheating", - "mouse-and-keyboard": "Mouse & Keyboard", - "mouse-wheel": "Mouse wheel", - "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", - muted: "Muted", - name: "Name", - "native-mkb": "Native Mouse & Keyboard", - new: "New", - "new-version-available": [ - e => `Version ${e.version} available`, - , - , - e => `Version ${e.version} verfügbar`, - , - 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} 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", - normal: "Normal", - off: "Off", - on: "On", - "only-supports-some-games": "Only supports some games", - opacity: "Opacity", - other: "Other", - playing: "Playing", - playtime: "Playtime", - poland: "Poland", - 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-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 => `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", - "right-click-to-unbind": "Right-click on a key to unbind it", - "right-stick": "Right stick", - "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", - settings: "Settings", - "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", - 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", - 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-games": "All games", - "tc-all-white": "All white", - "tc-auto-off": "Off when controller found", - "tc-availability": "Availability", - "tc-custom-layout-style": "Custom layout's button style", - "tc-default-opacity": "Default opacity", - "tc-muted-colors": "Muted colors", - "tc-standard-layout-style": "Standard layout's button style", - "text-size": "Text size", - toggle: "Toggle", - "top-center": "Top-center", - "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", - "transparent-background": "Transparent background", - "true-achievements": "TrueAchievements", - ui: "UI", - "unexpected-behavior": "May cause unexpected behavior", - "united-states": "United States", - unknown: "Unknown", - unlimited: "Unlimited", - unmuted: "Unmuted", - "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", - "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", - wallpaper: "Wallpaper", - webgl2: "WebGL2" + activate: "Activate", + activated: "Activated", + active: "Active", + advanced: "Advanced", + "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", + "back-to-home": "Back to home", + "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", + battery: "Battery", + "battery-saving": "Battery saving", + "better-xcloud": "Better xCloud", + "bitrate-audio-maximum": "Maximum audio bitrate", + "bitrate-video-maximum": "Maximum video bitrate", + "bottom-left": "Bottom-left", + "bottom-right": "Bottom-right", + brazil: "Brazil", + brightness: "Brightness", + "browser-unsupported-feature": "Your browser doesn't support this feature", + "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", + "clarity-boost": "Clarity boost", + "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", + clear: "Clear", + 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", + contrast: "Contrast", + controller: "Controller", + "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", + delete: "Delete", + 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-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", + "fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile", + "fortnite-force-console-version": "Fortnite: force console version", + "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", + import: "Import", + increase: "Increase", + "install-android": "Better xCloud app for Android", + japan: "Japan", + jitter: "Jitter", + "keyboard-shortcuts": "Keyboard shortcuts", + korea: "Korea", + language: "Language", + large: "Large", + layout: "Layout", + "left-stick": "Left stick", + "load-failed-message": "Failed to run Better xCloud", + "loading-screen": "Loading screen", + "local-co-op": "Local co-op", + "lowest-quality": "Lowest quality", + "map-mouse-to": "Map mouse to", + "max-fps": "Max FPS", + "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": "Using this feature when playing online could be viewed as cheating", + "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-wheel": "Mouse wheel", + "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", + muted: "Muted", + name: "Name", + "native-mkb": "Native Mouse & Keyboard", + new: "New", + "new-version-available": [ + e => `Version ${e.version} available`, + , + , + e => `Version ${e.version} verfügbar`, + , + 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} 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", + normal: "Normal", + off: "Off", + on: "On", + "only-supports-some-games": "Only supports some games", + opacity: "Opacity", + other: "Other", + playing: "Playing", + playtime: "Playtime", + poland: "Poland", + 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-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 => `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", + "right-click-to-unbind": "Right-click on a key to unbind it", + "right-stick": "Right stick", + "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", + settings: "Settings", + "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", + 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", + 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-games": "All games", + "tc-all-white": "All white", + "tc-auto-off": "Off when controller found", + "tc-availability": "Availability", + "tc-custom-layout-style": "Custom layout's button style", + "tc-default-opacity": "Default opacity", + "tc-muted-colors": "Muted colors", + "tc-standard-layout-style": "Standard layout's button style", + "text-size": "Text size", + toggle: "Toggle", + "top-center": "Top-center", + "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", + "transparent-background": "Transparent background", + "true-achievements": "TrueAchievements", + ui: "UI", + "unexpected-behavior": "May cause unexpected behavior", + "united-states": "United States", + unknown: "Unknown", + unlimited: "Unlimited", + unmuted: "Unmuted", + "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", + "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", + wallpaper: "Wallpaper", + webgl2: "WebGL2" }; class Translations { - static #EN_US = "en-US"; - static #KEY_LOCALE = "better_xcloud_locale"; - static #KEY_TRANSLATIONS = "better_xcloud_translations"; - static #enUsIndex = -1; - static #selectedLocaleIndex = -1; - static #selectedLocale = "en-US"; - static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); - static #foreignTranslations = {}; - static async init() { - Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshLocale(), await Translations.#loadTranslations(); + static #EN_US = "en-US"; + static #KEY_LOCALE = "better_xcloud_locale"; + static #KEY_TRANSLATIONS = "better_xcloud_translations"; + static #enUsIndex = -1; + static #selectedLocaleIndex = -1; + static #selectedLocale = "en-US"; + static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static #foreignTranslations = {}; + static async init() { + Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), 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); + const 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); } - static refreshLocale(newLocale) { - let locale; - if (newLocale) localStorage.setItem(Translations.#KEY_LOCALE, newLocale), locale = newLocale; - else locale = localStorage.getItem(Translations.#KEY_LOCALE); - const 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); + 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; } - 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 { - const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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); + if (async) Translations.downloadTranslationsAsync(Translations.#selectedLocale); + else await Translations.downloadTranslations(Translations.#selectedLocale); + } + static async downloadTranslations(locale) { + try { + const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(); var BypassServers = { - br: t("brazil"), - jp: t("japan"), - kr: t("korea"), - pl: t("poland"), - us: t("united-states") + 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" + 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 SettingElement { - static #renderOptions(key, setting, currentValue, onChange) { - const $control = CE("select", { - tabindex: 0 + static #renderOptions(key, setting, currentValue, onChange) { + const $control = CE("select", { + tabindex: 0 + }); + let $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const 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 = {}) { + const $control = CE("select", { + multiple: !0, + tabindex: 0 + }); + if (params && params.size) $control.setAttribute("size", params.size.toString()); + for (let value in setting.multipleOptions) { + const label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + const target = e.target; + target.selected = !target.selected; + const $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + const self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), $control; + } + static #renderNumber(key, setting, currentValue, onChange) { + const $control = CE("input", { + tabindex: 0, + type: "number", + min: setting.min, + max: setting.max + }); + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); + target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); + }), $control; + } + static #renderCheckbox(key, setting, currentValue, onChange) { + const $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 = {}) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnDec, $btnInc, $range = null, controlValue = value; + const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => { + value2 = parseInt(value2); + let textContent = null; + if (options.customTextValue) textContent = options.customTextValue(value2); + if (textContent === null) textContent = value2.toString() + options.suffix; + return textContent; + }, updateButtonsVisibility = () => { + $btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX); + }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { + "data-type": "dec", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { + "data-type": "inc", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+")); + if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; + if ($range = CE("input", { + id: `bx_setting_${key}`, + type: "range", + min: MIN, + max: MAX, + value, + step: STEPS, + tabindex: 0 + }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { + if (value = parseInt(e.target.value), controlValue === value) return; + controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); + }), $wrapper.addEventListener("input", (e) => { + BxEvent.dispatch($range, "input"); + }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { + const 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: i })); + } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + $wrapper.appendChild($markers); + } + updateButtonsVisibility(); + let interval, isHolding = !1; + const onClick = (e) => { + if (isHolding) { + e.preventDefault(), isHolding = !1; + return; + } + const $btn = e.target; + let value2 = parseInt(controlValue); + if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); + else value2 = Math.min(MAX, value2 + STEPS); + controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2); + }, onMouseDown = (e) => { + e.preventDefault(), isHolding = !0; + const args = arguments; + interval && clearInterval(interval), interval = window.setInterval(() => { + e.target && BxEvent.dispatch(e.target, "click", { + arguments: args }); - let $parent; - if (setting.optionsGroup) $parent = CE("optgroup", { - label: setting.optionsGroup - }), $control.appendChild($parent); - else $parent = $control; - for (let value in setting.options) { - const label = setting.options[value], $option = CE("option", { value }, label); - $parent.appendChild($option); - } - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - const 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 = {}) { - const $control = CE("select", { - multiple: !0, - tabindex: 0 - }); - if (params && params.size) $control.setAttribute("size", params.size.toString()); - for (let value in setting.multipleOptions) { - const label = setting.multipleOptions[value], $option = CE("option", { value }, label); - $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { - e.preventDefault(); - const target = e.target; - target.selected = !target.selected; - const $parent = target.parentElement; - $parent.focus(), BxEvent.dispatch($parent, "input"); - }), $control.appendChild($option); - } - return $control.addEventListener("mousedown", function(e) { - const self = this, orgScrollTop = self.scrollTop; - window.setTimeout(() => self.scrollTop = orgScrollTop, 0); - }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { - const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); - !e.ignoreOnChange && onChange(e, values); - }), $control; - } - static #renderNumber(key, setting, currentValue, onChange) { - const $control = CE("input", { - tabindex: 0, - type: "number", - min: setting.min, - max: setting.max - }); - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); - target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); - }), $control; - } - static #renderCheckbox(key, setting, currentValue, onChange) { - const $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 = {}) { - options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; - let $text, $btnDec, $btnInc, $range = null, controlValue = value; - const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => { - value2 = parseInt(value2); - let textContent = null; - if (options.customTextValue) textContent = options.customTextValue(value2); - if (textContent === null) textContent = value2.toString() + options.suffix; - return textContent; - }, updateButtonsVisibility = () => { - $btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX); - }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { - "data-type": "dec", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { - "data-type": "inc", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "+")); - if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; - if ($range = CE("input", { - id: `bx_setting_${key}`, - type: "range", - min: MIN, - max: MAX, - value, - step: STEPS, - tabindex: 0 - }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { - if (value = parseInt(e.target.value), controlValue === value) return; - controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); - }), $wrapper.addEventListener("input", (e) => { - BxEvent.dispatch($range, "input"); - }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { - const 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: i })); - } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) - $markers.appendChild(CE("option", { value: i })); - $wrapper.appendChild($markers); - } - updateButtonsVisibility(); - let interval, isHolding = !1; - const onClick = (e) => { - if (isHolding) { - e.preventDefault(), isHolding = !1; - return; - } - const $btn = e.target; - let value2 = parseInt(controlValue); - if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); - else value2 = Math.min(MAX, value2 + STEPS); - controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2); - }, onMouseDown = (e) => { - e.preventDefault(), isHolding = !0; - const args = arguments; - interval && clearInterval(interval), interval = window.setInterval(() => { - e.target && BxEvent.dispatch(e.target, "click", { - arguments: args - }); - }, 200); - }, onMouseUp = (e) => { - e.preventDefault(), interval && clearInterval(interval), isHolding = !1; - }, onContextMenu = (e) => e.preventDefault(); - return $wrapper.setValue = (value2) => { - $text.textContent = renderTextValue(value2), $range.value = value2; - }, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { - focus: $range || $btnInc - }), $wrapper; - } - static #METHOD_MAP = { - options: SettingElement.#renderOptions, - "multiple-options": SettingElement.#renderMultipleOptions, - number: SettingElement.#renderNumber, - "number-stepper": SettingElement.#renderNumberStepper, - checkbox: SettingElement.#renderCheckbox - }; - static render(type, key, setting, currentValue, onChange, options) { - const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); - if (type !== "number-stepper") $control.id = `bx_setting_${key}`; - if (type === "options" || type === "multiple-options") $control.name = $control.id; - return $control; - } - static fromPref(key, storage, onChange, overrideParams = {}) { - const definition = storage.getDefinition(key); - let currentValue = storage.getSetting(key), type; - if ("type" in definition) type = definition.type; - else if ("options" in definition) type = "options"; - else if ("multipleOptions" in definition) type = "multiple-options"; - else if (typeof definition.default === "number") type = "number"; - 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) => { - storage.setSetting(key, value), onChange && onChange(e, value); - }, params); - } + }, 200); + }, onMouseUp = (e) => { + e.preventDefault(), interval && clearInterval(interval), isHolding = !1; + }, onContextMenu = (e) => e.preventDefault(); + return $wrapper.setValue = (value2) => { + $text.textContent = renderTextValue(value2), $range.value = value2; + }, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { + focus: $range || $btnInc + }), $wrapper; + } + static #METHOD_MAP = { + options: SettingElement.#renderOptions, + "multiple-options": SettingElement.#renderMultipleOptions, + number: SettingElement.#renderNumber, + "number-stepper": SettingElement.#renderNumberStepper, + checkbox: SettingElement.#renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${key}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, storage, onChange, overrideParams = {}) { + const definition = storage.getDefinition(key); + let currentValue = storage.getSetting(key), type; + if ("type" in definition) type = definition.type; + else if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number"; + 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) => { + storage.setSetting(key, value), onChange && onChange(e, value); + }, params); + } } class BaseSettingsStore { - storage; - storageKey; - _settings; - definitions; - constructor(storageKey, definitions) { - this.storage = window.localStorage, this.storageKey = storageKey; - let settingId; - for (settingId in definitions) { - const setting = definitions[settingId]; - if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; - setting.ready && setting.ready.call(this, setting); - } - this.definitions = definitions, this._settings = null; + storage; + storageKey; + _settings; + definitions; + constructor(storageKey, definitions) { + this.storage = window.localStorage, this.storageKey = storageKey; + let settingId; + for (settingId in definitions) { + const setting = definitions[settingId]; + if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; + setting.ready && setting.ready.call(this, setting); } - get settings() { - if (this._settings) return this._settings; - const settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); - return this._settings = settings, settings; + this.definitions = definitions, this._settings = null; + } + get settings() { + if (this._settings) return this._settings; + const settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + return this._settings = settings, settings; + } + getDefinition(key) { + if (!this.definitions[key]) { + const error = "Request invalid definition: " + key; + throw alert(error), Error(error); } - getDefinition(key) { - if (!this.definitions[key]) { - const error = "Request invalid definition: " + key; - throw alert(error), Error(error); - } - return this.definitions[key]; + return this.definitions[key]; + } + getSetting(key, checkUnsupported = !0) { + if (typeof key === "undefined") { + debugger; + return; } - getSetting(key, checkUnsupported = !0) { - if (typeof key === "undefined") { - debugger; - return; - } - const definition = this.definitions[key]; - if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; - if (checkUnsupported && definition.unsupported) return definition.default; - if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); - return this.settings[key]; - } - setSetting(key, value, emitEvent = !1) { - return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { - storageKey: this.storageKey, - settingKey: key, - settingValue: value - }), value; - } - saveSettings() { - this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); - } - validateValue(key, value) { - const def = this.definitions[key]; - if (!def) return value; - if (typeof value === "undefined" || value === null) value = def.default; - if ("min" in def) value = Math.max(def.min, value); - if ("max" in def) value = Math.min(def.max, value); - if ("options" in def && !(value in def.options)) value = def.default; - else if ("multipleOptions" in def) { - if (value.length) { - const validOptions = Object.keys(def.multipleOptions); - value.forEach((item, idx) => { - validOptions.indexOf(item) === -1 && value.splice(idx, 1); - }); - } - if (!value.length) value = def.default; - } - return value; - } - getLabel(key) { - return this.definitions[key].label || key; - } - getValueText(key, value) { - const definition = this.definitions[key]; - if (definition.type === "number-stepper") { - const params = definition.params; - if (params.customTextValue) { - const text = params.customTextValue(value); - if (text) return text; - } - return value.toString(); - } else if ("options" in definition) { - const options = definition.options; - if (value in options) return options[value]; - } else if (typeof value === "boolean") return value ? t("on") : t("off"); - return value.toString(); + const definition = this.definitions[key]; + if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; + if (checkUnsupported && definition.unsupported) return definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); + return this.settings[key]; + } + setSetting(key, value, emitEvent = !1) { + return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { + storageKey: this.storageKey, + settingKey: key, + settingValue: value + }), value; + } + saveSettings() { + this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); + } + validateValue(key, value) { + const def = this.definitions[key]; + if (!def) return value; + if (typeof value === "undefined" || value === null) value = def.default; + if ("min" in def) value = Math.max(def.min, value); + if ("max" in def) value = Math.min(def.max, value); + if ("options" in def && !(value in def.options)) value = def.default; + else if ("multipleOptions" in def) { + if (value.length) { + const validOptions = Object.keys(def.multipleOptions); + value.forEach((item, idx) => { + validOptions.indexOf(item) === -1 && value.splice(idx, 1); + }); + } + if (!value.length) value = def.default; } + return value; + } + getLabel(key) { + return this.definitions[key].label || key; + } + getValueText(key, value) { + const definition = this.definitions[key]; + if (definition.type === "number-stepper") { + const params = definition.params; + if (params.customTextValue) { + const text = params.customTextValue(value); + if (text) return text; + } + return value.toString(); + } else if ("options" in definition) { + const options = definition.options; + if (value in options) return options[value]; + } else if (typeof value === "boolean") return value ? t("on") : t("off"); + return value.toString(); + } } class StreamStatsCollector { - static instance; - static getInstance() { - if (!StreamStatsCollector.instance) StreamStatsCollector.instance = new StreamStatsCollector; - return StreamStatsCollector.instance; - } - 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(); - } - }, - jit: { - current: 0, - grades: [30, 40, 60], - toString() { - return `${this.current.toFixed(2)}ms`; - } - }, - fps: { - current: 0, - toString() { - return this.current.toString(); - } - }, - btr: { - current: 0, - toString() { - return `${this.current.toFixed(2)} Mbps`; - } - }, - fl: { - received: 0, - dropped: 0, - toString() { - const framesDroppedPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); - return framesDroppedPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`; - } - }, - pl: { - received: 0, - dropped: 0, - toString() { - const packetsLostPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); - return packetsLostPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`; - } - }, - dt: { - current: 0, - total: 0, - grades: [6, 9, 12], - toString() { - return isNaN(this.current) ? "??ms" : `${this.current.toFixed(2)}ms`; - } - }, - dl: { - total: 0, - toString() { - return humanFileSize(this.total); - } - }, - 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) { - const 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 - }); - } + static instance; + static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new 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(); + } + }, + jit: { + current: 0, + grades: [30, 40, 60], + toString() { + return `${this.current.toFixed(2)}ms`; + } + }, + fps: { + current: 0, + toString() { + return this.current.toString(); + } + }, + btr: { + current: 0, + toString() { + return `${this.current.toFixed(2)} Mbps`; + } + }, + fl: { + received: 0, + dropped: 0, + toString() { + const framesDroppedPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); + return framesDroppedPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`; + } + }, + pl: { + received: 0, + dropped: 0, + toString() { + const packetsLostPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); + return packetsLostPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`; + } + }, + dt: { + current: 0, + total: 0, + grades: [6, 9, 12], + toString() { + return isNaN(this.current) ? "??ms" : `${this.current.toFixed(2)}ms`; + } + }, + dl: { + total: 0, + toString() { + return humanFileSize(this.total); + } + }, + 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) { + const diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : ""; + text += ` (${sign}${diffLevel}%)`; } - }; - lastVideoStat; - async collect() { - const stats = await STATES.currentStream.peerConnection?.getStats(); - if (!stats) return; - stats.forEach((stat) => { - if (stat.type === "inbound-rtp" && stat.kind === "video") { - const fps = this.currentStats.fps; - fps.current = stat.framesPerSecond || 0; - const pl = this.currentStats.pl; - pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; - const fl = this.currentStats.fl; - if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { - this.lastVideoStat = stat; - return; - } - const 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; - const btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; - btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; - const dt = this.currentStats.dt; - dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; - const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; - dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; - } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { - const ping = this.currentStats.ping; - ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; - const dl = this.currentStats.dl; - dl.total = stat.bytesReceived; - const ul = this.currentStats.ul; - ul.total = stat.bytesSent; - } - }); - let batteryLevel = 100, isCharging = !1; - if (STATES.browser.capabilities.batteryApi) try { - const bm = await navigator.getBattery(); - isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); - } catch (e) {} - const battery = this.currentStats.batt; - battery.current = batteryLevel, battery.isCharging = isCharging; - const playTime = this.currentStats.play, now = +new Date; - playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); - } - getStat(kind) { - return this.currentStats[kind]; - } - reset() { - const 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() { - window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - StreamStatsCollector.getInstance().reset(); + return text; + } + }, + time: { + toString() { + return (new Date()).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: !1 }); + } } + }; + lastVideoStat; + async collect() { + const stats = await STATES.currentStream.peerConnection?.getStats(); + if (!stats) return; + stats.forEach((stat) => { + if (stat.type === "inbound-rtp" && stat.kind === "video") { + const fps = this.currentStats.fps; + fps.current = stat.framesPerSecond || 0; + const pl = this.currentStats.pl; + pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; + const fl = this.currentStats.fl; + if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { + this.lastVideoStat = stat; + return; + } + const 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; + const btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; + btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; + const dt = this.currentStats.dt; + dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; + const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; + dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; + } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { + const ping = this.currentStats.ping; + ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; + const dl = this.currentStats.dl; + dl.total = stat.bytesReceived; + const ul = this.currentStats.ul; + ul.total = stat.bytesSent; + } + }); + let batteryLevel = 100, isCharging = !1; + if (STATES.browser.capabilities.batteryApi) try { + const bm = await navigator.getBattery(); + isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); + } catch (e) {} + const battery = this.currentStats.batt; + battery.current = batteryLevel, battery.isCharging = isCharging; + const playTime = this.currentStats.play, now = +new Date; + playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); + } + getStat(kind) { + return this.currentStats[kind]; + } + reset() { + const 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() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + StreamStatsCollector.getInstance().reset(); + }); + } } function getSupportedCodecProfiles() { - const options = { - default: t("default") - }; - if (!("getCapabilities" in RTCRtpReceiver)) return options; - let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; - const codecs = RTCRtpReceiver.getCapabilities("video").codecs; - for (let codec of codecs) { - if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; - const 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; + const options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; + const codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + const 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 BaseSettingsStore { - static DEFINITIONS = { - version_last_check: { - default: 0 - }, - version_latest: { - default: "" - }, - version_current: { - default: "" - }, - bx_locale: { - label: t("language"), - default: localStorage.getItem("better_xcloud_locale") || "en-US", - options: SUPPORTED_LANGUAGES - }, - server_region: { - label: t("region"), - default: "default" - }, - server_bypass_restriction: { - 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_preferred_locale: { - label: t("preferred-game-language"), - default: "default", - options: { - default: t("default"), - "ar-SA": "العربية", - "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)", - "ru-RU": "русский", - "sk-SK": "slovenčina", - "sv-SE": "svenska", - "tr-TR": "Türkçe", - "zh-CN": "中文(简体)", - "zh-TW": "中文 (繁體)" - } - }, - stream_target_resolution: { - label: t("target-resolution"), - default: "auto", - options: { - auto: t("default"), - "720p": "720p", - "1080p": "1080p" - }, - suggest: { - lowest: "720p", - highest: "1080p" - } - }, - stream_codec_profile: { - label: t("visual-quality"), - default: "default", - options: getSupportedCodecProfiles(), - ready: (setting) => { - const 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] - }; - } - }, - prefer_ipv6_server: { - label: t("prefer-ipv6-server"), - default: !1 - }, - screenshot_apply_filters: { - requiredVariants: "full", - label: t("screenshot-apply-filters"), - default: !1 - }, - skip_splash_video: { - label: t("skip-splash-video"), - default: !1 - }, - hide_dots_icon: { - label: t("hide-system-menu-icon"), - default: !1 - }, - stream_combine_sources: { - requiredVariants: "full", - label: t("combine-audio-video-streams"), - default: !1, - experimental: !0, - note: t("combine-audio-video-streams-summary") - }, - stream_touch_controller: { - requiredVariants: "full", - label: t("tc-availability"), - default: "all", - options: { - default: t("default"), - all: t("tc-all-games"), - off: t("off") - }, - unsupported: !STATES.userAgent.capabilities.touch, - ready: (setting) => { - if (setting.unsupported) setting.default = "default"; - } - }, - stream_touch_controller_auto_off: { - requiredVariants: "full", - label: t("tc-auto-off"), - default: !1, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_default_opacity: { - requiredVariants: "full", - type: "number-stepper", - label: t("tc-default-opacity"), - default: 100, - min: 10, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10, - hideSlider: !0 - }, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_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 - }, - stream_touch_controller_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 - }, - stream_simplify_menu: { - label: t("simplify-stream-menu"), - default: !1 - }, - mkb_hide_idle_cursor: { - requiredVariants: "full", - label: t("hide-idle-cursor"), - default: !1 - }, - stream_disable_feedback_dialog: { - requiredVariants: "full", - label: t("disable-post-stream-feedback-dialog"), - default: !1 - }, - bitrate_video_max: { - requiredVariants: "full", - type: "number-stepper", - label: t("bitrate-video-maximum"), - note: "⚠️ " + t("unexpected-behavior"), - default: 0, - min: 0, - max: 14336000, - steps: 102400, - params: { - exactTicks: 5120000, - customTextValue: (value) => { - if (value = parseInt(value), value === 0) return t("unlimited"); - else return (value / 1024000).toFixed(1) + " Mb/s"; - } - }, - suggest: { - highest: 0 - } - }, - game_bar_position: { - requiredVariants: "full", - label: t("position"), - default: "bottom-left", - options: { - "bottom-left": t("bottom-left"), - "bottom-right": t("bottom-right"), - off: t("off") - } - }, - local_co_op_enabled: { - requiredVariants: "full", - label: t("enable-local-co-op-support"), - default: !1, - note: CE("a", { - href: "https://github.com/redphx/better-xcloud/discussions/275", - target: "_blank" - }, t("enable-local-co-op-support-note")) - }, - controller_show_connection_status: { - label: t("show-controller-connection-status"), - default: !0 - }, - controller_enable_shortcuts: { - requiredVariants: "full", - default: !1 - }, - controller_enable_vibration: { - requiredVariants: "full", - label: t("controller-vibration"), - default: !0 - }, - controller_device_vibration: { - requiredVariants: "full", - label: t("device-vibration"), - default: "off", - options: { - on: t("on"), - auto: t("device-vibration-not-using-gamepad"), - off: t("off") - } - }, - controller_vibration_intensity: { - requiredVariants: "full", - label: t("vibration-intensity"), - type: "number-stepper", - default: 100, - min: 0, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - mkb_enabled: { - requiredVariants: "full", - label: t("enable-mkb"), - default: !1, - unsupported: !STATES.userAgent.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); - } - }, - native_mkb_enabled: { - requiredVariants: "full", - label: t("native-mkb"), - default: "default", - options: { - default: t("default"), - on: t("on"), - off: t("off") - }, - ready: (setting) => { - if (AppInterface) ; - else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; - else delete setting.options.on; - } - }, - native_mkb_scroll_x_sensitivity: { - requiredVariants: "full", - label: t("horizontal-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - native_mkb_scroll_y_sensitivity: { - requiredVariants: "full", - label: t("vertical-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - mkb_default_preset_id: { - requiredVariants: "full", - default: 0 - }, - mkb_absolute_mouse: { - requiredVariants: "full", - default: !1 - }, - reduce_animations: { - label: t("reduce-animations"), - default: !1 - }, - ui_loading_screen_game_art: { - requiredVariants: "full", - label: t("show-game-art"), - default: !0 - }, - ui_loading_screen_wait_time: { - label: t("show-wait-time"), - default: !0 - }, - ui_loading_screen_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_controller_friendly: { - 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_scrollbar_hide: { - label: t("hide-scrollbar"), - default: !1 - }, - ui_home_context_menu_disabled: { - requiredVariants: "full", - label: t("disable-home-context-menu"), - default: STATES.browser.capabilities.touch - }, - ui_hide_sections: { - 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: 6 - } - }, - ui_game_card_show_wait_time: { - requiredVariants: "full", - label: t("show-wait-time-in-game-card"), - default: !1 - }, - block_social_features: { - label: t("disable-social-features"), - default: !1 - }, - block_tracking: { - label: t("disable-xcloud-analytics"), - default: !1 - }, - user_agent_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") - } - }, - 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_power_preference: { - 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_max_fps: { - label: t("max-fps"), - type: "number-stepper", - default: 60, - min: 10, - max: 60, - steps: 10, - params: { - exactTicks: 10, - customTextValue: (value) => { - return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; - } - } - }, - video_sharpness: { - label: t("sharpness"), - type: "number-stepper", - 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: t("aspect-ratio-note"), - default: "16:9", - options: { - "16:9": "16:9", - "18:9": "18:9", - "21:9": "21:9", - "16:10": "16:10", - "4:3": "4:3", - fill: t("stretch") - } - }, - video_saturation: { - label: t("saturation"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_contrast: { - label: t("contrast"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_brightness: { - label: t("brightness"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - audio_mic_on_playing: { - label: t("enable-mic-on-startup"), - default: !1 - }, - audio_enable_volume_control: { - requiredVariants: "full", - label: t("enable-volume-control"), - default: !1 - }, - audio_volume: { - label: t("volume"), - type: "number-stepper", - default: 100, - min: 0, - max: 600, - steps: 10, - params: { - suffix: "%", - ticks: 100 - } - }, - stats_items: { - label: t("stats"), - default: ["ping", "fps", "btr", "dt", "pl", "fl"], - multipleOptions: { - time: `TIME: ${t("clock")}`, - play: `PLAY: ${t("playtime")}`, - batt: `BATT: ${t("battery")}`, - ping: `PING: ${t("stat-ping")}`, - jit: `JIT: ${t("jitter")}`, - fps: `FPS: ${t("stat-fps")}`, - btr: `BTR: ${t("stat-bitrate")}`, - dt: `DT: ${t("stat-decode-time")}`, - pl: `PL: ${t("stat-packets-lost")}`, - fl: `FL: ${t("stat-frames-lost")}`, - dl: `DL: ${t("downloaded")}`, - ul: `UL: ${t("uploaded")}` - }, - params: { - size: 6 - }, - ready: (setting) => { - const multipleOptions = setting.multipleOptions; - if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; - } - }, - stats_show_when_playing: { - label: t("show-stats-on-startup"), - default: !1 - }, - stats_quick_glance: { - 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_text_size: { - label: t("text-size"), - default: "0.9rem", - options: { - "0.9rem": t("small"), - "1.0rem": t("normal"), - "1.1rem": t("large") - } - }, - stats_transparent: { - label: t("transparent-background"), - default: !1 - }, - stats_opacity: { - label: t("opacity"), - type: "number-stepper", - default: 80, - min: 50, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - stats_conditional_formatting: { - label: t("conditional-formatting"), - default: !1 - }, - xhome_enabled: { - requiredVariants: "full", - label: t("enable-remote-play-feature"), - default: !1 - }, - xhome_resolution: { - requiredVariants: "full", - default: "1080p", - options: { - "1080p": "1080p", - "720p": "720p" - } - }, - game_fortnite_force_console: { - requiredVariants: "full", - label: "🎮 " + t("fortnite-force-console-version"), - default: !1, - note: t("fortnite-allow-stw-mode") - }, - game_msfs2020_force_native_mkb: { - requiredVariants: "full", - label: "✈️ " + t("msfs2020-force-native-mkb"), - default: !1, - note: t("may-not-work-properly") + static DEFINITIONS = { + version_last_check: { + default: 0 + }, + version_latest: { + default: "" + }, + version_current: { + default: "" + }, + bx_locale: { + label: t("language"), + default: localStorage.getItem("better_xcloud_locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + server_region: { + label: t("region"), + default: "default" + }, + server_bypass_restriction: { + 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_preferred_locale: { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "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)", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + stream_target_resolution: { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p" + }, + suggest: { + lowest: "720p", + highest: "1080p" + } + }, + stream_codec_profile: { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + const 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] + }; + } + }, + prefer_ipv6_server: { + label: t("prefer-ipv6-server"), + default: !1 + }, + screenshot_apply_filters: { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + skip_splash_video: { + label: t("skip-splash-video"), + default: !1 + }, + hide_dots_icon: { + label: t("hide-system-menu-icon"), + default: !1 + }, + stream_combine_sources: { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + stream_touch_controller: { + requiredVariants: "full", + label: t("tc-availability"), + default: "all", + options: { + default: t("default"), + all: t("tc-all-games"), + off: t("off") + }, + unsupported: !STATES.userAgent.capabilities.touch, + ready: (setting) => { + if (setting.unsupported) setting.default = "default"; + } + }, + stream_touch_controller_auto_off: { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_default_opacity: { + requiredVariants: "full", + type: "number-stepper", + label: t("tc-default-opacity"), + default: 100, + min: 10, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_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 + }, + stream_touch_controller_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 + }, + stream_simplify_menu: { + label: t("simplify-stream-menu"), + default: !1 + }, + mkb_hide_idle_cursor: { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + stream_disable_feedback_dialog: { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + bitrate_video_max: { + requiredVariants: "full", + type: "number-stepper", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 0, + max: 14336000, + steps: 102400, + params: { + exactTicks: 5120000, + customTextValue: (value) => { + if (value = parseInt(value), value === 0) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; } - }; - constructor() { - super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); + }, + suggest: { + highest: 0 + } + }, + game_bar_position: { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right"), + off: t("off") + } + }, + local_co_op_enabled: { + requiredVariants: "full", + label: t("enable-local-co-op-support"), + default: !1, + note: CE("a", { + href: "https://github.com/redphx/better-xcloud/discussions/275", + target: "_blank" + }, t("enable-local-co-op-support-note")) + }, + controller_show_connection_status: { + label: t("show-controller-connection-status"), + default: !0 + }, + controller_enable_shortcuts: { + requiredVariants: "full", + default: !1 + }, + controller_enable_vibration: { + requiredVariants: "full", + label: t("controller-vibration"), + default: !0 + }, + controller_device_vibration: { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + on: t("on"), + auto: t("device-vibration-not-using-gamepad"), + off: t("off") + } + }, + controller_vibration_intensity: { + requiredVariants: "full", + label: t("vibration-intensity"), + type: "number-stepper", + default: 100, + min: 0, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + mkb_enabled: { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: !STATES.userAgent.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); + } + }, + native_mkb_enabled: { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + on: t("on"), + off: t("off") + }, + ready: (setting) => { + if (AppInterface) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; + else delete setting.options.on; + } + }, + native_mkb_scroll_x_sensitivity: { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + native_mkb_scroll_y_sensitivity: { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + mkb_default_preset_id: { + requiredVariants: "full", + default: 0 + }, + mkb_absolute_mouse: { + requiredVariants: "full", + default: !1 + }, + reduce_animations: { + label: t("reduce-animations"), + default: !1 + }, + ui_loading_screen_game_art: { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + ui_loading_screen_wait_time: { + label: t("show-wait-time"), + default: !0 + }, + ui_loading_screen_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_controller_friendly: { + 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_scrollbar_hide: { + label: t("hide-scrollbar"), + default: !1 + }, + ui_home_context_menu_disabled: { + requiredVariants: "full", + label: t("disable-home-context-menu"), + default: STATES.browser.capabilities.touch + }, + ui_hide_sections: { + 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: 6 + } + }, + ui_game_card_show_wait_time: { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !1 + }, + block_social_features: { + label: t("disable-social-features"), + default: !1 + }, + block_tracking: { + label: t("disable-xcloud-analytics"), + default: !1 + }, + user_agent_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") + } + }, + 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_power_preference: { + 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_max_fps: { + label: t("max-fps"), + type: "number-stepper", + default: 60, + min: 10, + max: 60, + steps: 10, + params: { + exactTicks: 10, + customTextValue: (value) => { + return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; + } + } + }, + video_sharpness: { + label: t("sharpness"), + type: "number-stepper", + 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: t("aspect-ratio-note"), + default: "16:9", + options: { + "16:9": "16:9", + "18:9": "18:9", + "21:9": "21:9", + "16:10": "16:10", + "4:3": "4:3", + fill: t("stretch") + } + }, + video_saturation: { + label: t("saturation"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_contrast: { + label: t("contrast"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_brightness: { + label: t("brightness"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + audio_mic_on_playing: { + label: t("enable-mic-on-startup"), + default: !1 + }, + audio_enable_volume_control: { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + audio_volume: { + label: t("volume"), + type: "number-stepper", + default: 100, + min: 0, + max: 600, + steps: 10, + params: { + suffix: "%", + ticks: 100 + } + }, + stats_items: { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + time: `TIME: ${t("clock")}`, + play: `PLAY: ${t("playtime")}`, + batt: `BATT: ${t("battery")}`, + ping: `PING: ${t("stat-ping")}`, + jit: `JIT: ${t("jitter")}`, + fps: `FPS: ${t("stat-fps")}`, + btr: `BTR: ${t("stat-bitrate")}`, + dt: `DT: ${t("stat-decode-time")}`, + pl: `PL: ${t("stat-packets-lost")}`, + fl: `FL: ${t("stat-frames-lost")}`, + dl: `DL: ${t("downloaded")}`, + ul: `UL: ${t("uploaded")}` + }, + params: { + size: 6 + }, + ready: (setting) => { + const multipleOptions = setting.multipleOptions; + if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; + } + }, + stats_show_when_playing: { + label: t("show-stats-on-startup"), + default: !1 + }, + stats_quick_glance: { + 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_text_size: { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + stats_transparent: { + label: t("transparent-background"), + default: !1 + }, + stats_opacity: { + label: t("opacity"), + type: "number-stepper", + default: 80, + min: 50, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + stats_conditional_formatting: { + label: t("conditional-formatting"), + default: !1 + }, + xhome_enabled: { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + default: !1 + }, + xhome_resolution: { + requiredVariants: "full", + default: "1080p", + options: { + "1080p": "1080p", + "720p": "720p" + } + }, + game_fortnite_force_console: { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + }, + game_msfs2020_force_native_mkb: { + requiredVariants: "full", + label: "✈️ " + t("msfs2020-force-native-mkb"), + default: !1, + note: t("may-not-work-properly") } + }; + constructor() { + super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); + } } var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); STORAGE.Global = globalSettings; 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", "⇀"], - 11: ["R3", "↻"], - 200: ["Right Stick Up", "↿"], - 201: ["Right Stick Down", "⇃"], - 202: ["Right Stick Left", "↽"], - 203: ["Right Stick Right", "⇁"] + 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", "⇀"], + 11: ["R3", "↻"], + 200: ["Right Stick Up", "↿"], + 201: ["Right Stick Down", "⇃"], + 202: ["Right Stick Left", "↽"], + 203: ["Right Stick Right", "⇁"] }; var MouseMapTo; ((MouseMapTo2) => { - MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; - MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; - MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; + MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; + MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; + MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; })(MouseMapTo ||= {}); class StreamStats { - static instance; - static getInstance() { - if (!StreamStats.instance) StreamStats.instance = new StreamStats; - return StreamStats.instance; + static instance; + static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new 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") } - 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() { - this.render(); + }; + $container; + quickGlanceObserver; + 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.bind(this), 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(); + } + onStoppedPlaying() { + 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; + const $uiContainer = document.querySelector("div[data-testid=ui-container]"); + if (!$uiContainer) return; + this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { + for (let record of mutationList) { + const $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; + } + async update(forceUpdate = !1) { + if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { + this.onStoppedPlaying(); + return; } - 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.bind(this), this.REFRESH_INTERVAL); + const PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"); + let grade = ""; + const statsCollector = StreamStatsCollector.getInstance(); + await statsCollector.collect(); + let statKey; + for (statKey in this.stats) { + grade = ""; + const 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; } - 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(); - } - onStoppedPlaying() { - 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; - const $uiContainer = document.querySelector("div[data-testid=ui-container]"); - if (!$uiContainer) return; - this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { - for (let record of mutationList) { - const $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; - } - async update(forceUpdate = !1) { - if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { - this.onStoppedPlaying(); - return; - } - const PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"); - let grade = ""; - const statsCollector = StreamStatsCollector.getInstance(); - await statsCollector.collect(); - let statKey; - for (statKey in this.stats) { - grade = ""; - const 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() { - const PREF_ITEMS = getPref("stats_items"), $container = this.$container; - $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); - } - hideSettingsUi() { - if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); - } - async render() { - this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); - let statKey; - for (statKey in this.stats) { - const stat = this.stats[statKey], $div = CE("div", { - class: `bx-stat-${statKey}`, - title: stat.name - }, CE("label", {}, statKey.toUpperCase()), stat.$element); - this.$container.appendChild($div); - } - this.refreshStyles(), document.documentElement.appendChild(this.$container); - } - static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - const PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), 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(); + } + refreshStyles() { + const PREF_ITEMS = getPref("stats_items"), $container = this.$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); + } + hideSettingsUi() { + if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); + } + async render() { + this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); + let statKey; + for (statKey in this.stats) { + const stat = this.stats[statKey], $div = CE("div", { + class: `bx-stat-${statKey}`, + title: stat.name + }, CE("label", {}, statKey.toUpperCase()), stat.$element); + this.$container.appendChild($div); } + this.refreshStyles(), document.documentElement.appendChild(this.$container); + } + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + const PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), 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 Toast { - static $wrapper; - static $msg; - static $status; - static stack = []; - static isShowing = !1; - static timeout; - static DURATION = 3000; - static show(msg, status, options = {}) { - options = options || {}; - const args = Array.from(arguments); - if (options.instant) Toast.stack = [args], Toast.showNext(); - else Toast.stack.push(args), !Toast.isShowing && Toast.showNext(); - } - static showNext() { - if (!Toast.stack.length) { - Toast.isShowing = !1; - return; - } - Toast.isShowing = !0, Toast.timeout && clearTimeout(Toast.timeout), Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION); - const [msg, status, options] = Toast.stack.shift(); - if (options && options.html) Toast.$msg.innerHTML = msg; - else Toast.$msg.textContent = msg; - if (status) Toast.$status.classList.remove("bx-gone"), Toast.$status.textContent = status; - else Toast.$status.classList.add("bx-gone"); - const classList = Toast.$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - static hide() { - Toast.timeout = null; - const classList = Toast.$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static setup() { - Toast.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.$msg = CE("span", { class: "bx-toast-msg" }), Toast.$status = CE("span", { class: "bx-toast-status" })), Toast.$wrapper.addEventListener("transitionend", (e) => { - const classList = Toast.$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.showNext(); - }), document.documentElement.appendChild(Toast.$wrapper); + static $wrapper; + static $msg; + static $status; + static stack = []; + static isShowing = !1; + static timeout; + static DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) Toast.stack = [args], Toast.showNext(); + else Toast.stack.push(args), !Toast.isShowing && Toast.showNext(); + } + static showNext() { + if (!Toast.stack.length) { + Toast.isShowing = !1; + return; } + Toast.isShowing = !0, Toast.timeout && clearTimeout(Toast.timeout), Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION); + const [msg, status, options] = Toast.stack.shift(); + if (options && options.html) Toast.$msg.innerHTML = msg; + else Toast.$msg.textContent = msg; + if (status) Toast.$status.classList.remove("bx-gone"), Toast.$status.textContent = status; + else Toast.$status.classList.add("bx-gone"); + const classList = Toast.$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + static hide() { + Toast.timeout = null; + const classList = Toast.$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + } + static setup() { + Toast.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.$msg = CE("span", { class: "bx-toast-msg" }), Toast.$status = CE("span", { class: "bx-toast-status" })), Toast.$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.showNext(); + }), document.documentElement.appendChild(Toast.$wrapper); + } } function ceilToNearest(value, interval) { - return Math.ceil(value / interval) * interval; + return Math.ceil(value / interval) * interval; } function floorToNearest(value, interval) { - return Math.floor(value / interval) * 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; + 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(); + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); } class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - const currentValue = getPref("audio_volume"); - let 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 = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + static adjustGainNodeVolume(amount) { + if (!getPref("audio_enable_volume_control")) return 0; + const currentValue = getPref("audio_volume"); + let 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 = setPref("audio_volume", newValue, !0), 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 (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); + let targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); + 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 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); - let targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - 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 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media; - if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - const status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); } + } } class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - const $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple; - let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; - if (isMultiple) $content = CE("button", { - class: "bx-select-value bx-focusable", - tabindex: 0 - }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { - $checkBox.click(); - }), $checkBox.addEventListener("input", (e) => { - const $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - const getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - const $option = getOptionAtIndex(visibleIndex); - let content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - const fragment = document.createDocumentFragment(); - fragment.appendChild(CE("span", {}, $option.parentElement.label)), 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), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); - const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; - $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); - }, normalizeIndex = (index) => { - return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); - }, onPrevNext = (e) => { - if (!e.target) return; - const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; - let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; - if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; - if (isMultiple) render(); - else BxEvent.dispatch($select, "input"); - }; - $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { - mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") render(); - }); - }).observe($select, { - subtree: !0, - childList: !0, - attributes: !0 - }), render(); - const $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } + static wrap($select) { + $select.removeAttribute("tabindex"); + const $btnPrev = createButton({ + label: "<", + style: 32 + }), $btnNext = createButton({ + label: ">", + style: 32 + }), isMultiple = $select.multiple; + let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; + if (isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { + $checkBox.click(); + }), $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); + const getOptionAtIndex = (index) => { + return Array.from($select.querySelectorAll("option"))[index]; + }, render = (e) => { + if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { + $label.innerHTML = ""; + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, $option.parentElement.label)), 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), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + if (!e.target) return; + const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) render(); + else BxEvent.dispatch($select, "input"); + }; + $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") render(); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), render(); + const $div = CE("div", { + class: "bx-select", + _nearby: { + orientation: "horizontal", + focus: $btnNext + } + }, $select, $btnPrev, $content, $btnNext); + return Object.defineProperty($div, "value", { + get() { + return $select.value; + }, + set(value) { + $div.setValue(value); + } + }), $div.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, $div.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, $div.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, $div.setValue = (value) => { + if ("setValue" in $select) $select.setValue(value); + else $select.value = value; + }, $div; + } } function onChangeVideoPlayerType() { - const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); - if (!$videoProcessing) return; - let isDisabled = !1; - const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); - if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $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"), updateVideoPlayer(); + const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); + if (!$videoProcessing) return; + let isDisabled = !1; + const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $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"), updateVideoPlayer(); } -function limitVideoPlayerFps() { - const targetFps = getPref("video_max_fps"); - STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); +function limitVideoPlayerFps(targetFps) { + STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); } function updateVideoPlayer() { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - limitVideoPlayerFps(); - const options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") - }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + limitVideoPlayerFps(getPref("video_max_fps")); + const options = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); } window.addEventListener("resize", updateVideoPlayer); class MkbPreset { - static MOUSE_SETTINGS = { - map_to: { - label: t("map-mouse-to"), - type: "options", - default: MouseMapTo[2], - options: { - [MouseMapTo[2]]: t("right-stick"), - [MouseMapTo[1]]: t("left-stick"), - [MouseMapTo[0]]: t("off") - } - }, - sensitivity_y: { - label: t("horizontal-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - sensitivity_x: { - label: t("vertical-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - deadzone_counterweight: { - label: t("deadzone-counterweight"), - type: "number-stepper", - default: 20, - min: 1, - max: 50, - params: { - suffix: "%", - exactTicks: 10 - } - } - }; - static DEFAULT_PRESET = { - mapping: { - 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"], - 16: ["Backquote"], - 7: ["Mouse0"], - 6: ["Mouse2"], - 10: ["ShiftLeft"], - 11: ["KeyF"] - }, - mouse: { - map_to: MouseMapTo[2], - sensitivity_x: 100, - sensitivity_y: 100, - deadzone_counterweight: 20 - } - }; - static convert(preset) { - const obj = { - mapping: {}, - mouse: Object.assign({}, preset.mouse) - }; - for (let buttonIndex in preset.mapping) - for (let keyName of preset.mapping[parseInt(buttonIndex)]) - obj.mapping[keyName] = parseInt(buttonIndex); - const mouse = obj.mouse; - mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - const mouseMapTo = MouseMapTo[mouse["map_to"]]; - if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; - else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; - return console.log(obj), obj; + static MOUSE_SETTINGS = { + map_to: { + label: t("map-mouse-to"), + type: "options", + default: MouseMapTo[2], + options: { + [MouseMapTo[2]]: t("right-stick"), + [MouseMapTo[1]]: t("left-stick"), + [MouseMapTo[0]]: t("off") + } + }, + sensitivity_y: { + label: t("horizontal-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + sensitivity_x: { + label: t("vertical-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + deadzone_counterweight: { + label: t("deadzone-counterweight"), + type: "number-stepper", + default: 20, + min: 1, + max: 50, + params: { + suffix: "%", + exactTicks: 10 + } } + }; + static DEFAULT_PRESET = { + mapping: { + 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"], + 16: ["Backquote"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + map_to: MouseMapTo[2], + sensitivity_x: 100, + sensitivity_y: 100, + deadzone_counterweight: 20 + } + }; + static convert(preset) { + const obj = { + mapping: {}, + mouse: Object.assign({}, preset.mouse) + }; + for (let buttonIndex in preset.mapping) + for (let keyName of preset.mapping[parseInt(buttonIndex)]) + obj.mapping[keyName] = parseInt(buttonIndex); + const mouse = obj.mouse; + mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; + const mouseMapTo = MouseMapTo[mouse["map_to"]]; + if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; + else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; + return console.log(obj), obj; + } } class LocalDb { - static #instance; - static get INSTANCE() { - if (!LocalDb.#instance) LocalDb.#instance = new LocalDb; - return LocalDb.#instance; - } - static DB_NAME = "BetterXcloud"; - static DB_VERSION = 1; - static TABLE_PRESETS = "mkb_presets"; - #DB; - #open() { - return new Promise((resolve, reject) => { - if (this.#DB) { - resolve(); - return; - } - const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); - request.onupgradeneeded = (e) => { - const db = e.target.result; - switch (e.oldVersion) { - case 0: { - db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name"); - break; - } - } - }, request.onerror = (e) => { - console.log(e), alert(e.target.error.message), reject && reject(); - }, request.onsuccess = (e) => { - this.#DB = e.target.result, resolve(); - }; + static #instance; + static get INSTANCE() { + if (!LocalDb.#instance) LocalDb.#instance = new LocalDb; + return LocalDb.#instance; + } + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 1; + static TABLE_PRESETS = "mkb_presets"; + #DB; + #open() { + return new Promise((resolve, reject) => { + if (this.#DB) { + resolve(); + return; + } + const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + const db = e.target.result; + switch (e.oldVersion) { + case 0: { + db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name"); + break; + } + } + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.#DB = e.target.result, resolve(); + }; + }); + } + #table(name, type) { + const table = this.#DB.transaction(name, type || "readonly").objectStore(name); + return new Promise((resolve) => resolve(table)); + } + #call(method) { + const table = arguments[1]; + return new Promise((resolve) => { + const request = method.call(table, ...Array.from(arguments).slice(2)); + request.onsuccess = (e) => { + resolve([table, e.target.result]); + }; + }); + } + #count(table) { + return this.#call(table.count, ...arguments); + } + #add(table, data) { + return this.#call(table.add, ...arguments); + } + #put(table, data) { + return this.#call(table.put, ...arguments); + } + #delete(table, data) { + return this.#call(table.delete, ...arguments); + } + #get(table, id) { + return this.#call(table.get, ...arguments); + } + #getAll(table) { + return this.#call(table.getAll, ...arguments); + } + newPreset(name, data) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id]) => new Promise((resolve) => resolve(id))); + } + updatePreset(preset) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id]) => new Promise((resolve) => resolve(id))); + } + deletePreset(id) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id)).then(([table, id2]) => new Promise((resolve) => resolve(id2))); + } + getPreset(id) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id)).then(([table, preset]) => new Promise((resolve) => resolve(preset))); + } + getPresets() { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => { + if (count > 0) return new Promise((resolve) => { + this.#getAll(table).then(([table2, items]) => { + const presets = {}; + items.forEach((item) => presets[item.id] = item), resolve(presets); + }); }); - } - #table(name, type) { - const table = this.#DB.transaction(name, type || "readonly").objectStore(name); - return new Promise((resolve) => resolve(table)); - } - #call(method) { - const table = arguments[1]; - return new Promise((resolve) => { - const request = method.call(table, ...Array.from(arguments).slice(2)); - request.onsuccess = (e) => { - resolve([table, e.target.result]); - }; + const preset = { + name: t("default"), + data: MkbPreset.DEFAULT_PRESET + }; + return new Promise((resolve) => { + this.#add(table, preset).then(([table2, id]) => { + preset.id = id, setPref("mkb_default_preset_id", id), resolve({ [id]: preset }); }); - } - #count(table) { - return this.#call(table.count, ...arguments); - } - #add(table, data) { - return this.#call(table.add, ...arguments); - } - #put(table, data) { - return this.#call(table.put, ...arguments); - } - #delete(table, data) { - return this.#call(table.delete, ...arguments); - } - #get(table, id) { - return this.#call(table.get, ...arguments); - } - #getAll(table) { - return this.#call(table.getAll, ...arguments); - } - newPreset(name, data) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id]) => new Promise((resolve) => resolve(id))); - } - updatePreset(preset) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id]) => new Promise((resolve) => resolve(id))); - } - deletePreset(id) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id)).then(([table, id2]) => new Promise((resolve) => resolve(id2))); - } - getPreset(id) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id)).then(([table, preset]) => new Promise((resolve) => resolve(preset))); - } - getPresets() { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => { - if (count > 0) return new Promise((resolve) => { - this.#getAll(table).then(([table2, items]) => { - const presets = {}; - items.forEach((item) => presets[item.id] = item), resolve(presets); - }); - }); - const preset = { - name: t("default"), - data: MkbPreset.DEFAULT_PRESET - }; - return new Promise((resolve) => { - this.#add(table, preset).then(([table2, id]) => { - preset.id = id, setPref("mkb_default_preset_id", id), resolve({ [id]: preset }); - }); - }); - }); - } + }); + }); + } } class KeyHelper { - static #NON_PRINTABLE_KEYS = { - Backquote: "`", - 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, name; - if (e instanceof KeyboardEvent) code = e.code || e.key; - 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) name = KeyHelper.codeToKeyName(code); - return code ? { code, name } : null; - } - static codeToKeyName(code) { - return 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; - } + static #NON_PRINTABLE_KEYS = { + Backquote: "`", + 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, name; + if (e instanceof KeyboardEvent) code = e.code || e.key; + 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) name = KeyHelper.codeToKeyName(code); + return code ? { code, name } : null; + } + static codeToKeyName(code) { + return 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; + } } var LOG_TAG = "PointerClient"; class PointerClient { - static instance; - static getInstance() { - if (!PointerClient.instance) PointerClient.instance = new PointerClient; - return PointerClient.instance; - } - socket; - mkbHandler; - 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(LOG_TAG, "connected"); - }), this.socket.addEventListener("error", (event) => { - BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event); - }), this.socket.addEventListener("close", (event) => { - this.socket = null; - }), this.socket.addEventListener("message", (event) => { - const dataView = new DataView(event.data); - let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT; - switch (messageType) { - 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); - } - }); - } - onMove(dataView, offset) { - const x = dataView.getInt16(offset); - offset += Int16Array.BYTES_PER_ELEMENT; - const y = dataView.getInt16(offset); - this.mkbHandler?.handleMouseMove({ - movementX: x, - movementY: y - }); - } - onPress(messageType, dataView, offset) { - const button = dataView.getUint8(offset); - this.mkbHandler?.handleMouseClick({ - pointerButton: button, - pressed: messageType === 2 - }); - } - onScroll(dataView, offset) { - const vScroll = dataView.getInt16(offset); - offset += Int16Array.BYTES_PER_ELEMENT; - const 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; - } + static instance; + static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient); + socket; + mkbHandler; + 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(LOG_TAG, "connected"); + }), this.socket.addEventListener("error", (event) => { + BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event); + }), this.socket.addEventListener("close", (event) => { + this.socket = null; + }), this.socket.addEventListener("message", (event) => { + const dataView = new DataView(event.data); + let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT; + switch (messageType) { + 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); + } + }); + } + onMove(dataView, offset) { + const x = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const y = dataView.getInt16(offset); + this.mkbHandler?.handleMouseMove({ + movementX: x, + movementY: y + }); + } + onPress(messageType, dataView, offset) { + const button = dataView.getUint8(offset); + this.mkbHandler?.handleMouseClick({ + pointerButton: button, + pressed: messageType === 2 + }); + } + onScroll(dataView, offset) { + const vScroll = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const 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; - } + mkbHandler; + constructor(handler) { + this.mkbHandler = handler; + } } class MkbHandler {} var PointerToMouseButton = { - 1: 0, - 2: 2, - 4: 1 + 1: 0, + 2: 2, + 4: 1 }, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; - init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; - try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; - } catch (e) { - Toast.show("Cannot enable Mouse & Keyboard feature"); - } - } - start() { - this.#connected && AppInterface.requestPointerCapture(); - } - stop() { - this.#connected && AppInterface.releasePointerCapture(); - } - destroy() { - this.#connected && this.#pointerClient?.stop(); + #pointerClient; + #connected = !1; + init() { + this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + try { + this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); } + } + start() { + this.#connected && AppInterface.requestPointerCapture(); + } + stop() { + this.#connected && AppInterface.releasePointerCapture(); + } + destroy() { + this.#connected && this.#pointerClient?.stop(); + } } class PointerLockMouseDataProvider extends MouseDataProvider { - init() {} - 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); - } - destroy() {} - #onMouseMoveEvent = (e) => { - this.mkbHandler.handleMouseMove({ - movementX: e.movementX, - movementY: e.movementY - }); + init() {} + 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); + } + destroy() {} + #onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); + }; + #onMouseEvent = (e) => { + e.preventDefault(); + const isMouseDown = e.type === "mousedown", data = { + mouseButton: e.button, + pressed: isMouseDown }; - #onMouseEvent = (e) => { - e.preventDefault(); - const isMouseDown = e.type === "mousedown", data = { - mouseButton: e.button, - pressed: isMouseDown - }; - this.mkbHandler.handleMouseClick(data); + this.mkbHandler.handleMouseClick(data); + }; + #onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + const data = { + vertical: e.deltaY, + horizontal: e.deltaX }; - #onWheelEvent = (e) => { - if (!KeyHelper.getKeyFromEvent(e)) return; - const data = { - vertical: e.deltaY, - horizontal: e.deltaX - }; - if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); - }; - #disableContextMenu = (e) => e.preventDefault(); + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + #disableContextMenu = (e) => e.preventDefault(); } class EmulatedMkbHandler extends MkbHandler { - static #instance; - static getInstance() { - if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; - return EmulatedMkbHandler.#instance; + static instance; + static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler); + #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static DEFAULT_PANNING_SENSITIVITY = 0.001; + static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; + static MAXIMUM_STICK_RANGE = 1.1; + #VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 3, + 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 = window.navigator.getGamepads.bind(window.navigator); + #enabled = !1; + #mouseDataProvider; + #isPolling = !1; + #prevWheelCode = null; + #wheelStoppedTimeout; + #detectMouseStoppedTimeout; + #$message; + #escKeyDownTime = -1; + #STICK_MAP; + #LEFT_STICK_X = []; + #LEFT_STICK_Y = []; + #RIGHT_STICK_X = []; + #RIGHT_STICK_Y = []; + constructor() { + super(); + this.#STICK_MAP = { + 102: [this.#LEFT_STICK_X, 0, -1], + 103: [this.#LEFT_STICK_X, 0, 1], + 100: [this.#LEFT_STICK_Y, 1, -1], + 101: [this.#LEFT_STICK_Y, 1, 1], + 202: [this.#RIGHT_STICK_X, 2, -1], + 203: [this.#RIGHT_STICK_X, 2, 1], + 200: [this.#RIGHT_STICK_Y, 3, -1], + 201: [this.#RIGHT_STICK_Y, 3, 1] + }; + } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { + const gamepads = this.#nativeGetGamepads() || []; + return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + }; + #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + #updateStick(stick, x, y) { + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); + } + #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + #resetGamepad = () => { + const gamepad = this.#getVirtualGamepad(); + gamepad.axes = [0, 0, 0, 0]; + for (let button of gamepad.buttons) + button.pressed = !1, button.value = 0; + gamepad.timestamp = performance.now(); + }; + #pressButton = (buttonIndex, pressed) => { + const virtualGamepad = this.#getVirtualGamepad(); + if (buttonIndex >= 100) { + let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + valueArr = valueArr, axisIndex = axisIndex; + 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]][2]; + else value = 0; + virtualGamepad.axes[axisIndex] = value; + } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; + virtualGamepad.timestamp = performance.now(); + }; + #onKeyboardEvent = (e) => { + const isKeyDown = e.type === "keydown"; + if (e.code === "F8") { + if (!isKeyDown) e.preventDefault(), this.toggle(); + return; } - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - #VIRTUAL_GAMEPAD = { - id: VIRTUAL_GAMEPAD_ID, - index: 3, - 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 = window.navigator.getGamepads.bind(window.navigator); - #enabled = !1; - #mouseDataProvider; - #isPolling = !1; - #prevWheelCode = null; - #wheelStoppedTimeout; - #detectMouseStoppedTimeout; - #$message; - #escKeyDownTime = -1; - #STICK_MAP; - #LEFT_STICK_X = []; - #LEFT_STICK_Y = []; - #RIGHT_STICK_X = []; - #RIGHT_STICK_Y = []; - constructor() { - super(); - this.#STICK_MAP = { - 102: [this.#LEFT_STICK_X, 0, -1], - 103: [this.#LEFT_STICK_X, 0, 1], - 100: [this.#LEFT_STICK_Y, 1, -1], - 101: [this.#LEFT_STICK_Y, 1, 1], - 202: [this.#RIGHT_STICK_X, 2, -1], - 203: [this.#RIGHT_STICK_X, 2, 1], - 200: [this.#RIGHT_STICK_Y, 3, -1], - 201: [this.#RIGHT_STICK_Y, 3, 1] - }; + 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; } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - const gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + if (!this.#isPolling) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + }; + #onMouseStopped = () => { + this.#detectMouseStoppedTimeout = null; + const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 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]; + const keyCode = "Mouse" + mouseButton, key = { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode) }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); - } - #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - const gamepad = this.#getVirtualGamepad(); - gamepad.axes = [0, 0, 0, 0]; - for (let button of gamepad.buttons) - button.pressed = !1, button.value = 0; - gamepad.timestamp = performance.now(); - }; - #pressButton = (buttonIndex, pressed) => { - const virtualGamepad = this.#getVirtualGamepad(); - if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; - valueArr = valueArr, axisIndex = axisIndex; - 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]][2]; - else value = 0; - virtualGamepad.axes[axisIndex] = value; - } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; - virtualGamepad.timestamp = performance.now(); - }; - #onKeyboardEvent = (e) => { - const isKeyDown = e.type === "keydown"; - if (e.code === "F8") { - if (!isKeyDown) e.preventDefault(), this.toggle(); - return; + if (!key.name) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.#pressButton(buttonIndex, data.pressed); + }; + handleMouseMove = (data) => { + const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + if (mouseMapTo === 0) return; + this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + const 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; + const key = { + code, + name: KeyHelper.codeToKeyName(code) + }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); + return this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + }, 20), !0; + }; + toggle = (force) => { + if (typeof force !== "undefined") this.#enabled = force; + else this.#enabled = !this.#enabled; + if (this.#enabled) document.body.requestPointerLock(); + else document.pointerLockElement && document.exitPointerLock(); + }; + #getCurrentPreset = () => { + return new Promise((resolve) => { + const presetId = getPref("mkb_default_preset_id"); + LocalDb.INSTANCE.getPreset(presetId).then((preset) => { + resolve(preset); + }); + }); + }; + refreshPresetData = () => { + this.#getCurrentPreset().then((preset) => { + this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); + }); + }; + waitForMouseData = (wait) => { + this.#$message && this.#$message.classList.toggle("bx-gone", !wait); + }; + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage = () => { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ + style: 1 | 256 | 64, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), CE("div", {}, createButton({ + label: t("ignore"), + style: 4, + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); } - 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) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; - if (typeof buttonIndex === "undefined") return; - if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); - }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 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]; - const keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) - }; - if (!key.name) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; - if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; - let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); - if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; - const 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; - const key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return !1; - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); - return this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); - }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); - else document.pointerLockElement && document.exitPointerLock(); - }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - const presetId = getPref("mkb_default_preset_id"); - LocalDb.INSTANCE.getPreset(presetId).then((preset) => { - resolve(preset); - }); - }); - }; - refreshPresetData = () => { - this.#getCurrentPreset().then((preset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); - }); - }; - waitForMouseData = (wait) => { - this.#$message && this.#$message.classList.toggle("bx-gone", !wait); - }; - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { - document.pointerLockElement && document.exitPointerLock(); - }; - #initMessage = () => { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ - style: 1 | 256 | 64, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), CE("div", {}, createButton({ - label: t("ignore"), - style: 4, - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); - } - }), createButton({ - label: t("edit"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(); - const dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #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; + }), createButton({ + label: t("edit"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(); + const dialog = SettingsNavigationDialog.getInstance(); + dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); } + })))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + }; + #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 (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), window.addEventListener(BxEvent.XCLOUD_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 (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); - else this.waitForMouseData(!0); - }; - destroy = () => { - if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && 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), window.removeEventListener(BxEvent.XCLOUD_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, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); - const 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; - const virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), 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() {} + } + init = () => { + if (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), window.addEventListener(BxEvent.XCLOUD_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 (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + else this.waitForMouseData(!0); + }; + destroy = () => { + if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && 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), window.removeEventListener(BxEvent.XCLOUD_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, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); + const 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; + const virtualGamepad = this.#getVirtualGamepad(); + if (virtualGamepad.connected) this.#resetGamepad(), 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() {} } class NavigationDialog { - dialogManager; - constructor() { - this.dialogManager = NavigationDialogManager.getInstance(); - } - show() { - if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); - } - hide() { - NavigationDialogManager.getInstance().hide(); - } - getFocusedElement() { - const $activeElement = document.activeElement; - if (!$activeElement) return null; - if (this.$container.contains($activeElement)) return $activeElement; - return null; - } - onBeforeMount() {} - onMounted() {} - onBeforeUnmount() {} - onUnmounted() {} - handleKeyPress(key) { - return !1; - } - handleGamepad(button) { - return !1; - } + dialogManager; + constructor() { + this.dialogManager = NavigationDialogManager.getInstance(); + } + show() { + if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); + } + hide() { + NavigationDialogManager.getInstance().hide(); + } + getFocusedElement() { + const $activeElement = document.activeElement; + if (!$activeElement) return null; + if (this.$container.contains($activeElement)) return $activeElement; + return null; + } + onBeforeMount() {} + onMounted() {} + onBeforeUnmount() {} + onUnmounted() {} + handleKeyPress(key) { + return !1; + } + handleGamepad(button) { + return !1; + } } class NavigationDialogManager { - static instance; - static getInstance() { - if (!NavigationDialogManager.instance) NavigationDialogManager.instance = new NavigationDialogManager; - return NavigationDialogManager.instance; + static instance; + static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager); + static GAMEPAD_POLLING_INTERVAL = 50; + static GAMEPAD_KEYS = [ + 12, + 13, + 14, + 15, + 0, + 1, + 4, + 5, + 6, + 7 + ]; + 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" } - static GAMEPAD_POLLING_INTERVAL = 50; - static GAMEPAD_KEYS = [ - 12, - 13, - 14, - 15, - 0, - 1, - 4, - 5, - 6, - 7 - ]; - 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; + constructor() { + if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + e.preventDefault(), e.stopPropagation(), 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()), getPref("ui_controller_friendly")) + new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + const $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + this.calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); + } + calculateSelectBoxes($root) { + $root.querySelectorAll(".bx-select:not([data-calculated]) select").forEach(($select) => { + const $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + return; + } + const rect = $select.getBoundingClientRect(); + let $label, width = Math.ceil(rect.width); + if (!width) return; + if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; + else $label = $parent.querySelector("div"); + $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + }); + } + handleEvent(event) { + switch (event.type) { + case "keydown": + const $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key; + let handled = this.dialog?.handleKeyPress(keyCode); + if (handled) { + event.preventDefault(), event.stopPropagation(); + return; } - }; - gamepadPollingIntervalId = null; - gamepadLastStates = []; - gamepadHoldingIntervalId = null; - $overlay; - $container; - dialog = null; - constructor() { - if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { - e.preventDefault(), e.stopPropagation(), 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()), getPref("ui_controller_friendly")) - new MutationObserver((mutationList) => { - if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; - const $dialog = mutationList[0].addedNodes[0]; - if (!$dialog || !($dialog instanceof HTMLElement)) return; - this.calculateSelectBoxes($dialog); - }).observe(this.$container, { childList: !0 }); + 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")); + } else if (keyCode === "Escape") handled = !0, this.hide(); + if (handled) event.preventDefault(), event.stopPropagation(); + break; } - calculateSelectBoxes($root) { - $root.querySelectorAll(".bx-select:not([data-calculated]) select").forEach(($select) => { - const $parent = $select.parentElement; - if ($parent.classList.contains("bx-full-width")) { - $parent.dataset.calculated = "true"; + } + isShowing() { + return this.$container && !this.$container.classList.contains("bx-gone"); + } + pollGamepad() { + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad || !gamepad.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + const { axes, buttons } = gamepad; + let 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) { + const 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(() => { + const lastState2 = this.gamepadLastStates[gamepad.index]; + if (lastState2) { + if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { + this.handleGamepad(gamepad, heldButton); return; + } } - const rect = $select.getBoundingClientRect(); - let $label, width = Math.ceil(rect.width); - if (!width) return; - if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; - else $label = $parent.querySelector("div"); - $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; - }); + this.clearGamepadHoldingInterval(); + }, 200); + continue; + } + if (releasedButton === null) { + this.clearGamepadHoldingInterval(); + continue; + } + if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (releasedButton === 0) { + document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click")); + return; + } else if (releasedButton === 1) { + this.hide(); + return; + } + if (this.handleGamepad(gamepad, releasedButton)) return; } - handleEvent(event) { - switch (event.type) { - case "keydown": - const $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key; - let 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")); - } else if (keyCode === "Escape") handled = !0, this.hide(); - if (handled) event.preventDefault(), event.stopPropagation(); - break; - } + } + 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") { + const $range = document.activeElement; + if (direction === 4 || direction === 2) $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")), handled = !0; } - isShowing() { - return this.$container && !this.$container.classList.contains("bx-gone"); + 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) { + if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); + this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); + } + hide() { + this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_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.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; + } + 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) { + const nearby = $elm.nearby || {}; + if (nearby.selfOrientation) return nearby.selfOrientation; + let orientation, $current = $elm.parentElement; + while ($current !== this.$container) { + const tmp = $current.nearby?.orientation; + if ($current.nearby && tmp) { + orientation = tmp; + break; + } + $current = $current.parentElement; } - pollGamepad() { - const gamepads = window.navigator.getGamepads(); - for (let gamepad of gamepads) { - if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - const { axes, buttons } = gamepad; - let 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) { - const 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(() => { - const lastState2 = this.gamepadLastStates[gamepad.index]; - if (lastState2) { - if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { - this.handleGamepad(gamepad, heldButton); - return; - } - } - this.clearGamepadHoldingInterval(); - }, 200); - continue; - } - if (releasedButton === null) { - this.clearGamepadHoldingInterval(); - continue; - } - if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; - if (releasedButton === 0) { - document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click")); - return; - } else if (releasedButton === 1) { - this.hide(); - return; - } - if (this.handleGamepad(gamepad, releasedButton)) return; - } + 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; + const $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + if (siblingProperty) { + let $sibling = $target; + while ($sibling[siblingProperty]) { + $sibling = $sibling[siblingProperty]; + const $focusable = this.findFocusableElement($sibling, direction); + if ($focusable) return $focusable; + } } - 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") { - const $range = document.activeElement; - if (direction === 4 || direction === 2) $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; + if (nearby.loop) { + if (nearby.loop(direction)) return null; } - clearGamepadHoldingInterval() { - this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = 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; + const 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; + } } - show(dialog) { - if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); - this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); - } - hide() { - this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_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.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; - } - 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) { - const nearby = $elm.nearby || {}; - if (nearby.selfOrientation) return nearby.selfOrientation; - let orientation, $current = $elm.parentElement; - while ($current !== this.$container) { - const 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; - const $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); - let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; - if (siblingProperty) { - let $sibling = $target; - while ($sibling[siblingProperty]) { - $sibling = $sibling[siblingProperty]; - const $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; - const 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; - } - } - const 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; - const $target = this.findFocusableElement($child, direction); - if ($target) return $target; - } - return null; - } - startGamepadPolling() { - this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); - } - stopGamepadPolling() { - this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; - } - focusDirection(direction) { - const dialog = this.dialog; - if (!dialog) return; - const $focusing = dialog.getFocusedElement(); - if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; - const $target = this.findNextTarget($focusing, direction, !0); - this.focus($target); - } - unmountCurrentDialog() { - const dialog = this.dialog; - dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + const 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; + const $target = this.findFocusableElement($child, direction); + if ($target) return $target; } + return null; + } + startGamepadPolling() { + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + } + stopGamepadPolling() { + this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; + } + focusDirection(direction) { + const dialog = this.dialog; + if (!dialog) return; + const $focusing = dialog.getFocusedElement(); + if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; + const $target = this.findNextTarget($focusing, direction, !0); + this.focus($target); + } + unmountCurrentDialog() { + const dialog = this.dialog; + dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + } } var BxIcon = { - BETTER_XCLOUD: "", - TRUE_ACHIEVEMENTS: "", - STREAM_SETTINGS: "", - STREAM_STATS: "", - CLOSE: "", - COMMAND: "", - CONTROLLER: "", - CREATE_SHORTCUT: "", - DISPLAY: "", - EYE: "", - EYE_SLASH: "", - HOME: "", - NATIVE_MKB: "", - NEW: "", - COPY: "", - TRASH: "", - CURSOR_TEXT: "", - POWER: "", - QUESTION: "", - REFRESH: "", - VIRTUAL_CONTROLLER: "", - REMOTE_PLAY: "", - CARET_LEFT: "", - CARET_RIGHT: "", - SCREENSHOT: "", - SPEAKER_MUTED: "", - TOUCH_CONTROL_ENABLE: "", - TOUCH_CONTROL_DISABLE: "", - MICROPHONE: "", - MICROPHONE_MUTED: "", - BATTERY: "", - PLAYTIME: "", - SERVER: "", - DOWNLOAD: "", - UPLOAD: "", - AUDIO: "" + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + COMMAND: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + VIRTUAL_CONTROLLER: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" }; var VIBRATION_DATA_MAP = { - gamepadIndex: 8, - leftMotorPercent: 8, - rightMotorPercent: 8, - leftTriggerMotorPercent: 8, - rightTriggerMotorPercent: 8, - durationMs: 16 + gamepadIndex: 8, + leftMotorPercent: 8, + rightMotorPercent: 8, + leftTriggerMotorPercent: 8, + rightTriggerMotorPercent: 8, + durationMs: 16 }; class VibrationManager { - static #playDeviceVibration(data) { - if (AppInterface) { - AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); - return; + static #playDeviceVibration(data) { + if (AppInterface) { + AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); + return; + } + const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; + if (intensity === 0 || intensity === 100) { + window.navigator.vibrate(intensity ? data.durationMs : 0); + return; + } + const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); + window.navigator.vibrate(pulses); + } + static supportControllerVibration() { + return Gamepad.prototype.hasOwnProperty("vibrationActuator"); + } + static supportDeviceVibration() { + return !!window.navigator.vibrate; + } + static updateGlobalVars(stopVibration = !0) { + if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { + window.BX_ENABLE_DEVICE_VIBRATION = !1; + return; + } + stopVibration && window.navigator.vibrate(0); + const value = getPref("controller_device_vibration"); + let enabled; + if (value === "on") enabled = !0; + else if (value === "auto") { + enabled = !0; + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad) { + enabled = !1; + break; } - const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; - if (intensity === 0 || intensity === 100) { - window.navigator.vibrate(intensity ? data.durationMs : 0); - return; - } - const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); - window.navigator.vibrate(pulses); - } - static supportControllerVibration() { - return Gamepad.prototype.hasOwnProperty("vibrationActuator"); - } - static supportDeviceVibration() { - return !!window.navigator.vibrate; - } - static updateGlobalVars(stopVibration = !0) { - if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { - window.BX_ENABLE_DEVICE_VIBRATION = !1; - return; - } - stopVibration && window.navigator.vibrate(0); - const value = getPref("controller_device_vibration"); - let enabled; - if (value === "on") enabled = !0; - else if (value === "auto") { - enabled = !0; - const gamepads = window.navigator.getGamepads(); - for (let gamepad of gamepads) - if (gamepad) { - enabled = !1; - break; - } - } else enabled = !1; - window.BX_ENABLE_DEVICE_VIBRATION = enabled; - } - static #onMessage(e) { - if (!window.BX_ENABLE_DEVICE_VIBRATION) return; - if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; - const dataView = new DataView(e.data); - let 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; - const vibrationType = dataView.getUint8(offset); - if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; - const data = {}; - let 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; - VibrationManager.#playDeviceVibration(data); - } - static initialSetup() { - window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "input") return; - dataChannel.addEventListener("message", VibrationManager.#onMessage); - }); - } + } else enabled = !1; + window.BX_ENABLE_DEVICE_VIBRATION = enabled; + } + static #onMessage(e) { + if (!window.BX_ENABLE_DEVICE_VIBRATION) return; + if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; + const dataView = new DataView(e.data); + let 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; + const vibrationType = dataView.getUint8(offset); + if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; + const data = {}; + let 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; + VibrationManager.#playDeviceVibration(data); + } + static initialSetup() { + window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "input") return; + dataChannel.addEventListener("message", VibrationManager.#onMessage); + }); + } } var FeatureGates = { - PwaPrompt: !1, - EnableWifiWarnings: !1, - EnableUpdateRequiredPage: !1, - ShowForcedUpdateScreen: !1 + PwaPrompt: !1, + EnableWifiWarnings: !1, + EnableUpdateRequiredPage: !1, + ShowForcedUpdateScreen: !1 }; if (getPref("ui_home_context_menu_disabled")) FeatureGates.EnableHomeContextMenu = !1; if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); class FullscreenText { - static instance; - static getInstance() { - if (!FullscreenText.instance) FullscreenText.instance = new FullscreenText; - return FullscreenText.instance; - } - $text; - 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"); - } + static instance; + static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); + $text; + 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 SettingsNavigationDialog extends NavigationDialog { - static instance; - static getInstance() { - if (!SettingsNavigationDialog.instance) SettingsNavigationDialog.instance = new SettingsNavigationDialog; - return SettingsNavigationDialog.instance; - } - $container; - $tabs; - $settings; - $btnReload; - $btnGlobalReload; - $noteGlobalReload; - $btnSuggestion; - renderFullSettings; - suggestedSettings = { - recommended: {}, - default: {}, - lowest: {}, - highest: {} - }; - suggestedSettingLabels = {}; - settingElements = {}; - TAB_GLOBAL_ITEMS = [{ - group: "general", - label: t("better-xcloud"), - helpUrl: "https://better-xcloud.github.io/features/", - items: [ - ($parent) => { - const PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; - if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { - const opts = { - label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), - style: 1 | 32 | 64 - }; - 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: 64 | 32, - onClick: (e) => { - AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); - } - })); - else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ - label: "🔥 " + t("install-android"), - style: 64 | 32, - url: "https://better-xcloud.github.io/android" - })); - this.$btnGlobalReload = createButton({ - label: t("settings-reload"), - classes: ["bx-settings-reload-button", "bx-gone"], - style: 32 | 64, - 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", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); - const $div = CE("div", { - class: "bx-top-buttons", - _nearby: { - orientation: "vertical" - } - }, ...topButtons); - $parent.appendChild($div); - }, - "bx_locale", - "server_bypass_restriction", - "ui_controller_friendly", - "xhome_enabled" - ] - }, { - group: "server", - label: t("server"), - items: [ - "server_region", - "stream_preferred_locale", - "prefer_ipv6_server" - ] - }, { - group: "stream", - label: t("stream"), - items: [ - "stream_target_resolution", - "stream_codec_profile", - "bitrate_video_max", - "audio_enable_volume_control", - "stream_disable_feedback_dialog", - "screenshot_apply_filters", - "audio_mic_on_playing", - "game_fortnite_force_console", - "stream_combine_sources" - ] - }, { - requiredVariants: "full", - group: "co-op", - label: t("local-co-op"), - items: [ - "local_co_op_enabled" - ] - }, { - requiredVariants: "full", - group: "mkb", - label: t("mouse-and-keyboard"), - unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { - href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", - target: "_blank" - }, "⚠️ " + t("browser-unsupported-feature")) : null, - unsupported: !STATES.userAgent.capabilities.mkb, - items: [ - "native_mkb_enabled", - "game_msfs2020_force_native_mkb", - "mkb_enabled", - "mkb_hide_idle_cursor" - ] - }, { - requiredVariants: "full", - group: "touch-control", - label: t("touch-controller"), - unsupported: !STATES.userAgent.capabilities.touch, - unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, - items: [ - "stream_touch_controller", - "stream_touch_controller_auto_off", - "stream_touch_controller_default_opacity", - "stream_touch_controller_style_standard", - "stream_touch_controller_style_custom" - ] - }, { - group: "ui", - label: t("ui"), - items: [ - "ui_layout", - "ui_game_card_show_wait_time", - "ui_home_context_menu_disabled", - "controller_show_connection_status", - "stream_simplify_menu", - "skip_splash_video", - !AppInterface && "ui_scrollbar_hide", - "hide_dots_icon", - "reduce_animations", - "block_social_features", - "ui_hide_sections" - ] - }, { - requiredVariants: "full", - group: "game-bar", - label: t("game-bar"), - items: [ - "game_bar_position" - ] - }, { - group: "loading-screen", - label: t("loading-screen"), - items: [ - "ui_loading_screen_game_art", - "ui_loading_screen_wait_time", - "ui_loading_screen_rocket" - ] - }, { - group: "other", - label: t("other"), - items: [ - "block_tracking" - ] - }, { - group: "advanced", - label: t("advanced"), - items: [ - { - pref: "user_agent_profile", - onCreated: (setting, $control) => { - const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { - id: `bx_setting_inp_${setting.pref}`, - type: "text", - placeholder: defaultUserAgent, - autocomplete: "off", - class: "bx-settings-custom-user-agent", - tabindex: 0 - }); - $inpCustomUserAgent.addEventListener("input", (e) => { - const 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) => { - $parent.appendChild(CE("a", { - class: "bx-donation-link", - href: "https://ko-fi.com/redphx", - target: "_blank", - tabindex: 0 - }, `❤️ ${t("support-better-xcloud")}`)); - }, - ($parent) => { - try { - const 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) => { - const debugInfo = deepClone(BX_FLAGS.DeviceInfo); - debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); - const $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ - label: "Debug info", - style: 4 | 64 | 32, - onClick: (e) => { - const $pre = e.target.closest("button")?.nextElementSibling; - $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); - } - }), CE("pre", { - class: "bx-focusable bx-gone", - tabindex: 0, - on: { - click: async (e) => { - await copyToClipboard(e.target.innerText); - } - } - }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); - $parent.appendChild($debugInfo); - } - ] - }]; - TAB_DISPLAY_ITEMS = [{ - requiredVariants: "full", - group: "audio", - label: t("audio"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", - items: [{ - pref: "audio_volume", - onChange: (e, value) => { - SoundShortcut.setGainNodeVolume(value); - }, - params: { - disabled: !getPref("audio_enable_volume_control") - }, - onCreated: (setting, $elm) => { - const $range = $elm.querySelector("input[type=range"); - window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { - const { storageKey, settingKey, settingValue } = e; - if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; - $range.value = settingValue, BxEvent.dispatch($range, "input", { - ignoreOnChange: !0 - }); - }); - } - }] - }, { - group: "video", - label: t("video"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#video", - items: [{ - pref: "video_player_type", - onChange: onChangeVideoPlayerType - }, { - pref: "video_max_fps", - onChange: limitVideoPlayerFps - }, { - pref: "video_power_preference", - onChange: () => { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - streamPlayer.reloadPlayer(), updateVideoPlayer(); - } - }, { - pref: "video_processing", - onChange: updateVideoPlayer - }, { - pref: "video_ratio", - onChange: updateVideoPlayer - }, { - pref: "video_sharpness", - onChange: updateVideoPlayer - }, { - pref: "video_saturation", - onChange: updateVideoPlayer - }, { - pref: "video_contrast", - onChange: updateVideoPlayer - }, { - pref: "video_brightness", - onChange: updateVideoPlayer - }] - }]; - TAB_CONTROLLER_ITEMS = [ - { - group: "controller", - label: t("controller"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", - items: [{ - pref: "controller_enable_vibration", - unsupported: !VibrationManager.supportControllerVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, { - pref: "controller_device_vibration", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { - pref: "controller_vibration_intensity", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }] - }, - !1 - ]; - TAB_VIRTUAL_CONTROLLER_ITEMS = [{ - group: "mkb", - label: t("virtual-controller"), - helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: !1 - }]; - TAB_NATIVE_MKB_ITEMS = [{ - requiredVariants: "full", - group: "native-mkb", - label: t("native-mkb"), - items: [] - }]; - TAB_SHORTCUTS_ITEMS = [{ - requiredVariants: "full", - group: "controller-shortcuts", - label: t("controller-shortcuts"), - content: !1 - }]; - TAB_STATS_ITEMS = [{ - group: "stats", - label: t("stream-stats"), - helpUrl: "https://better-xcloud.github.io/stream-stats/", - items: [ - { - pref: "stats_show_when_playing" - }, - { - pref: "stats_quick_glance", - onChange: (e) => { - const streamStats = StreamStats.getInstance(); - e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); - } - }, - { - pref: "stats_items", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_position", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_text_size", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_opacity", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_transparent", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_conditional_formatting", - onChange: StreamStats.refreshStyles - } - ] - }]; - SETTINGS_UI = [ - { - icon: BxIcon.HOME, - group: "global", - items: this.TAB_GLOBAL_ITEMS - }, - { - icon: BxIcon.DISPLAY, - group: "stream", - items: this.TAB_DISPLAY_ITEMS - }, - { - icon: BxIcon.CONTROLLER, - group: "controller", - items: this.TAB_CONTROLLER_ITEMS, - requiredVariants: "full" - }, - !1, - !1, - { - icon: BxIcon.COMMAND, - group: "shortcuts", - items: this.TAB_SHORTCUTS_ITEMS, - requiredVariants: "full" - }, - { - icon: BxIcon.STREAM_STATS, - group: "stats", - items: this.TAB_STATS_ITEMS + static instance; + static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog); + $container; + $tabs; + $settings; + $btnReload; + $btnGlobalReload; + $noteGlobalReload; + $btnSuggestion; + renderFullSettings; + suggestedSettings = { + recommended: {}, + default: {}, + lowest: {}, + highest: {} + }; + suggestedSettingLabels = {}; + settingElements = {}; + TAB_GLOBAL_ITEMS = [{ + group: "general", + label: t("better-xcloud"), + helpUrl: "https://better-xcloud.github.io/features/", + items: [ + ($parent) => { + const PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { + const opts = { + label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), + style: 1 | 32 | 64 + }; + if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); + else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; + topButtons.push(createButton(opts)); } - ]; - constructor() { - super(); - this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); - } - getDialog() { - return this; - } - getContent() { - return this.$container; - } - onMounted() { - if (!this.renderFullSettings) return; - if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - const $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); - if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; - } - reloadPage() { - this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); - } - async getRecommendedSettings(deviceCode) { - try { - const json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {}; - if (json.schema_version !== 1) return null; - const 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; - } - addDefaultSuggestedSetting(prefKey, value) { - let key; - for (key in this.suggestedSettings) - if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; - } - 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] = getPrefDefinition(prefKey).default; - } - } - isSupportedVariant(requiredVariants) { - if (typeof requiredVariants === "undefined") return !0; - return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); - } - async renderSuggestions(e) { - const $btnSuggest = e.target.closest("div"); - $btnSuggest.toggleAttribute("bx-open"); - let $content = $btnSuggest.nextElementSibling; - if ($content) { - BxEvent.dispatch($content.querySelector("select"), "input"); - return; - } - for (let settingTab of this.SETTINGS_UI) { - if (!settingTab || !settingTab.items) continue; - for (let settingTabContent of settingTab.items) { - if (!settingTabContent || !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.suggestedSettingLabels[prefKey] = settingTabContent.label; - } - } - } - let recommendedDevice = ""; - if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { - if (BX_FLAGS.DeviceInfo.androidInfo) { - const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board; - recommendedDevice = await this.getRecommendedSettings(deviceCode); - } - } - const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); - else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); - else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); - this.generateDefaultSuggestedSettings(); - const $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, 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) => { - const profile = $select.value; - removeChildElements($suggestedSettings); - const fragment = document.createDocumentFragment(); - let 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)); - const settings = this.suggestedSettings[profile]; - let prefKey; - for (prefKey in settings) { - const currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue; - let $child, $value; - if (isSameValue) $value = currentValueText; - else { - const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); - $value = currentValueText + " ➔ " + suggestedValueText; - } - let $checkbox; - const breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(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: `bx_suggest_${prefKey}` - }), CE("label", { - for: `bx_suggest_${prefKey}` - }, 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"); - const onClickApply = () => { - const profile = $select.value, settings = this.suggestedSettings[profile]; - let prefKey; - for (prefKey in settings) { - const suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); - if (!$checkBox.checked || $checkBox.disabled) continue; - const $control = this.settingElements[prefKey]; - if (!$control) { - setPref(prefKey, suggestedValue); - continue; - } - 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"), + if (AppInterface) topButtons.push(createButton({ + label: t("app-settings"), + icon: BxIcon.STREAM_SETTINGS, style: 64 | 32, - onClick: onClickApply - }); - $content = CE("div", { - class: "bx-suggest-box", - _nearby: { - orientation: "vertical" - } - }, BxSelectElement.wrap($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); - } - renderTab(settingTab) { - const $svg = createSvgIcon(settingTab.icon); - return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => { - for (let $child of Array.from(this.$settings.children)) - if ($child.getAttribute("data-tab-group") === settingTab.group) { - if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); - } else $child.classList.add("bx-gone"); - for (let $child of Array.from(this.$tabs.children)) - $child.classList.remove("bx-active"); - $svg.classList.add("bx-active"); - }), $svg; - } - onGlobalSettingChanged(e) { - this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); - } - renderServerSetting(setting) { - let selectedValue; - const $control = CE("select", { - id: `bx_setting_${setting.pref}`, - title: setting.label, - tabindex: 0 - }); - $control.name = $control.id, $control.addEventListener("input", (e) => { - setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e); - }), selectedValue = getPref("server_region"), setting.options = {}; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - let value = regionName, label = `${region.shortName} - ${regionName}`; - if (region.isDefault) { - if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; - } - setting.options[value] = label; - } - for (let value in setting.options) { - const label = setting.options[value], $option = CE("option", { value }, label); - $control.appendChild($option); - } - return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; - } - renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { - if (typeof setting === "string") setting = { - pref: setting - }; - const pref = setting.pref; - let $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, STORAGE.Global, async (e) => { - const newLocale = e.target.value; - if (getPref("ui_controller_friendly")) { - 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 === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { - const value = e.target.value; - let isCustom = value === "custom", userAgent2 = UserAgent.get(value); - UserAgent.updateStorage(value); - const $inp = $control.nextElementSibling; - $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); - }); - else { - let onChange = setting.onChange; - if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); - $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); - } - if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); - pref && (this.settingElements[pref] = $control); - } - let prefDefinition = null; - if (pref) prefDefinition = getPrefDefinition(pref); - if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; - let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote; - const experimental = prefDefinition?.experimental || setting.experimental; - 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 $label; - const $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${pref}`, - "data-type": settingTabContent.group, - _nearby: { - orientation: "horizontal" - } - }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); - if ($link) $link.classList.add("bx-focusable"), setNearby($label, { - focus: $link - }); - $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); - } - setupDialog() { - let $tabs, $settings; - const $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", {}, this.$btnReload = createButton({ - icon: BxIcon.REFRESH, - style: 32 | 16, onClick: (e) => { - this.reloadPage(); + AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); } - }), createButton({ - icon: BxIcon.CLOSE, - style: 32 | 16, - onClick: (e) => { - this.dialogManager.hide(); + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ + label: "🔥 " + t("install-android"), + style: 64 | 32, + url: "https://better-xcloud.github.io/android" + })); + this.$btnGlobalReload = createButton({ + label: t("settings-reload"), + classes: ["bx-settings-reload-button", "bx-gone"], + style: 32 | 64, + 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", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + const $div = CE("div", { + class: "bx-top-buttons", + _nearby: { + orientation: "vertical" + } + }, ...topButtons); + $parent.appendChild($div); + }, + "bx_locale", + "server_bypass_restriction", + "ui_controller_friendly", + "xhome_enabled" + ] + }, { + group: "server", + label: t("server"), + items: [ + "server_region", + "stream_preferred_locale", + "prefer_ipv6_server" + ] + }, { + group: "stream", + label: t("stream"), + items: [ + "stream_target_resolution", + "stream_codec_profile", + "bitrate_video_max", + "audio_enable_volume_control", + "stream_disable_feedback_dialog", + "screenshot_apply_filters", + "audio_mic_on_playing", + "game_fortnite_force_console", + "stream_combine_sources" + ] + }, { + requiredVariants: "full", + group: "co-op", + label: t("local-co-op"), + items: [ + "local_co_op_enabled" + ] + }, { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { + href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", + target: "_blank" + }, "⚠️ " + t("browser-unsupported-feature")) : null, + unsupported: !STATES.userAgent.capabilities.mkb, + items: [ + "native_mkb_enabled", + "game_msfs2020_force_native_mkb", + "mkb_enabled", + "mkb_hide_idle_cursor" + ] + }, { + requiredVariants: "full", + group: "touch-control", + label: t("touch-controller"), + unsupported: !STATES.userAgent.capabilities.touch, + unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, + items: [ + "stream_touch_controller", + "stream_touch_controller_auto_off", + "stream_touch_controller_default_opacity", + "stream_touch_controller_style_standard", + "stream_touch_controller_style_custom" + ] + }, { + group: "ui", + label: t("ui"), + items: [ + "ui_layout", + "ui_game_card_show_wait_time", + "ui_home_context_menu_disabled", + "controller_show_connection_status", + "stream_simplify_menu", + "skip_splash_video", + !AppInterface && "ui_scrollbar_hide", + "hide_dots_icon", + "reduce_animations", + "block_social_features", + "ui_hide_sections" + ] + }, { + requiredVariants: "full", + group: "game-bar", + label: t("game-bar"), + items: [ + "game_bar_position" + ] + }, { + group: "loading-screen", + label: t("loading-screen"), + items: [ + "ui_loading_screen_game_art", + "ui_loading_screen_wait_time", + "ui_loading_screen_rocket" + ] + }, { + group: "other", + label: t("other"), + items: [ + "block_tracking" + ] + }, { + group: "advanced", + label: t("advanced"), + items: [ + { + pref: "user_agent_profile", + onCreated: (setting, $control) => { + const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { + id: `bx_setting_inp_${setting.pref}`, + type: "text", + placeholder: defaultUserAgent, + autocomplete: "off", + class: "bx-settings-custom-user-agent", + tabindex: 0 + }); + $inpCustomUserAgent.addEventListener("input", (e) => { + const 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) => { + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + try { + const 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) => { + const debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); + const $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 4 | 64 | 32, + onClick: (e) => { + const $pre = e.target.closest("button")?.nextElementSibling; + $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); + } + }), CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + on: { + click: async (e) => { + await copyToClipboard(e.target.innerText); } - }))), $settings = CE("div", { - class: "bx-settings-tab-contents", - _nearby: { - orientation: "vertical", - focus: () => this.jumpToSettingGroup("next"), - loop: (direction) => { - if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; - return !1; - } - } - })); - this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => { - if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); + $parent.appendChild($debugInfo); + } + ] + }]; + TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", + group: "audio", + label: t("audio"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: "audio_volume", + onChange: (e, value) => { + SoundShortcut.setGainNodeVolume(value); + }, + params: { + disabled: !getPref("audio_enable_volume_control") + }, + onCreated: (setting, $elm) => { + const $range = $elm.querySelector("input[type=range"); + window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { + const { storageKey, settingKey, settingValue } = e; + if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; + $range.value = settingValue, BxEvent.dispatch($range, "input", { + ignoreOnChange: !0 + }); }); - for (let settingTab of this.SETTINGS_UI) { - if (!settingTab) continue; - if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; - if (settingTab.group !== "global" && !this.renderFullSettings) continue; - const $svg = this.renderTab(settingTab); - $tabs.appendChild($svg); - const $tabContent = CE("div", { - class: "bx-gone", - "data-tab-group": settingTab.group - }); - for (let settingTabContent of settingTab.items) { - if (settingTabContent === !1) continue; - if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; - if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; - let label = settingTabContent.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: 1024 | 8 | 32 - }); - } - if (label) { - const $title = CE("h2", { - _nearby: { - orientation: "horizontal" - } - }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: 4 | 32, - url: settingTabContent.helpUrl, - title: t("help") - })); - $tabContent.appendChild($title); - } - if (settingTabContent.unsupportedNote) { - const $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); - $tabContent.appendChild($note); - } - if (settingTabContent.unsupported) continue; - if (settingTabContent.content) { - $tabContent.appendChild(settingTabContent.content); - continue; - } - settingTabContent.items = settingTabContent.items || []; - for (let setting of settingTabContent.items) { - if (setting === !1) continue; - if (typeof setting === "function") { - setting.apply(this, [$tabContent]); - continue; - } - this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); - } - } - $settings.appendChild($tabContent); + } + }] + }, { + group: "video", + label: t("video"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#video", + items: [{ + pref: "video_player_type", + onChange: onChangeVideoPlayerType + }, { + pref: "video_max_fps", + onChange: (e) => { + limitVideoPlayerFps(parseInt(e.target.value)); + } + }, { + pref: "video_power_preference", + onChange: () => { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + streamPlayer.reloadPlayer(), updateVideoPlayer(); + } + }, { + pref: "video_processing", + onChange: updateVideoPlayer + }, { + pref: "video_ratio", + onChange: updateVideoPlayer + }, { + pref: "video_sharpness", + onChange: updateVideoPlayer + }, { + pref: "video_saturation", + onChange: updateVideoPlayer + }, { + pref: "video_contrast", + onChange: updateVideoPlayer + }, { + pref: "video_brightness", + onChange: updateVideoPlayer + }] + }]; + TAB_CONTROLLER_ITEMS = [ + { + group: "controller", + label: t("controller"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", + items: [{ + pref: "controller_enable_vibration", + unsupported: !VibrationManager.supportControllerVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, { + pref: "controller_device_vibration", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { + pref: "controller_vibration_intensity", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }] + }, + !1 + ]; + TAB_VIRTUAL_CONTROLLER_ITEMS = [{ + group: "mkb", + label: t("virtual-controller"), + helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", + content: !1 + }]; + TAB_NATIVE_MKB_ITEMS = [{ + requiredVariants: "full", + group: "native-mkb", + label: t("native-mkb"), + items: [] + }]; + TAB_SHORTCUTS_ITEMS = [{ + requiredVariants: "full", + group: "controller-shortcuts", + label: t("controller-shortcuts"), + content: !1 + }]; + TAB_STATS_ITEMS = [{ + group: "stats", + label: t("stream-stats"), + helpUrl: "https://better-xcloud.github.io/stream-stats/", + items: [ + { + pref: "stats_show_when_playing" + }, + { + pref: "stats_quick_glance", + onChange: (e) => { + const streamStats = StreamStats.getInstance(); + e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); } - $tabs.firstElementChild.dispatchEvent(new Event("click")); + }, + { + pref: "stats_items", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_position", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_text_size", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_opacity", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_transparent", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_conditional_formatting", + onChange: StreamStats.refreshStyles + } + ] + }]; + SETTINGS_UI = [ + { + icon: BxIcon.HOME, + group: "global", + items: this.TAB_GLOBAL_ITEMS + }, + { + icon: BxIcon.DISPLAY, + group: "stream", + items: this.TAB_DISPLAY_ITEMS + }, + { + icon: BxIcon.CONTROLLER, + group: "controller", + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + !1, + !1, + { + icon: BxIcon.COMMAND, + group: "shortcuts", + items: this.TAB_SHORTCUTS_ITEMS, + requiredVariants: "full" + }, + { + icon: BxIcon.STREAM_STATS, + group: "stats", + items: this.TAB_STATS_ITEMS } - focusTab(tabId) { - const $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); - $tab && $tab.dispatchEvent(new Event("click")); + ]; + constructor() { + super(); + this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + onMounted() { + if (!this.renderFullSettings) return; + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + const $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + } + reloadPage() { + this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); + } + async getRecommendedSettings(deviceCode) { + try { + const json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {}; + if (json.schema_version !== 1) return null; + const 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; + } + addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + 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] = getPrefDefinition(prefKey).default; } - focusIfNeeded() { - this.jumpToSettingGroup("next"); + } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } + async renderSuggestions(e) { + const $btnSuggest = e.target.closest("div"); + $btnSuggest.toggleAttribute("bx-open"); + let $content = $btnSuggest.nextElementSibling; + if ($content) { + BxEvent.dispatch($content.querySelector("select"), "input"); + return; } - focusActiveTab() { - const $currentTab = this.$tabs.querySelector(".bx-active"); - return $currentTab && $currentTab.focus(), !0; - } - focusVisibleSetting(type = "first") { - const controls = Array.from(this.$settings.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; - const $focusable = this.dialogManager.findFocusableElement($control); - if ($focusable) { - if (this.dialogManager.focus($focusable)) return !0; - } + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab || !settingTab.items) continue; + for (let settingTabContent of settingTab.items) { + if (!settingTabContent || !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.suggestedSettingLabels[prefKey] = settingTabContent.label; } - return !1; + } } - focusVisibleTab(type = "first") { - const 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; + let recommendedDevice = ""; + if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { + if (BX_FLAGS.DeviceInfo.androidInfo) { + const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board; + recommendedDevice = await this.getRecommendedSettings(deviceCode); + } } - jumpToSettingGroup(direction) { - const $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)"); - if (!$tabContent) return !1; - let $header; - const $focusing = document.activeElement; - if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; + if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); + else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); + else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); + this.generateDefaultSuggestedSettings(); + const $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, 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) => { + const profile = $select.value; + removeChildElements($suggestedSettings); + const fragment = document.createDocumentFragment(); + let 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)); + const settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue; + let $child, $value; + if (isSameValue) $value = currentValueText; else { - const $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling"; - let $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]; + const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); + $value = currentValueText + " ➔ " + suggestedValueText; + } + let $checkbox; + const breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(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: `bx_suggest_${prefKey}` + }), CE("label", { + for: `bx_suggest_${prefKey}` + }, 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"); + const onClickApply = () => { + const profile = $select.value, settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); + if (!$checkBox.checked || $checkBox.disabled) continue; + const $control = this.settingElements[prefKey]; + if (!$control) { + setPref(prefKey, suggestedValue); + continue; + } + 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: 64 | 32, + onClick: onClickApply + }); + $content = CE("div", { + class: "bx-suggest-box", + _nearby: { + orientation: "vertical" + } + }, BxSelectElement.wrap($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); + } + renderTab(settingTab) { + const $svg = createSvgIcon(settingTab.icon); + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => { + for (let $child of Array.from(this.$settings.children)) + if ($child.getAttribute("data-tab-group") === settingTab.group) { + if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); + } else $child.classList.add("bx-gone"); + for (let $child of Array.from(this.$tabs.children)) + $child.classList.remove("bx-active"); + $svg.classList.add("bx-active"); + }), $svg; + } + onGlobalSettingChanged(e) { + this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger"); + } + renderServerSetting(setting) { + let selectedValue; + const $control = CE("select", { + id: `bx_setting_${setting.pref}`, + title: setting.label, + tabindex: 0 + }); + $control.name = $control.id, $control.addEventListener("input", (e) => { + setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e); + }), selectedValue = getPref("server_region"), setting.options = {}; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + let value = regionName, label = `${region.shortName} - ${regionName}`; + if (region.isDefault) { + if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; + } + setting.options[value] = label; + } + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $control.appendChild($option); + } + return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; + } + renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { + if (typeof setting === "string") setting = { + pref: setting + }; + const pref = setting.pref; + let $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, STORAGE.Global, async (e) => { + const newLocale = e.target.value; + if (getPref("ui_controller_friendly")) { + 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 === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { + const value = e.target.value; + let isCustom = value === "custom", userAgent2 = UserAgent.get(value); + UserAgent.updateStorage(value); + const $inp = $control.nextElementSibling; + $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); + }); + else { + let onChange = setting.onChange; + if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); + $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); + } + if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); + pref && (this.settingElements[pref] = $control); + } + let prefDefinition = null; + if (pref) prefDefinition = getPrefDefinition(pref); + if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; + let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote; + const experimental = prefDefinition?.experimental || setting.experimental; + 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 $label; + const $row = CE("label", { + class: "bx-settings-row", + for: `bx_setting_${pref}`, + "data-type": settingTabContent.group, + _nearby: { + orientation: "horizontal" + } + }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + } + setupDialog() { + let $tabs, $settings; + const $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", {}, this.$btnReload = createButton({ + icon: BxIcon.REFRESH, + style: 32 | 16, + onClick: (e) => { + this.reloadPage(); + } + }), createButton({ + icon: BxIcon.CLOSE, + style: 32 | 16, + onClick: (e) => { + this.dialogManager.hide(); + } + }))), $settings = CE("div", { + class: "bx-settings-tab-contents", + _nearby: { + orientation: "vertical", + focus: () => this.jumpToSettingGroup("next"), + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + })); + this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => { + if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + }); + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab) continue; + if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; + if (settingTab.group !== "global" && !this.renderFullSettings) continue; + const $svg = this.renderTab(settingTab); + $tabs.appendChild($svg); + const $tabContent = CE("div", { + class: "bx-gone", + "data-tab-group": settingTab.group + }); + for (let settingTabContent of settingTab.items) { + if (settingTabContent === !1) continue; + if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; + let label = settingTabContent.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: 1024 | 8 | 32 + }); + } + if (label) { + const $title = CE("h2", { + _nearby: { + orientation: "horizontal" } + }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 4 | 32, + url: settingTabContent.helpUrl, + title: t("help") + })); + $tabContent.appendChild($title); } - let $target; - if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1); - if ($target) return this.dialogManager.focus($target); - return !1; - } - 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; - default: - handled = !1; - break; + if (settingTabContent.unsupportedNote) { + const $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); + $tabContent.appendChild($note); } - return handled; - } - handleGamepad(button) { - let handled = !0; - switch (button) { - case 4: - case 5: - this.focusActiveTab(); - break; - case 6: - this.jumpToSettingGroup("previous"); - break; - case 7: - this.jumpToSettingGroup("next"); - break; - default: - handled = !1; - break; + if (settingTabContent.unsupported) continue; + if (settingTabContent.content) { + $tabContent.appendChild(settingTabContent.content); + continue; } - return handled; + settingTabContent.items = settingTabContent.items || []; + for (let setting of settingTabContent.items) { + if (setting === !1) continue; + if (typeof setting === "function") { + setting.apply(this, [$tabContent]); + continue; + } + this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); + } + } + $settings.appendChild($tabContent); } + $tabs.firstElementChild.dispatchEvent(new Event("click")); + } + focusTab(tabId) { + const $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); + $tab && $tab.dispatchEvent(new Event("click")); + } + focusIfNeeded() { + this.jumpToSettingGroup("next"); + } + focusActiveTab() { + const $currentTab = this.$tabs.querySelector(".bx-active"); + return $currentTab && $currentTab.focus(), !0; + } + focusVisibleSetting(type = "first") { + const controls = Array.from(this.$settings.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; + const $focusable = this.dialogManager.findFocusableElement($control); + if ($focusable) { + if (this.dialogManager.focus($focusable)) return !0; + } + } + return !1; + } + focusVisibleTab(type = "first") { + const 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) { + const $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)"); + if (!$tabContent) return !1; + let $header; + const $focusing = document.activeElement; + if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + else { + const $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling"; + let $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; + } + 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; + default: + handled = !1; + break; + } + return handled; + } + handleGamepad(button) { + let handled = !0; + switch (button) { + case 4: + case 5: + this.focusActiveTab(); + break; + case 6: + this.jumpToSettingGroup("previous"); + break; + case 7: + this.jumpToSettingGroup("next"); + break; + default: + handled = !1; + break; + } + return handled; + } } var BxExposed = { - getTitleInfo: () => STATES.currentStream.titleInfo, - modifyTitleInfo: !1, - 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 { - const 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: !1, - resetControllerShortcut: !1, - overrideSettings: { - Tv_settings: { - hasCompletedOnboarding: !0 - } - }, - disableGamepadPolling: !1, - backButtonPressed: () => { - const navigationDialogManager = NavigationDialogManager.getInstance(); - if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; - const 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; + getTitleInfo: () => STATES.currentStream.titleInfo, + modifyTitleInfo: !1, + 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 { + const 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: !1, + resetControllerShortcut: !1, + overrideSettings: { + Tv_settings: { + hasCompletedOnboarding: !0 + } + }, + disableGamepadPolling: !1, + backButtonPressed: () => { + const navigationDialogManager = NavigationDialogManager.getInstance(); + if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; + const 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; + } }; function localRedirect(path) { - const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); - if (!$pageContent) return; - const $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(); + const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); + if (!$pageContent) return; + const $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 = getPref("server_region"); - if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; - else return preferredRegion; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - if (!region.isDefault) continue; - if (shortName && region.shortName) return region.shortName; - else return regionName; - } - return null; + let preferredRegion = getPref("server_region"); + if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; + else return preferredRegion; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (!region.isDefault) continue; + if (shortName && region.shortName) return region.shortName; + else return regionName; + } + return null; } class HeaderSection { - static #$remotePlayBtn = createButton({ - classes: ["bx-header-remote-play-button", "bx-gone"], - icon: BxIcon.REMOTE_PLAY, - title: t("remote-play"), - style: 4 | 32 | 512, - onClick: (e) => { - RemotePlayManager.getInstance().togglePopup(); - } - }); - static #$settingsBtn = createButton({ - classes: ["bx-header-settings-button"], - label: "???", - style: 8 | 16 | 32 | 128, - onClick: (e) => { - SettingsNavigationDialog.getInstance().show(); - } - }); - static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn); - static #observer; - static #timeout; - static #injectSettingsButton($parent) { - if (!$parent) return; - const PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn; - if (isElementVisible(HeaderSection.#$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(HeaderSection.#$buttonsWrapper); + static #$remotePlayBtn = createButton({ + classes: ["bx-header-remote-play-button", "bx-gone"], + icon: BxIcon.REMOTE_PLAY, + title: t("remote-play"), + style: 4 | 32 | 512, + onClick: (e) => { + RemotePlayManager.getInstance().togglePopup(); } - static checkHeader() { - let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); - if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); - $target && HeaderSection.#injectSettingsButton($target); - } - static showRemotePlayButton() { - HeaderSection.#$remotePlayBtn.classList.remove("bx-gone"); - } - static watchHeader() { - const $root = document.querySelector("#PageContent header") || document.querySelector("#root"); - if (!$root) return; - HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => { - HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000); - }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader(); + }); + static #$settingsBtn = createButton({ + classes: ["bx-header-settings-button"], + label: "???", + style: 8 | 16 | 32 | 128, + onClick: (e) => { + SettingsNavigationDialog.getInstance().show(); } + }); + static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn); + static #observer; + static #timeout; + static #injectSettingsButton($parent) { + if (!$parent) return; + const PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn; + if (isElementVisible(HeaderSection.#$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(HeaderSection.#$buttonsWrapper); + } + static checkHeader() { + let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); + if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); + $target && HeaderSection.#injectSettingsButton($target); + } + static showRemotePlayButton() { + HeaderSection.#$remotePlayBtn.classList.remove("bx-gone"); + } + static watchHeader() { + const $root = document.querySelector("#PageContent header") || document.querySelector("#root"); + if (!$root) return; + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => { + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000); + }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader(); + } } class RemotePlayNavigationDialog extends NavigationDialog { - static instance; - static getInstance() { - if (!RemotePlayNavigationDialog.instance) RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog; - return RemotePlayNavigationDialog.instance; - } - STATE_LABELS = { - On: t("powered-on"), - Off: t("powered-off"), - ConnectedStandby: t("standby"), - Unknown: t("unknown") - }; - $container; - constructor() { - super(); - this.setupDialog(); - } - setupDialog() { - const $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"); - let $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); - if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); - $resolutions.addEventListener("input", (e) => { - const value = e.target.value; - $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); - }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { - manualTrigger: !0 - }); - const $qualitySettings = CE("div", { - class: "bx-remote-play-settings" - }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutions)); - $fragment.appendChild($qualitySettings); - const manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); - for (let con of consoles) { - const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, 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 | 32, - 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: 4 | 32, - url: "https://better-xcloud.github.io/remote-play", - label: t("help") - }), createButton({ - style: 4 | 32, - label: t("close"), - onClick: (e) => this.hide() - }))), this.$container = $fragment; - } - getDialog() { - return this; - } - getContent() { - return this.$container; - } - focusIfNeeded() { - const $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); - $btnConnect && $btnConnect.focus(); + static instance; + static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog); + STATE_LABELS = { + On: t("powered-on"), + Off: t("powered-off"), + ConnectedStandby: t("standby"), + Unknown: t("unknown") + }; + $container; + constructor() { + super(); + this.setupDialog(); + } + setupDialog() { + const $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"); + let $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); + if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); + $resolutions.addEventListener("input", (e) => { + const value = e.target.value; + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); + }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { + manualTrigger: !0 + }); + const $qualitySettings = CE("div", { + class: "bx-remote-play-settings" + }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutions)); + $fragment.appendChild($qualitySettings); + const manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); + for (let con of consoles) { + const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, 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 | 32, + 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: 4 | 32, + url: "https://better-xcloud.github.io/remote-play", + label: t("help") + }), createButton({ + style: 4 | 32, + label: t("close"), + onClick: (e) => this.hide() + }))), this.$container = $fragment; + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + focusIfNeeded() { + const $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); + $btnConnect && $btnConnect.focus(); + } } var LOG_TAG2 = "RemotePlay"; class RemotePlayManager { - static instance; - static getInstance() { - if (!this.instance) this.instance = new RemotePlayManager; - return this.instance; + static instance; + static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager); + isInitialized = !1; + XCLOUD_TOKEN; + XHOME_TOKEN; + consoles; + regions = []; + initialize() { + if (this.isInitialized) return; + this.isInitialized = !0, this.getXhomeToken(() => { + this.getConsolesList(() => { + BxLogger.info(LOG_TAG2, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); + }); + }); + } + get xcloudToken() { + return this.XCLOUD_TOKEN; + } + set xcloudToken(token) { + this.XCLOUD_TOKEN = token; + } + get xhomeToken() { + return this.XHOME_TOKEN; + } + getConsoles() { + return this.consoles; + } + getXhomeToken(callback) { + if (this.XHOME_TOKEN) { + callback(); + return; } - isInitialized = !1; - XCLOUD_TOKEN; - XHOME_TOKEN; - consoles; - regions = []; - initialize() { - if (this.isInitialized) return; - this.isInitialized = !0, this.getXhomeToken(() => { - this.getConsolesList(() => { - BxLogger.info(LOG_TAG2, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); - }); - }); - } - get xcloudToken() { - return this.XCLOUD_TOKEN; - } - set xcloudToken(token) { - this.XCLOUD_TOKEN = token; - } - get xhomeToken() { - return this.XHOME_TOKEN; - } - getConsoles() { - return this.consoles; - } - getXhomeToken(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++) { + const key = localStorage.key(i); + if (!key.startsWith("Auth.User.")) continue; + const 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; } - 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++) { - const key = localStorage.key(i); - if (!key.startsWith("Auth.User.")) continue; - const 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; - } - } - const 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(); - }); + break; + } } - async getConsolesList(callback) { - if (this.consoles) { - callback(); - return; - } - const options = { - method: "GET", - headers: { - Authorization: `Bearer ${this.XHOME_TOKEN}` - } - }; - for (let region of this.regions) - try { - const 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(); + const 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; } - play(serverId, resolution) { - if (resolution) setPref("xhome_resolution", resolution); - STATES.remotePlay.config = { - serverId - }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); + const options = { + method: "GET", + headers: { + Authorization: `Bearer ${this.XHOME_TOKEN}` + } + }; + for (let region of this.regions) + try { + const 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) setPref("xhome_resolution", resolution); + 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; } - 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; - } - if (AppInterface && AppInterface.showRemotePlayDialog) { - AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); - return; - } - RemotePlayNavigationDialog.getInstance().show(); + if (this.consoles.length === 0) { + Toast.show(t("no-consoles-found"), "", { instant: !0 }); + return; } - static detect() { - if (!getPref("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; + if (AppInterface && AppInterface.showRemotePlayDialog) { + AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); + return; } + RemotePlayNavigationDialog.getInstance().show(); + } + static detect() { + if (!getPref("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 LoadingScreen { - static $bgStyle; - static $waitTimeBox; - static waitTimeInterval = null; - static orgWebTitle; - static secondsToString(seconds) { - const 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 $bgStyle; + static $waitTimeBox; + static waitTimeInterval = null; + static orgWebTitle; + static secondsToString(seconds) { + const 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() { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) return; + if (!LoadingScreen.$bgStyle) { + const $bgStyle = CE("style"); + document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; } - static setup() { - const titleInfo = STATES.currentStream.titleInfo; - if (!titleInfo) return; - if (!LoadingScreen.$bgStyle) { - const $bgStyle = CE("style"); - document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; - } - if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_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", $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;}`; - const bg = new Image; - bg.onload = (e) => { - $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; - }, bg.src = imageUrl; - } - static setupWaitTime(waitTime) { - if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); - let secondsLeft = waitTime, $countDown, $estimated; - LoadingScreen.orgWebTitle = document.title; - const 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", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, 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"), getPref("ui_loading_screen_game_art") && LoadingScreen.$bgStyle) { - const $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; + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_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", $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;}`; + const bg = new Image; + bg.onload = (e) => { + $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; + }, bg.src = imageUrl; + } + static setupWaitTime(waitTime) { + if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); + let secondsLeft = waitTime, $countDown, $estimated; + LoadingScreen.orgWebTitle = document.title; + const 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", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, 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"), getPref("ui_loading_screen_game_art") && LoadingScreen.$bgStyle) { + const $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 #BUTTONS = { - scriptSettings: createButton({ - label: t("better-xcloud"), - style: 64 | 32 | 1, - onClick: (e) => { - window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { - setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); - }, { once: !0 }), GuideMenu.#closeGuideMenu(); - } - }), - closeApp: AppInterface && createButton({ - icon: BxIcon.POWER, - label: t("close-app"), - title: t("close-app"), - style: 64 | 32 | 2, - onClick: (e) => { - AppInterface.closeApp(); - }, - attributes: { - "data-state": "normal" - } - }), - reloadPage: createButton({ - icon: BxIcon.REFRESH, - label: t("reload-page"), - title: t("reload-page"), - style: 64 | 32, - onClick: (e) => { - if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); - else window.location.reload(); - GuideMenu.#closeGuideMenu(); - } - }), - backToHome: createButton({ - icon: BxIcon.HOME, - label: t("back-to-home"), - title: t("back-to-home"), - style: 64 | 32, - onClick: (e) => { - confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); - }, - attributes: { - "data-state": "playing" - } - }) - }; - static #$renderedButtons; - static #closeGuideMenu() { - if (window.BX_EXPOSED.dialogRoutes) { - window.BX_EXPOSED.dialogRoutes.closeAll(); - return; - } - const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); - $btnClose && $btnClose.click(); + static #BUTTONS = { + scriptSettings: createButton({ + label: t("better-xcloud"), + style: 64 | 32 | 1, + onClick: (e) => { + window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { + setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + }, { once: !0 }), GuideMenu.#closeGuideMenu(); + } + }), + closeApp: AppInterface && createButton({ + icon: BxIcon.POWER, + label: t("close-app"), + title: t("close-app"), + style: 64 | 32 | 2, + onClick: (e) => { + AppInterface.closeApp(); + }, + attributes: { + "data-state": "normal" + } + }), + reloadPage: createButton({ + icon: BxIcon.REFRESH, + label: t("reload-page"), + title: t("reload-page"), + style: 64 | 32, + onClick: (e) => { + if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); + else window.location.reload(); + GuideMenu.#closeGuideMenu(); + } + }), + backToHome: createButton({ + icon: BxIcon.HOME, + label: t("back-to-home"), + title: t("back-to-home"), + style: 64 | 32, + onClick: (e) => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); + }, + attributes: { + "data-state": "playing" + } + }) + }; + static #$renderedButtons; + static #closeGuideMenu() { + if (window.BX_EXPOSED.dialogRoutes) { + window.BX_EXPOSED.dialogRoutes.closeAll(); + return; } - static #renderButtons() { - if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; - const $div = CE("div", { - class: "bx-guide-home-buttons" - }), buttons = [ - GuideMenu.#BUTTONS.scriptSettings, - [ - GuideMenu.#BUTTONS.backToHome, - GuideMenu.#BUTTONS.reloadPage, - GuideMenu.#BUTTONS.closeApp - ] - ]; - for (let $button of buttons) { - if (!$button) continue; - if ($button instanceof HTMLElement) $div.appendChild($button); - else if (Array.isArray($button)) { - const $wrapper = CE("div", {}); - for (let $child of $button) - $child && $wrapper.appendChild($child); - $div.appendChild($wrapper); - } - } - return GuideMenu.#$renderedButtons = $div, $div; + const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); + $btnClose && $btnClose.click(); + } + static #renderButtons() { + if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; + const $div = CE("div", { + class: "bx-guide-home-buttons" + }), buttons = [ + GuideMenu.#BUTTONS.scriptSettings, + [ + GuideMenu.#BUTTONS.backToHome, + GuideMenu.#BUTTONS.reloadPage, + GuideMenu.#BUTTONS.closeApp + ] + ]; + for (let $button of buttons) { + if (!$button) continue; + if ($button instanceof HTMLElement) $div.appendChild($button); + else if (Array.isArray($button)) { + const $wrapper = CE("div", {}); + for (let $child of $button) + $child && $wrapper.appendChild($child); + $div.appendChild($wrapper); + } } - static #injectHome($root, isPlaying = !1) { - let $target = null; - if (isPlaying) { - $target = $root.querySelector("a[class*=QuitGameButton]"); - const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); - $btnXcloudHome && ($btnXcloudHome.style.display = "none"); - } else { - const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); - if ($dividers) $target = $dividers[$dividers.length - 1]; - } - if (!$target) return !1; - const $buttons = GuideMenu.#renderButtons(); - $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + return GuideMenu.#$renderedButtons = $div, $div; + } + static #injectHome($root, isPlaying = !1) { + let $target = null; + if (isPlaying) { + $target = $root.querySelector("a[class*=QuitGameButton]"); + const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); + $btnXcloudHome && ($btnXcloudHome.style.display = "none"); + } else { + const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); + if ($dividers) $target = $dividers[$dividers.length - 1]; } - static async#onShown(e) { - if (e.where === "home") { - const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); - $root && GuideMenu.#injectHome($root, STATES.isPlaying); - } + if (!$target) return !1; + const $buttons = GuideMenu.#renderButtons(); + $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + } + static async#onShown(e) { + if (e.where === "home") { + const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && GuideMenu.#injectHome($root, STATES.isPlaying); } - static addEventListeners() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); - } - static observe($addedElm) { - const className = $addedElm.className; - if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; - const $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" }); - } + } + static addEventListeners() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); + } + static observe($addedElm) { + const className = $addedElm.className; + if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; + const $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() { - if (!StreamBadges.instance) StreamBadges.instance = new StreamBadges; - return StreamBadges.instance; + static instance; + static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new 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" } - 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; + setRegion(region) { + this.serverInfo.server = { + region, + ipv6: !1 }; - $container; - intervalId; - REFRESH_INTERVAL = 3000; - setRegion(region) { - this.serverInfo.server = { - region, - ipv6: !1 - }; + } + renderBadge(name, value) { + const badgeInfo = this.badges[name]; + let $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; + } + async updateBadges(forceUpdate = !1) { + if (!this.$container || !forceUpdate && !this.$container.isConnected) { + this.stop(); + return; } - renderBadge(name, value) { - const badgeInfo = this.badges[name]; - let $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; + const statsCollector = StreamStatsCollector.getInstance(); + await statsCollector.collect(); + const 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() + }; + let name; + for (name in badges) { + const value = badges[name]; + if (value === null) continue; + const $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 updateBadges(forceUpdate = !1) { - if (!this.$container || !forceUpdate && !this.$container.isConnected) { - this.stop(); - return; - } - const statsCollector = StreamStatsCollector.getInstance(); - await statsCollector.collect(); - const 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() - }; - let name; - for (name in badges) { - const value = badges[name]; - if (value === null) continue; - const $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.bind(this), this.REFRESH_INTERVAL); + } + stop() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + async render() { + if (this.$container) return this.start(), this.$container; + await this.getServerStats(); + let batteryLevel = ""; + if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; + const BADGES = [ + ["playtime", "1m"], + ["battery", batteryLevel], + ["download", humanFileSize(0)], + ["upload", humanFileSize(0)], + this.serverInfo.server ? 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" }); + return BADGES.forEach((item) => { + if (!item) return; + let $badge; + if (!(item instanceof HTMLElement)) $badge = this.renderBadge(...item); + else $badge = item; + $container.appendChild($badge); + }), this.$container = $container, await this.start(), $container; + } + async getServerStats() { + const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; + let videoCodecId, videoWidth = 0, videoHeight = 0; + const allAudioCodecs = {}; + let audioCodecId; + const allCandidates = {}; + let candidateId; + if (stats.forEach((stat) => { + if (stat.type === "codec") { + const 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 === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") candidateId = stat.remoteCandidateId; + else if (stat.type === "remote-candidate") allCandidates[stat.id] = stat.address; + }), videoCodecId) { + const videoStat = allVideoCodecs[videoCodecId], video = { + width: videoWidth, + height: videoHeight, + codec: videoStat.mimeType.substring(6) + }; + if (video.codec === "H264") { + const 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) { + const profile = video.profile; + let 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; } - async start() { - await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL); + if (audioCodecId) { + const 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; } - stop() { - this.intervalId && clearInterval(this.intervalId), this.intervalId = null; - } - async render() { - if (this.$container) return this.start(), this.$container; - await this.getServerStats(); - let batteryLevel = ""; - if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; - const BADGES = [ - ["playtime", "1m"], - ["battery", batteryLevel], - ["download", humanFileSize(0)], - ["upload", humanFileSize(0)], - this.serverInfo.server ? 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" }); - return BADGES.forEach((item) => { - if (!item) return; - let $badge; - if (!(item instanceof HTMLElement)) $badge = this.renderBadge(...item); - else $badge = item; - $container.appendChild($badge); - }), this.$container = $container, await this.start(), $container; - } - async getServerStats() { - const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; - let videoCodecId, videoWidth = 0, videoHeight = 0; - const allAudioCodecs = {}; - let audioCodecId; - const allCandidates = {}; - let candidateId; - if (stats.forEach((stat) => { - if (stat.type === "codec") { - const 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 === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") candidateId = stat.remoteCandidateId; - else if (stat.type === "remote-candidate") allCandidates[stat.id] = stat.address; - }), videoCodecId) { - const videoStat = allVideoCodecs[videoCodecId], video = { - width: videoWidth, - height: videoHeight, - codec: videoStat.mimeType.substring(6) - }; - if (video.codec === "H264") { - const 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) { - const profile = video.profile; - let 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) { - const 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 (candidateId) { - BxLogger.info("candidate", candidateId, allCandidates); - const server = this.serverInfo.server; - if (server) { - server.ipv6 = allCandidates[candidateId].includes(":"); - let text = ""; - if (server.region) text += server.region; - text += "@" + (server.ipv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); - } - } - } - static setupEvents() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, async (e) => { - if (e.where !== "home" || !STATES.isPlaying) return; - const $btnQuit = document.querySelector("#gamepass-dialog-root a[class*=QuitGameButton]"); - if ($btnQuit) $btnQuit.insertAdjacentElement("beforebegin", await StreamBadges.getInstance().render()); - }); + if (candidateId) { + BxLogger.info("candidate", candidateId, allCandidates); + const server = this.serverInfo.server; + if (server) { + server.ipv6 = allCandidates[candidateId].includes(":"); + let text = ""; + if (server.region) text += server.region; + text += "@" + (server.ipv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); + } } + } + static setupEvents() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, async (e) => { + if (e.where !== "home" || !STATES.isPlaying) return; + const $btnQuit = document.querySelector("#gamepass-dialog-root a[class*=QuitGameButton]"); + if ($btnQuit) $btnQuit.insertAdjacentElement("beforebegin", await StreamBadges.getInstance().render()); + }); + } } class XcloudInterceptor { - static async#handleLogin(request, init) { - const bypassServer = getPref("server_bypass_restriction"); - if (bypassServer !== "off") { - const ip = BypassServerIps[bypassServer]; - ip && request.headers.set("X-Forwarded-For", ip); - } - const response = await NATIVE_FETCH(request, init); - if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; - const obj = await response.clone().json(); - RemotePlayManager.getInstance().xcloudToken = obj.gsToken; - const serverEmojis = { - AustraliaEast: "🇦🇺", - AustraliaSouthEast: "🇦🇺", - BrazilSouth: "🇧🇷", - EastUS: "🇺🇸", - EastUS2: "🇺🇸", - JapanEast: "🇯🇵", - KoreaCentral: "🇰🇷", - MexicoCentral: "🇲🇽", - NorthCentralUs: "🇺🇸", - SouthCentralUS: "🇺🇸", - UKSouth: "🇬🇧", - WestEurope: "🇪🇺", - WestUS: "🇺🇸", - WestUS2: "🇺🇸" - }, serverRegex = /\/\/(\w+)\./; - for (let region of obj.offeringSettings.regions) { - const regionName = region.name; - let shortName = region.name; - if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); - let match = serverRegex.exec(region.baseUri); - if (match) { - if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName; - } - region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); - } - BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY); - const preferredRegion = getPreferredServerRegion(); - if (preferredRegion && preferredRegion in STATES.serverRegions) { - const 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#handleLogin(request, init) { + const bypassServer = getPref("server_bypass_restriction"); + if (bypassServer !== "off") { + const ip = BypassServerIps[bypassServer]; + ip && request.headers.set("X-Forwarded-For", ip); } - static async#handlePlay(request, init) { - const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); - let badgeRegion = parsedUrl.host.split(".", 1)[0]; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - if (parsedUrl.origin == region.baseUri) { - badgeRegion = regionName; - break; - } - } - StreamBadges.getInstance().setRegion(badgeRegion); - const body = await request.clone().json(); - if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { - const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; - body.settings.osName = osName; - } - if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; - const newRequest = new Request(request, { - body: JSON.stringify(body) - }); - return NATIVE_FETCH(newRequest); + const response = await NATIVE_FETCH(request, init); + if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; + const obj = await response.clone().json(); + RemotePlayManager.getInstance().xcloudToken = obj.gsToken; + const serverEmojis = { + AustraliaEast: "🇦🇺", + AustraliaSouthEast: "🇦🇺", + BrazilSouth: "🇧🇷", + EastUS: "🇺🇸", + EastUS2: "🇺🇸", + JapanEast: "🇯🇵", + KoreaCentral: "🇰🇷", + MexicoCentral: "🇲🇽", + NorthCentralUs: "🇺🇸", + SouthCentralUS: "🇺🇸", + UKSouth: "🇬🇧", + WestEurope: "🇪🇺", + WestUS: "🇺🇸", + WestUS2: "🇺🇸" + }, serverRegex = /\/\/(\w+)\./; + for (let region of obj.offeringSettings.regions) { + const regionName = region.name; + let shortName = region.name; + if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); + let match = serverRegex.exec(region.baseUri); + if (match) { + if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName; + } + region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); } - static async#handleWaitTime(request, init) { - const response = await NATIVE_FETCH(request, init); - if (getPref("ui_loading_screen_wait_time")) { - const json = await response.clone().json(); - if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); - } - return response; + BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY); + const preferredRegion = getPreferredServerRegion(); + if (preferredRegion && preferredRegion in STATES.serverRegions) { + const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); + tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; } - static async#handleConfiguration(request, init) { - if (request.method !== "GET") return NATIVE_FETCH(request, init); - const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); - if (!text.length) return response; - const obj = JSON.parse(text); - let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; - overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; - let overrideMkb = null; - if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; - if (getPref("native_mkb_enabled") === "off") overrideMkb = !1; - if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { - enableMouseInput: overrideMkb, - enableKeyboardInput: overrideMkb - }); - if (getPref("audio_mic_on_playing")) 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; + return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; + } + static async#handlePlay(request, init) { + const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); + let badgeRegion = parsedUrl.host.split(".", 1)[0]; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (parsedUrl.origin == region.baseUri) { + badgeRegion = regionName; + break; + } } - 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); + StreamBadges.getInstance().setRegion(badgeRegion); + const body = await request.clone().json(); + if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { + const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; + body.settings.osName = osName; } + if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; + const newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async#handleWaitTime(request, init) { + const response = await NATIVE_FETCH(request, init); + if (getPref("ui_loading_screen_wait_time")) { + const 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); + const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); + if (!text.length) return response; + const obj = JSON.parse(text); + let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; + overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; + let overrideMkb = null; + if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("native_mkb_enabled") === "off") overrideMkb = !1; + if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, { + enableMouseInput: overrideMkb, + enableKeyboardInput: overrideMkb + }); + if (getPref("audio_mic_on_playing")) 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"); + window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer"); } function clearDbLogs(dbName, table) { - const request = window.indexedDB.open(dbName); - request.onsuccess = (e) => { - const db = e.target.result; - try { - const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); - objectStoreRequest.onsuccess = function() { - console.log(`[Better xCloud] Cleared ${dbName}.${table}`); - }; - } catch (ex) {} - }; + const request = window.indexedDB.open(dbName); + request.onsuccess = (e) => { + const db = e.target.result; + try { + const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); + objectStoreRequest.onsuccess = function() { + console.log(`[Better xCloud] Cleared ${dbName}.${table}`); + }; + } catch (ex) {} + }; } function clearAllLogs() { - clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); + clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); } function updateIceCandidates(candidates, options) { - const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; - for (let item of candidates) { - if (item.candidate == "a=end-of-candidates") continue; - const groups = pattern.exec(item.candidate).groups; - lst.push(groups); - } - if (options.preferIpv6Server) lst.sort((a, b) => { - const firstIp = a.ip, secondIp = b.ip; - return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; - }); - const newCandidates = []; - let foundation = 1; - const newCandidate = (candidate) => { - return { - candidate, - messageType: "iceCandidate", - sdpMLineIndex: "0", - sdpMid: "0" - }; + const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = []; + for (let item of candidates) { + if (item.candidate == "a=end-of-candidates") continue; + const groups = pattern.exec(item.candidate).groups; + lst.push(groups); + } + if (options.preferIpv6Server) lst.sort((a, b) => { + const firstIp = a.ip, secondIp = b.ip; + return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; + }); + const newCandidates = []; + let foundation = 1; + const newCandidate = (candidate) => { + return { + candidate, + messageType: "iceCandidate", + sdpMLineIndex: "0", + sdpMid: "0" }; - if (lst.forEach((item) => { - item.foundation = foundation, item.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.port} ${item.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; + }; + if (lst.forEach((item) => { + item.foundation = foundation, item.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.port} ${item.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) { - const response = await NATIVE_FETCH(request), text = await response.clone().text(); - if (!text.length) return response; - const options = { - preferIpv6Server: getPref("prefer_ipv6_server"), - consoleAddrs - }, obj = JSON.parse(text); - let 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; + const response = await NATIVE_FETCH(request), text = await response.clone().text(); + if (!text.length) return response; + const options = { + preferIpv6Server: getPref("prefer_ipv6_server"), + consoleAddrs + }, obj = JSON.parse(text); + let 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 (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ - "https://arc.msn.com", - "https://browser.events.data.microsoft.com", - "https://dc.services.visualstudio.com", - "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" - ]); - if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ - "https://peoplehub.xboxlive.com/users/me/people/social", - "https://peoplehub.xboxlive.com/users/me/people/recommendations", - "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" - ]); - const 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 blocked of BLOCKED_URLS) - if (this._url.startsWith(blocked)) { - if (blocked === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); - return !1; - } - return nativeXhrSend.apply(this, arguments); - }; - let gamepassAllGames = []; - 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)) continue; - return new Response('{"acc":1,"webResult":{}}', { - status: 200, - statusText: "200 OK" - }); - } - if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING); - if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING); - if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { - const 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) { - console.log(e); - } - if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { - const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); - if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c")) for (let i = 1;i < obj.length; i++) - gamepassAllGames.push(obj[i].id); - else if (!1) try {} catch (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")) { - const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); - try { - const newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item) => ({ id: item })); - 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"; - return XcloudInterceptor.handle(request, init); - }; + let BLOCKED_URLS = []; + if (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://arc.msn.com", + "https://browser.events.data.microsoft.com", + "https://dc.services.visualstudio.com", + "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" + ]); + if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://peoplehub.xboxlive.com/users/me/people/social", + "https://peoplehub.xboxlive.com/users/me/people/recommendations", + "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" + ]); + const 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 blocked of BLOCKED_URLS) + if (this._url.startsWith(blocked)) { + if (blocked === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); + return !1; + } + return nativeXhrSend.apply(this, arguments); + }; + let gamepassAllGames = []; + 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)) continue; + return new Response('{"acc":1,"webResult":{}}', { + status: 200, + statusText: "200 OK" + }); + } + if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING); + if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING); + if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { + const 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) { + console.log(e); + } + if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c")) for (let i = 1;i < obj.length; i++) + gamepassAllGames.push(obj[i].id); + else if (!1) try {} catch (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")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + try { + const newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item) => ({ id: item })); + 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"; + return XcloudInterceptor.handle(request, init); + }; } function showGamepadToast(gamepad) { - if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; - BxLogger.info("Gamepad", gamepad); - let text = "🎮"; - if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; - const 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 }); + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; + const 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 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-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:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:'promptfont';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}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-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)}.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}select[multiple]{overflow:auto}#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}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);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-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-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))}.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-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));backdrop-filter:blur(4px) brightness(1.5)}.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);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-left:10px}.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-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-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:20px;border-radius:8px;z-index:var(--bx-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-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-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-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-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-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-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-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.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-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]: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-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.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;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);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 > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-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}@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 + *{margin:0 0 0 auto}.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}.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-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.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-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-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.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}.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;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;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}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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 span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{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 button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;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[data-disabled=true] button{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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;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:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.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:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.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}}`; - const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; - if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); - 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 (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); - if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; - if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; - if (getPref("hide_dots_icon")) 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}", getPref("stream_simplify_menu")) 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 (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; - const $style = CE("style", {}, css); - document.documentElement.appendChild($style); + 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-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:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:'promptfont';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}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-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)}.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}select[multiple]{overflow:auto}#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}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);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-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-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))}.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-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));backdrop-filter:blur(4px) brightness(1.5)}.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);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-left:10px}.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-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-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:20px;border-radius:8px;z-index:var(--bx-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-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-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-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-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-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-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-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.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-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]: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-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.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;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);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 > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-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}@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 + *{margin:0 0 0 auto}.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}.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-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.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-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-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.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}.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;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;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}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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 span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{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 button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;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[data-disabled=true] button{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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;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:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.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:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.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}}`; + const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; + if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + 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 (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; + if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("hide_dots_icon")) 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}", getPref("stream_simplify_menu")) 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 (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + const $style = CE("style", {}, css); + document.documentElement.appendChild($style); } function preloadFonts() { - const $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); + const $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 #timeout; - static #cursorVisible = !0; - static show() { - document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; - } - static hide() { - document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; - } - static onMouseMove(e) { - !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); - } - static start() { - MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); - } - static stop() { - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); - } + static #timeout; + static #cursorVisible = !0; + static show() { + document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; + } + static hide() { + document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; + } + static onMouseMove(e) { + !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); + } + static start() { + MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); + } + static stop() { + MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); + } } function patchHistoryMethod(type) { - const orig = window.history[type]; - return function(...args) { - return BxEvent.dispatch(window, BxEvent.POPSTATE, { - arguments: args - }), orig.apply(this, arguments); - }; + const 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); - const $settings = document.querySelector(".bx-settings-container"); - if ($settings) $settings.classList.add("bx-gone"); - NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; + window.setTimeout(RemotePlayManager.detect, 10); + const $settings = document.querySelector(".bx-settings-container"); + if ($settings) $settings.classList.add("bx-gone"); + NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); } function setCodecPreferences(sdp, preferredCodec) { - const 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) { - const id = match[1]; - if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); - } - if (!preferredCodecIds.length) return sdp; - const lines = sdp.split("\r\n"); - for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (!line.startsWith("m=video")) continue; - const tmp = line.trim().split(" "); - let ids = tmp.slice(3); - ids = ids.filter((item) => !preferredCodecIds.includes(item)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); - break; - } - return lines.join("\r\n"); + const 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) { + const id = match[1]; + if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id); + } + if (!preferredCodecIds.length) return sdp; + const lines = sdp.split("\r\n"); + for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + if (!line.startsWith("m=video")) continue; + const tmp = line.trim().split(" "); + let ids = tmp.slice(3); + ids = ids.filter((item) => !preferredCodecIds.includes(item)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" "); + break; + } + return lines.join("\r\n"); } function patchSdpBitrate(sdp, video, audio) { - const lines = sdp.split("\r\n"), mediaSet = new Set; - !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); - const 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; - const 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; - } - } + const lines = sdp.split("\r\n"), mediaSet = new Set; + !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); + const 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; + const 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\n"); + } + return lines.join("\r\n"); } var clarity_boost_default = "#version 300 es\n\nin vec4 position;\n\nvoid main() {\ngl_Position = position;\n}\n"; var clarity_boost_default2 = "#version 300 es\n\nprecision mediump float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nconst int FILTER_UNSHARP_MASKING = 1;\n\nconst float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;\n\nconst vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nout vec4 fragColor;\n\nvec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) {\nvec2 texelSize = 1.0 / iResolution.xy;\n\nvec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb;\nvec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb;\nvec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb;\n\nvec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb;\nvec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb;\n\nvec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb;\nvec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb;\nvec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb;\n\nif (filterId == FILTER_UNSHARP_MASKING) {\nvec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;\ngaussianBlur /= 16.0;\n\nreturn e + (e - gaussianBlur) * sharpenFactor / 3.0;\n}\n\nvec3 minRgb = min(min(min(d, e), min(f, b)), h);\nminRgb += min(min(a, c), min(g, i));\n\nvec3 maxRgb = max(max(max(d, e), max(f, b)), h);\nmaxRgb += max(max(a, c), max(g, i));\n\nvec3 reciprocalMaxRgb = 1.0 / maxRgb;\nvec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\namplifyRgb = inversesqrt(amplifyRgb);\n\nvec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));\nvec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\nvec3 window = b + d + f + h;\nvec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\nreturn mix(e, outColor, sharpenFactor / 2.0);\n}\n\nvoid main() {\nvec2 uv = gl_FragCoord.xy / iResolution.xy;\nvec3 color = texture(data, uv).rgb;\n\ncolor = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color;\n\ncolor = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color;\n\ncolor = contrast * (color - 0.5) + 0.5;\n\ncolor = brightness * color;\n\nfragColor = vec4(color, 1.0);\n}\n"; 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 = Math.ceil(1000 / this.targetFps); - lastFrameTime = 0; - animFrameId = null; - constructor($video) { - BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; - const $canvas = document.createElement("canvas"); - $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas); + 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 = Math.ceil(1000 / this.targetFps); + lastFrameTime = 0; + animFrameId = null; + constructor($video) { + BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; + const $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() { + const 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); + } + drawFrame() { + if (this.targetFps === 0) return; + if (this.targetFps < 60) { + const currentTime = performance.now(); + if (currentTime - this.lastFrameTime < this.frameInterval) return; + this.lastFrameTime = currentTime; } - setFilter(filterId, update = !0) { - this.options.filterId = filterId, update && this.updateCanvas(); + const 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 animate; + if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { + const $video = this.$video; + animate = () => { + if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate); + }, this.animFrameId = $video.requestVideoFrameCallback(animate); + } else animate = () => { + if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate); + }, this.animFrameId = requestAnimationFrame(animate); + } + setupShaders() { + BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); + const gl = this.$canvas.getContext("webgl2", { + isBx: !0, + antialias: !0, + alpha: !1, + powerPreference: getPref("video_power_preference") + }); + this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); + const vShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); + const fShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); + const 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(); + const 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); + const 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; } - 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.frameInterval = Math.ceil(1000 / target); - } - getCanvas() { - return this.$canvas; - } - updateCanvas() { - const 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); - } - drawFrame() { - if (this.targetFps < 60) { - const currentTime = performance.now(); - if (currentTime - this.lastFrameTime < this.frameInterval) return; - this.lastFrameTime = currentTime; - } - const 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 animate; - if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { - const $video = this.$video; - animate = () => { - if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate); - }, this.animFrameId = $video.requestVideoFrameCallback(animate); - } else animate = () => { - if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate); - }, this.animFrameId = requestAnimationFrame(animate); - } - setupShaders() { - BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); - const gl = this.$canvas.getContext("webgl2", { - isBx: !0, - antialias: !0, - alpha: !1, - powerPreference: getPref("video_power_preference") - }); - this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); - const vShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); - const fShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); - const 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(); - const 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); - const 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(); - const gl = this.gl; - if (gl) { - gl.getExtension("WEBGL_lose_context")?.loseContext(); - for (let resource of this.resources) - if (resource instanceof WebGLProgram) gl.useProgram(null), 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; + } + destroy() { + BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); + const 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); + $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) { + this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix"); + return; } - setupVideoElements() { - if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) { - this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix"); - return; - } - const $fragment = document.createDocumentFragment(); - this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); - const $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); + const $fragment = document.createDocumentFragment(); + this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); + const $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() { + const filters = [], sharpness = this.options.sharpness || 0; + if (this.options.processing === "usm" && sharpness != 0) { + const 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)"); } - getVideoPlayerFilterStyle() { - const filters = [], sharpness = this.options.sharpness || 0; - if (this.options.processing === "usm" && sharpness != 0) { - const 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)"); - } - const saturation = this.options.saturation || 100; - if (saturation != 100) filters.push(`saturate(${saturation}%)`); - const contrast = this.options.contrast || 100; - if (contrast != 100) filters.push(`contrast(${contrast}%)`); - const brightness = this.options.brightness || 100; - if (brightness != 100) filters.push(`brightness(${brightness}%)`); - return filters.join(" "); - } - resizePlayer() { - const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; - let $webGL2Canvas; - if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); - let targetWidth, targetHeight, targetObjectFit; - if (PREF_RATIO.includes(":")) { - const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); - let width = 0, height = 0; - const 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(), 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; - if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); - } - setPlayerType(type, refreshPlayer = !1) { - if (this.playerType !== type) if (type === "webgl2") { - if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); - else this.webGL2Player.resume(); - this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel"); - } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel"); - 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") { - const options = this.options, webGL2Player = this.webGL2Player; - if (options.processing === "usm") webGL2Player.setFilter(1); - else webGL2Player.setFilter(2); - 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;`; - 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(); + const saturation = this.options.saturation || 100; + if (saturation != 100) filters.push(`saturate(${saturation}%)`); + const contrast = this.options.contrast || 100; + if (contrast != 100) filters.push(`contrast(${contrast}%)`); + const brightness = this.options.brightness || 100; + if (brightness != 100) filters.push(`brightness(${brightness}%)`); + return filters.join(" "); + } + resizePlayer() { + const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; + let $webGL2Canvas; + if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); + let targetWidth, targetHeight, targetObjectFit; + if (PREF_RATIO.includes(":")) { + const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); + let width = 0, height = 0; + const 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(), 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; + if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); + } + setPlayerType(type, refreshPlayer = !1) { + if (this.playerType !== type) if (type === "webgl2") { + if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); + else this.webGL2Player.resume(); + this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel"); + } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel"); + 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") { + const options = this.options, webGL2Player = this.webGL2Player; + if (options.processing === "usm") webGL2Player.setFilter(1); + else webGL2Player.setFilter(2); + 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;`; + 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() { - const PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { - if (this.style.visibility = "visible", !this.videoWidth) return; - const playerOptions = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") - }; - STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_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); - } - const $parent = this.parentElement; - if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); - return nativePlay.apply(this); + const PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { + if (this.style.visibility = "visible", !this.videoWidth) return; + const playerOptions = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") }; + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_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); + } + const $parent = this.parentElement; + if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); + return nativePlay.apply(this); + }; } function patchRtcCodecs() { - if (getPref("stream_codec_profile") === "default") return; - if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; + if (getPref("stream_codec_profile") === "default") return; + if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; } function patchRtcPeerConnection() { - const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; - RTCPeerConnection.prototype.createDataChannel = function() { - const dataChannel = nativeCreateDataChannel.apply(this, arguments); - return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { - dataChannel - }), dataChannel; - }; - const maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); - if (codec !== "default" || maxVideoBitrate > 0) { - const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; - RTCPeerConnection.prototype.setLocalDescription = function(description) { - if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); - try { - if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); - } catch (e) { - BxLogger.error("setLocalDescription", e); - } - return nativeSetLocalDescription.apply(this, arguments); - }; - } - const OrgRTCPeerConnection = window.RTCPeerConnection; - window.RTCPeerConnection = function() { - const conn = new OrgRTCPeerConnection; - return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { - BxLogger.info("connectionstatechange", conn.connectionState); - }), conn; + const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; + RTCPeerConnection.prototype.createDataChannel = function() { + const dataChannel = nativeCreateDataChannel.apply(this, arguments); + return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { + dataChannel + }), dataChannel; + }; + const maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); + if (codec !== "default" || maxVideoBitrate > 0) { + const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; + RTCPeerConnection.prototype.setLocalDescription = function(description) { + if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); + try { + if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + } catch (e) { + BxLogger.error("setLocalDescription", e); + } + return nativeSetLocalDescription.apply(this, arguments); }; + } + const OrgRTCPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function() { + const conn = new OrgRTCPeerConnection; + return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { + BxLogger.info("connectionstatechange", conn.connectionState); + }), conn; + }; } function patchAudioContext() { - const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; - window.AudioContext = function(options) { - if (options && options.latencyHint) options.latencyHint = 0; - const ctx = new OrgAudioContext(options); - return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { - const gainNode = nativeCreateGain.apply(this); - return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; - }, STATES.currentStream.audioContext = ctx, ctx; - }; + const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; + window.AudioContext = function(options) { + if (options && options.latencyHint) options.latencyHint = 0; + const ctx = new OrgAudioContext(options); + return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { + const gainNode = nativeCreateGain.apply(this); + return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + }, STATES.currentStream.audioContext = ctx, ctx; + }; } function patchMeControl() { - const 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); + const 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() { - window.adobe = Object.freeze({}); + window.adobe = Object.freeze({}); } function patchCanvasContext() { - const 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]); - }; + const 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]); + }; } class ProductDetailsPage { - static $btnShortcut = AppInterface && createButton({ - icon: BxIcon.CREATE_SHORTCUT, - label: t("create-shortcut"), - style: 32, - tabIndex: 0, - onClick: (e) => { - AppInterface.createShortcut(window.location.pathname.substring(6)); - } - }); - static $btnWallpaper = AppInterface && createButton({ - icon: BxIcon.DOWNLOAD, - label: t("wallpaper"), - style: 32, - tabIndex: 0, - onClick: async (e) => { - try { - const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); - if (!matches?.groups) return; - const titleSlug = matches.groups.titleSlug.replaceAll("%" + "7C", "-"), productId = matches.groups.productId; - AppInterface.downloadWallpapers(titleSlug, productId); - } catch (e2) {} - } - }); - static injectTimeoutId = null; - static injectButtons() { - if (!AppInterface) return; - ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { - const $container = document.querySelector("div[class*=ActionButtons-module__container]"); - if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { - class: "bx-product-details-buttons" - }, BX_FLAGS.DeviceInfo.deviceType === "android" && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper)); - }, 500); + static $btnShortcut = AppInterface && createButton({ + icon: BxIcon.CREATE_SHORTCUT, + label: t("create-shortcut"), + style: 32, + tabIndex: 0, + onClick: (e) => { + AppInterface.createShortcut(window.location.pathname.substring(6)); } + }); + static $btnWallpaper = AppInterface && createButton({ + icon: BxIcon.DOWNLOAD, + label: t("wallpaper"), + style: 32, + tabIndex: 0, + onClick: async (e) => { + try { + const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); + if (!matches?.groups) return; + const titleSlug = matches.groups.titleSlug.replaceAll("%" + "7C", "-"), productId = matches.groups.productId; + AppInterface.downloadWallpapers(titleSlug, productId); + } catch (e2) {} + } + }); + static injectTimeoutId = null; + static injectButtons() { + if (!AppInterface) return; + ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { + const $container = document.querySelector("div[class*=ActionButtons-module__container]"); + if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { + class: "bx-product-details-buttons" + }, BX_FLAGS.DeviceInfo.deviceType === "android" && 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; - const $container = $btnOrg.cloneNode(!0); - let timeout; - if (STATES.browser.capabilities.touch) { - const onTransitionStart = (e) => { - if (e.propertyName !== "opacity") return; - timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none"; - }, onTransitionEnd = (e) => { - if (e.propertyName !== "opacity") return; - const $streamHud = e.target.closest("#StreamHud"); - if (!$streamHud) return; - if ($streamHud.style.left === "0px") { - const $target = e.target; - timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { - $target.style.pointerEvents = "auto"; - }, 100); - } - }; - $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); + static $btnStreamSettings; + static $btnStreamStats; + static $btnRefresh; + static $btnHome; + static observer; + static cloneStreamHudButton($btnOrg, label, svgIcon) { + if (!$btnOrg) return null; + const $container = $btnOrg.cloneNode(!0); + let timeout; + if (STATES.browser.capabilities.touch) { + const onTransitionStart = (e) => { + if (e.propertyName !== "opacity") return; + timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none"; + }, onTransitionEnd = (e) => { + if (e.propertyName !== "opacity") return; + const $streamHud = e.target.closest("#StreamHud"); + if (!$streamHud) return; + if ($streamHud.style.left === "0px") { + const $target = e.target; + timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { + $target.style.pointerEvents = "auto"; + }, 100); } - const $button = $container.querySelector("button"); - if (!$button) return null; - $button.setAttribute("title", label); - const $orgSvg = $button.querySelector("svg"); - if (!$orgSvg) return null; - const $svg = createSvgIcon(svgIcon); - return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container; + }; + $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); } - static cloneCloseButton($btnOrg, icon, className, onChange) { - if (!$btnOrg) return null; - const $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; + const $button = $container.querySelector("button"); + if (!$button) return null; + $button.setAttribute("title", label); + const $orgSvg = $button.querySelector("svg"); + if (!$orgSvg) return null; + const $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; + const $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() { + const $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) { + const $orgButton = $streamHud.querySelector("div[class^=HUDButton]"); + if (!$orgButton) return; + const hideGripHandle = () => { + const $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(); + }; + let $btnStreamSettings = StreamUiHandler.$btnStreamSettings; + if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); + }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; + const streamStats = StreamStats.getInstance(); + let $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(); + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn); + }), StreamUiHandler.$btnStreamStats = $btnStreamStats; + const $btnParent = $orgButton.parentElement; + if ($btnStreamSettings && $btnStreamStats) { + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); } - static async handleStreamMenu() { - const $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) { - const $orgButton = $streamHud.querySelector("div[class^=HUDButton]"); - if (!$orgButton) return; - const hideGripHandle = () => { - const $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(); - }; - let $btnStreamSettings = StreamUiHandler.$btnStreamSettings; - if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); - }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; - const streamStats = StreamStats.getInstance(); - let $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(); - const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); - $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn); - }), StreamUiHandler.$btnStreamStats = $btnStreamStats; - const $btnParent = $orgButton.parentElement; - if ($btnStreamSettings && $btnStreamStats) { - const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); - $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); - } - const $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(); - const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); - if (!$screen) return; - const observer = new MutationObserver((mutationList) => { - mutationList.forEach((item) => { - if (item.type !== "childList") return; - item.addedNodes.forEach(async ($node) => { - if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; - let $elm = $node; - if (!($elm instanceof HTMLElement)) return; - const className = $elm.className || ""; - if (className.includes("PureErrorPage")) { - BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); - 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); - }); - }); + const $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(); + const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); + if (!$screen) return; + const observer = new MutationObserver((mutationList) => { + mutationList.forEach((item) => { + if (item.type !== "childList") return; + item.addedNodes.forEach(async ($node) => { + if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; + let $elm = $node; + if (!($elm instanceof HTMLElement)) return; + const className = $elm.className || ""; + if (className.includes("PureErrorPage")) { + BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); + 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; - } + }); + }); + observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer; + } } class XboxApi { - static CACHED_TITLES = {}; - static async getProductTitle(xboxTitleId) { - if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; - try { - const 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 null; - } + static CACHED_TITLES = {}; + static async getProductTitle(xboxTitleId) { + if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; + try { + const 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 null; + } } function unload() { - if (!STATES.isPlaying) return; - STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(); + if (!STATES.isPlaying) return; + STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(); } function observeRootDialog($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) { - const $addedElm = mutation.addedNodes[0]; - if ($addedElm instanceof HTMLElement && $addedElm.className) { - if ($root.querySelector("div[class*=GuideDialog]")) GuideMenu.observe($addedElm); - } - } - const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); - if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); + 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) { + const $addedElm = mutation.addedNodes[0]; + if ($addedElm instanceof HTMLElement && $addedElm.className) { + if ($root.querySelector("div[class*=GuideDialog]")) GuideMenu.observe($addedElm); } - }).observe($root, { subtree: !0, childList: !0 }); + } + const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); + if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); + } + }).observe($root, { subtree: !0, childList: !0 }); } function waitForRootDialog() { - const observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - if (mutation.type !== "childList") continue; - const $target = mutation.target; - if ($target.id && $target.id === "gamepass-dialog-root") { - observer.disconnect(), observeRootDialog($target); - break; - } - } - }); - observer.observe(document.documentElement, { subtree: !0, childList: !0 }); + const observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + const $target = mutation.target; + if ($target.id && $target.id === "gamepass-dialog-root") { + observer.disconnect(), observeRootDialog($target); + break; + } + } + }); + observer.observe(document.documentElement, { subtree: !0, childList: !0 }); } function main() { - if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); - if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); + if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); + if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } if (window.location.pathname.includes("/auth/msa")) { - const nativePushState = window.history.pushState; - throw window.history.pushState = function(...args) { - const 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"); + const nativePushState = window.history.pushState; + throw window.history.pushState = function(...args) { + const 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); window.addEventListener("load", (e) => { - window.setTimeout(() => { - if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0); - }, 3000); + 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) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); - else window.setTimeout(HeaderSection.watchHeader, 2000); - if (getPref("ui_hide_sections").includes("friends")) { - const $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); - $parent && ($parent.style.display = "none"); - } - preloadFonts(); + if (document.readyState !== "interactive") return; + if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); + else window.setTimeout(HeaderSection.watchHeader, 2000); + if (getPref("ui_hide_sections").includes("friends")) { + const $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); @@ -5635,46 +5607,46 @@ window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, (e) => { - if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); + if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); }, { once: !0 }); window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { - STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); + STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); }); window.addEventListener(BxEvent.STREAM_LOADING, (e) => { - if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); - else STATES.currentStream.titleSlug = "remote-play"; + if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); + else STATES.currentStream.titleSlug = "remote-play"; }); getPref("ui_loading_screen_game_art") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { - if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); + if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); }); window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); + STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer(); }); window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { - BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { - if (e.component === "product-details") ProductDetailsPage.injectButtons(); + if (e.component === "product-details") ProductDetailsPage.injectButtons(); }); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "message") return; - dataChannel.addEventListener("message", async (msg) => { - if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; - if (msg.data.includes("/titleinfo")) { - const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); - if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.remotePlay.isPlaying) { - if (STATES.currentStream.titleSlug = "remote-play", json.focused) { - const productTitle = await XboxApi.getProductTitle(xboxTitleId); - if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle); - } - } + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") return; + dataChannel.addEventListener("message", async (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); + if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.remotePlay.isPlaying) { + if (STATES.currentStream.titleSlug = "remote-play", json.focused) { + const productTitle = await XboxApi.getProductTitle(xboxTitleId); + if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle); } - }); + } + } + }); }); window.addEventListener(BxEvent.STREAM_STOPPED, unload); window.addEventListener("pagehide", (e) => { - BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); main(); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index 05a9476..3564aed 100644 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Better xCloud // @namespace https://github.com/redphx -// @version 5.8.4 +// @version 5.8.5-beta // @description Improve Xbox Cloud Gaming (xCloud) experience // @author redphx // @license MIT @@ -14,3922 +14,3903 @@ // ==/UserScript== "use strict"; class BxLogger { - static info = (tag, ...args) => BxLogger.log("#008746", tag, ...args); - static warning = (tag, ...args) => 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); - } + static info = (tag, ...args) => BxLogger.log("#008746", tag, ...args); + static warning = (tag, ...args) => 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" - } + 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; + 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 SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "123.0.0.0"; if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) { - const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); - if (match) CHROMIUM_VERSION = match[1]; + const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/); + if (match) CHROMIUM_VERSION = match[1]; } class UserAgent { - static STORAGE_KEY = "better_xcloud_user_agent"; - 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} SmartTV`, - "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) { - const 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) { - const 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; - const userAgent = UserAgent.getDefault().toLowerCase(); - let result = userAgent.includes("safari") && !userAgent.includes("chrom"); - return this.#isSafari = result, result; - } - static isSafariMobile() { - if (this.#isSafariMobile !== null) return this.#isSafariMobile; - const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); - return this.#isSafariMobile = result, result; - } - static isMobile() { - if (this.#isMobile !== null) return this.#isMobile; - const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); - return this.#isMobile = result, result; - } - static spoof() { - const 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 - }); + static STORAGE_KEY = "better_xcloud_user_agent"; + 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} SmartTV`, + "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) { + const 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) { + const 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; + const userAgent = UserAgent.getDefault().toLowerCase(); + let result = userAgent.includes("safari") && !userAgent.includes("chrom"); + return this.#isSafari = result, result; + } + static isSafariMobile() { + if (this.#isSafariMobile !== null) return this.#isSafariMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile"); + return this.#isSafariMobile = result, result; + } + static isMobile() { + if (this.#isMobile !== null) return this.#isMobile; + const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent); + return this.#isMobile = result, result; + } + static spoof() { + const 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 + }); + } } function deepClone(obj) { - if ("structuredClone" in window) return structuredClone(obj); - if (!obj) return {}; - return JSON.parse(JSON.stringify(obj)); + if ("structuredClone" in window) return structuredClone(obj); + if (!obj) return {}; + return JSON.parse(JSON.stringify(obj)); } -var SCRIPT_VERSION = "5.8.4", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; +var SCRIPT_VERSION = "5.8.5-beta", 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, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { - supportedRegion: !0, - serverRegions: {}, - selectedRegion: {}, - gsToken: "", - isSignedIn: !1, - isPlaying: !1, - appContext: {}, - browser: { - capabilities: { - touch: browserHasTouchSupport, - batteryApi: "getBattery" in window.navigator - } - }, - userAgent: { - isTv, - capabilities: { - touch: userAgentHasTouchSupport, - mkb: supportMkb - } - }, - currentStream: {}, - remotePlay: {}, - pointerServerPort: 9269 + supportedRegion: !0, + serverRegions: {}, + selectedRegion: {}, + gsToken: "", + isSignedIn: !1, + isPlaying: !1, + appContext: {}, + browser: { + capabilities: { + touch: browserHasTouchSupport, + batteryApi: "getBattery" in window.navigator + } + }, + userAgent: { + isTv, + capabilities: { + touch: userAgentHasTouchSupport, + mkb: supportMkb + } + }, + currentStream: {}, + remotePlay: {}, + pointerServerPort: 9269 }, STORAGE = {}; var BxEvent; ((BxEvent) => { - BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", 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.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", 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_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", 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; - } - const 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", eventName, data); + BxEvent.JUMP_BACK_IN_READY = "bx-jump-back-in-ready", BxEvent.POPSTATE = "bx-popstate", BxEvent.TITLE_INFO_READY = "bx-title-info-ready", BxEvent.SETTINGS_CHANGED = "bx-settings-changed", BxEvent.STREAM_LOADING = "bx-stream-loading", BxEvent.STREAM_STARTING = "bx-stream-starting", BxEvent.STREAM_STARTED = "bx-stream-started", BxEvent.STREAM_PLAYING = "bx-stream-playing", BxEvent.STREAM_STOPPED = "bx-stream-stopped", BxEvent.STREAM_ERROR_PAGE = "bx-stream-error-page", BxEvent.STREAM_WEBRTC_CONNECTED = "bx-stream-webrtc-connected", BxEvent.STREAM_WEBRTC_DISCONNECTED = "bx-stream-webrtc-disconnected", 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.XCLOUD_SERVERS_READY = "bx-servers-ready", BxEvent.XCLOUD_SERVERS_UNAVAILABLE = "bx-servers-unavailable", BxEvent.DATA_CHANNEL_CREATED = "bx-data-channel-created", BxEvent.GAME_BAR_ACTION_ACTIVATED = "bx-game-bar-action-activated", BxEvent.MICROPHONE_STATE_CHANGED = "bx-microphone-state-changed", BxEvent.SPEAKER_STATE_CHANGED = "bx-speaker-state-changed", 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_DIALOG_SHOWN = "bx-xcloud-dialog-shown", BxEvent.XCLOUD_DIALOG_DISMISSED = "bx-xcloud-dialog-dismissed", 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; } - BxEvent.dispatch = dispatch; + const 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", eventName, data); + } + BxEvent.dispatch = dispatch; })(BxEvent ||= {}); window.BxEvent = BxEvent; class NavigationUtils { - static setNearby($elm, nearby) { - $elm.nearby = $elm.nearby || {}; - let key; - for (key in nearby) - $elm.nearby[key] = nearby[key]; - } + static setNearby($elm, nearby) { + $elm.nearby = $elm.nearby || {}; + let key; + for (key in nearby) + $elm.nearby[key] = nearby[key]; + } } var setNearby = NavigationUtils.setNearby; function createElement(elmName, props = {}, ..._) { - let $elm; - const hasNs = "xmlns" in props; - if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; - else $elm = document.createElement(elmName); - if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; - for (let key in props) { - if ($elm.hasOwnProperty(key)) continue; - if (hasNs) $elm.setAttributeNS(null, key, props[key]); - else if (key === "on") for (let eventName in props[key]) - $elm.addEventListener(eventName, props[key][eventName]); - else $elm.setAttribute(key, props[key]); - } - for (let i = 2, size = arguments.length;i < size; i++) { - const arg = arguments[i]; - if (arg instanceof Node) $elm.appendChild(arg); - else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); - } - return $elm; + let $elm; + const hasNs = "xmlns" in props; + if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns; + else $elm = document.createElement(elmName); + if (props._nearby) setNearby($elm, props._nearby), delete props._nearby; + for (let key in props) { + if ($elm.hasOwnProperty(key)) continue; + if (hasNs) $elm.setAttributeNS(null, key, props[key]); + else if (key === "on") for (let eventName in props[key]) + $elm.addEventListener(eventName, props[key][eventName]); + else $elm.setAttribute(key, props[key]); + } + for (let i = 2, size = arguments.length;i < size; i++) { + const arg = arguments[i]; + if (arg instanceof Node) $elm.appendChild(arg); + else if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.appendChild(document.createTextNode(arg)); + } + return $elm; } function getReactProps($elm) { - for (let key in $elm) - if (key.startsWith("__reactProps")) return $elm[key]; - return null; + for (let key in $elm) + if (key.startsWith("__reactProps")) return $elm[key]; + return null; } function escapeHtml(html) { - const text = document.createTextNode(html), $span = document.createElement("span"); - return $span.appendChild(text), $span.innerHTML; + const text = document.createTextNode(html), $span = document.createElement("span"); + return $span.appendChild(text), $span.innerHTML; } function isElementVisible($elm) { - const rect = $elm.getBoundingClientRect(); - return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; + const rect = $elm.getBoundingClientRect(); + return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height; } function removeChildElements($parent) { - while ($parent.firstElementChild) - $parent.firstElementChild.remove(); + while ($parent.firstElementChild) + $parent.firstElementChild.remove(); } function clearFocus() { - if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); + if (document.activeElement instanceof HTMLElement) document.activeElement.blur(); } function clearDataSet($elm) { - Object.keys($elm.dataset).forEach((key) => { - delete $elm.dataset[key]; - }); + Object.keys($elm.dataset).forEach((key) => { + delete $elm.dataset[key]; + }); } function humanFileSize(size) { - const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); - return (size / Math.pow(1024, i)).toFixed(2) + " " + FILE_SIZE_UNITS[i]; + const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); + return (size / Math.pow(1024, i)).toFixed(2) + " " + 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; - const output = []; - return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); + let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1; + if (m === 60) h += 1, m = 0; + const output = []; + return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" "); } function secondsToHms(seconds) { - let h = Math.floor(seconds / 3600); - seconds %= 3600; - let m = Math.floor(seconds / 60), s = seconds % 60; - const output = []; - if (h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), s > 0 || output.length === 0) output.push(`${s}s`); - return output.join(" "); + let h = Math.floor(seconds / 3600); + seconds %= 3600; + let m = Math.floor(seconds / 60), s = seconds % 60; + const output = []; + if (h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), s > 0 || output.length === 0) output.push(`${s}s`); + return output.join(" "); } var ButtonStyleClass = { - 1: "bx-primary", - 2: "bx-danger", - 4: "bx-ghost", - 8: "bx-frosted", - 16: "bx-drop-shadow", - 32: "bx-focusable", - 64: "bx-full-width", - 128: "bx-full-height", - 256: "bx-tall", - 512: "bx-circular", - 1024: "bx-normal-case", - 2048: "bx-normal-link" + 1: "bx-primary", + 2: "bx-danger", + 4: "bx-ghost", + 8: "bx-frosted", + 16: "bx-drop-shadow", + 32: "bx-focusable", + 64: "bx-full-width", + 128: "bx-full-height", + 256: "bx-tall", + 512: "bx-circular", + 1024: "bx-normal-case", + 2048: "bx-normal-link" }, CE = createElement, svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement, createSvgIcon = (icon) => { - return svgParser(icon.toString()); + return svgParser(icon.toString()); }, ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i)), createButton = (options) => { - let $btn; - if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; - else $btn = CE("button", { class: "bx-button", type: "button" }); - const style = options.style || 0; - style && ButtonStyleIndices.forEach((index) => { - style & index && $btn.classList.add(ButtonStyleClass[index]); - }), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; - for (let key in options.attributes) - if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); - return $btn; + let $btn; + if (options.url) $btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank"; + else $btn = CE("button", { class: "bx-button", type: "button" }); + const style = options.style || 0; + style && ButtonStyleIndices.forEach((index) => { + style & index && $btn.classList.add(ButtonStyleClass[index]); + }), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0; + for (let key in options.attributes) + if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]); + return $btn; }, CTN = document.createTextNode.bind(document); window.BX_CE = createElement; var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"]; 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": "中文(繁體)" + "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 = { - activate: "Activate", - activated: "Activated", - active: "Active", - advanced: "Advanced", - "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", - "back-to-home": "Back to home", - "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", - battery: "Battery", - "battery-saving": "Battery saving", - "better-xcloud": "Better xCloud", - "bitrate-audio-maximum": "Maximum audio bitrate", - "bitrate-video-maximum": "Maximum video bitrate", - "bottom-left": "Bottom-left", - "bottom-right": "Bottom-right", - brazil: "Brazil", - brightness: "Brightness", - "browser-unsupported-feature": "Your browser doesn't support this feature", - "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", - "clarity-boost": "Clarity boost", - "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", - clear: "Clear", - 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", - contrast: "Contrast", - controller: "Controller", - "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", - delete: "Delete", - 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-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", - "fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile", - "fortnite-force-console-version": "Fortnite: force console version", - "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", - import: "Import", - increase: "Increase", - "install-android": "Better xCloud app for Android", - japan: "Japan", - jitter: "Jitter", - "keyboard-shortcuts": "Keyboard shortcuts", - korea: "Korea", - language: "Language", - large: "Large", - layout: "Layout", - "left-stick": "Left stick", - "load-failed-message": "Failed to run Better xCloud", - "loading-screen": "Loading screen", - "local-co-op": "Local co-op", - "lowest-quality": "Lowest quality", - "map-mouse-to": "Map mouse to", - "max-fps": "Max FPS", - "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": "Using this feature when playing online could be viewed as cheating", - "mouse-and-keyboard": "Mouse & Keyboard", - "mouse-wheel": "Mouse wheel", - "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", - muted: "Muted", - name: "Name", - "native-mkb": "Native Mouse & Keyboard", - new: "New", - "new-version-available": [ - e => `Version ${e.version} available`, - , - , - e => `Version ${e.version} verfügbar`, - , - 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} 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", - normal: "Normal", - off: "Off", - on: "On", - "only-supports-some-games": "Only supports some games", - opacity: "Opacity", - other: "Other", - playing: "Playing", - playtime: "Playtime", - poland: "Poland", - 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-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 => `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", - "right-click-to-unbind": "Right-click on a key to unbind it", - "right-stick": "Right stick", - "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", - settings: "Settings", - "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", - 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", - 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-games": "All games", - "tc-all-white": "All white", - "tc-auto-off": "Off when controller found", - "tc-availability": "Availability", - "tc-custom-layout-style": "Custom layout's button style", - "tc-default-opacity": "Default opacity", - "tc-muted-colors": "Muted colors", - "tc-standard-layout-style": "Standard layout's button style", - "text-size": "Text size", - toggle: "Toggle", - "top-center": "Top-center", - "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", - "transparent-background": "Transparent background", - "true-achievements": "TrueAchievements", - ui: "UI", - "unexpected-behavior": "May cause unexpected behavior", - "united-states": "United States", - unknown: "Unknown", - unlimited: "Unlimited", - unmuted: "Unmuted", - "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", - "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", - wallpaper: "Wallpaper", - webgl2: "WebGL2" + activate: "Activate", + activated: "Activated", + active: "Active", + advanced: "Advanced", + "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", + "back-to-home": "Back to home", + "back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?", + battery: "Battery", + "battery-saving": "Battery saving", + "better-xcloud": "Better xCloud", + "bitrate-audio-maximum": "Maximum audio bitrate", + "bitrate-video-maximum": "Maximum video bitrate", + "bottom-left": "Bottom-left", + "bottom-right": "Bottom-right", + brazil: "Brazil", + brightness: "Brightness", + "browser-unsupported-feature": "Your browser doesn't support this feature", + "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", + "clarity-boost": "Clarity boost", + "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON", + clear: "Clear", + 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", + contrast: "Contrast", + controller: "Controller", + "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", + delete: "Delete", + 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-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", + "fortnite-allow-stw-mode": "Allows playing \"Save the World\" mode on mobile", + "fortnite-force-console-version": "Fortnite: force console version", + "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", + import: "Import", + increase: "Increase", + "install-android": "Better xCloud app for Android", + japan: "Japan", + jitter: "Jitter", + "keyboard-shortcuts": "Keyboard shortcuts", + korea: "Korea", + language: "Language", + large: "Large", + layout: "Layout", + "left-stick": "Left stick", + "load-failed-message": "Failed to run Better xCloud", + "loading-screen": "Loading screen", + "local-co-op": "Local co-op", + "lowest-quality": "Lowest quality", + "map-mouse-to": "Map mouse to", + "max-fps": "Max FPS", + "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": "Using this feature when playing online could be viewed as cheating", + "mouse-and-keyboard": "Mouse & Keyboard", + "mouse-wheel": "Mouse wheel", + "msfs2020-force-native-mkb": "MSFS2020: force native M&KB support", + muted: "Muted", + name: "Name", + "native-mkb": "Native Mouse & Keyboard", + new: "New", + "new-version-available": [ + e => `Version ${e.version} available`, + , + , + e => `Version ${e.version} verfügbar`, + , + 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} 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", + normal: "Normal", + off: "Off", + on: "On", + "only-supports-some-games": "Only supports some games", + opacity: "Opacity", + other: "Other", + playing: "Playing", + playtime: "Playtime", + poland: "Poland", + 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-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 => `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", + "right-click-to-unbind": "Right-click on a key to unbind it", + "right-stick": "Right stick", + "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", + settings: "Settings", + "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", + 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", + 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-games": "All games", + "tc-all-white": "All white", + "tc-auto-off": "Off when controller found", + "tc-availability": "Availability", + "tc-custom-layout-style": "Custom layout's button style", + "tc-default-opacity": "Default opacity", + "tc-muted-colors": "Muted colors", + "tc-standard-layout-style": "Standard layout's button style", + "text-size": "Text size", + toggle: "Toggle", + "top-center": "Top-center", + "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", + "transparent-background": "Transparent background", + "true-achievements": "TrueAchievements", + ui: "UI", + "unexpected-behavior": "May cause unexpected behavior", + "united-states": "United States", + unknown: "Unknown", + unlimited: "Unlimited", + unmuted: "Unmuted", + "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", + "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", + wallpaper: "Wallpaper", + webgl2: "WebGL2" }; class Translations { - static #EN_US = "en-US"; - static #KEY_LOCALE = "better_xcloud_locale"; - static #KEY_TRANSLATIONS = "better_xcloud_translations"; - static #enUsIndex = -1; - static #selectedLocaleIndex = -1; - static #selectedLocale = "en-US"; - static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); - static #foreignTranslations = {}; - static async init() { - Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshLocale(), await Translations.#loadTranslations(); + static #EN_US = "en-US"; + static #KEY_LOCALE = "better_xcloud_locale"; + static #KEY_TRANSLATIONS = "better_xcloud_translations"; + static #enUsIndex = -1; + static #selectedLocaleIndex = -1; + static #selectedLocale = "en-US"; + static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES); + static #foreignTranslations = {}; + static async init() { + Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), 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); + const 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); } - static refreshLocale(newLocale) { - let locale; - if (newLocale) localStorage.setItem(Translations.#KEY_LOCALE, newLocale), locale = newLocale; - else locale = localStorage.getItem(Translations.#KEY_LOCALE); - const 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); + 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; } - 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 { - const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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); + if (async) Translations.downloadTranslationsAsync(Translations.#selectedLocale); + else await Translations.downloadTranslations(Translations.#selectedLocale); + } + static async downloadTranslations(locale) { + try { + const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/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(); var BypassServers = { - br: t("brazil"), - jp: t("japan"), - kr: t("korea"), - pl: t("poland"), - us: t("united-states") + 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" + 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 SettingElement { - static #renderOptions(key, setting, currentValue, onChange) { - const $control = CE("select", { - tabindex: 0 + static #renderOptions(key, setting, currentValue, onChange) { + const $control = CE("select", { + tabindex: 0 + }); + let $parent; + if (setting.optionsGroup) $parent = CE("optgroup", { + label: setting.optionsGroup + }), $control.appendChild($parent); + else $parent = $control; + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $parent.appendChild($option); + } + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const 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 = {}) { + const $control = CE("select", { + multiple: !0, + tabindex: 0 + }); + if (params && params.size) $control.setAttribute("size", params.size.toString()); + for (let value in setting.multipleOptions) { + const label = setting.multipleOptions[value], $option = CE("option", { value }, label); + $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { + e.preventDefault(); + const target = e.target; + target.selected = !target.selected; + const $parent = target.parentElement; + $parent.focus(), BxEvent.dispatch($parent, "input"); + }), $control.appendChild($option); + } + return $control.addEventListener("mousedown", function(e) { + const self = this, orgScrollTop = self.scrollTop; + window.setTimeout(() => self.scrollTop = orgScrollTop, 0); + }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { + const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); + !e.ignoreOnChange && onChange(e, values); + }), $control; + } + static #renderNumber(key, setting, currentValue, onChange) { + const $control = CE("input", { + tabindex: 0, + type: "number", + min: setting.min, + max: setting.max + }); + return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { + const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); + target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); + }), $control; + } + static #renderCheckbox(key, setting, currentValue, onChange) { + const $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 = {}) { + options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; + let $text, $btnDec, $btnInc, $range = null, controlValue = value; + const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => { + value2 = parseInt(value2); + let textContent = null; + if (options.customTextValue) textContent = options.customTextValue(value2); + if (textContent === null) textContent = value2.toString() + options.suffix; + return textContent; + }, updateButtonsVisibility = () => { + $btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX); + }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { + "data-type": "dec", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { + "data-type": "inc", + type: "button", + class: options.hideSlider ? "bx-focusable" : "", + tabindex: options.hideSlider ? 0 : -1 + }, "+")); + if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; + if ($range = CE("input", { + id: `bx_setting_${key}`, + type: "range", + min: MIN, + max: MAX, + value, + step: STEPS, + tabindex: 0 + }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { + if (value = parseInt(e.target.value), controlValue === value) return; + controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); + }), $wrapper.addEventListener("input", (e) => { + BxEvent.dispatch($range, "input"); + }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { + const 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: i })); + } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) + $markers.appendChild(CE("option", { value: i })); + $wrapper.appendChild($markers); + } + updateButtonsVisibility(); + let interval, isHolding = !1; + const onClick = (e) => { + if (isHolding) { + e.preventDefault(), isHolding = !1; + return; + } + const $btn = e.target; + let value2 = parseInt(controlValue); + if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); + else value2 = Math.min(MAX, value2 + STEPS); + controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2); + }, onMouseDown = (e) => { + e.preventDefault(), isHolding = !0; + const args = arguments; + interval && clearInterval(interval), interval = window.setInterval(() => { + e.target && BxEvent.dispatch(e.target, "click", { + arguments: args }); - let $parent; - if (setting.optionsGroup) $parent = CE("optgroup", { - label: setting.optionsGroup - }), $control.appendChild($parent); - else $parent = $control; - for (let value in setting.options) { - const label = setting.options[value], $option = CE("option", { value }, label); - $parent.appendChild($option); - } - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - const 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 = {}) { - const $control = CE("select", { - multiple: !0, - tabindex: 0 - }); - if (params && params.size) $control.setAttribute("size", params.size.toString()); - for (let value in setting.multipleOptions) { - const label = setting.multipleOptions[value], $option = CE("option", { value }, label); - $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) { - e.preventDefault(); - const target = e.target; - target.selected = !target.selected; - const $parent = target.parentElement; - $parent.focus(), BxEvent.dispatch($parent, "input"); - }), $control.appendChild($option); - } - return $control.addEventListener("mousedown", function(e) { - const self = this, orgScrollTop = self.scrollTop; - window.setTimeout(() => self.scrollTop = orgScrollTop, 0); - }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => { - const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value); - !e.ignoreOnChange && onChange(e, values); - }), $control; - } - static #renderNumber(key, setting, currentValue, onChange) { - const $control = CE("input", { - tabindex: 0, - type: "number", - min: setting.min, - max: setting.max - }); - return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => { - const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value))); - target.value = value.toString(), !e.ignoreOnChange && onChange(e, value); - }), $control; - } - static #renderCheckbox(key, setting, currentValue, onChange) { - const $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 = {}) { - options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider; - let $text, $btnDec, $btnInc, $range = null, controlValue = value; - const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => { - value2 = parseInt(value2); - let textContent = null; - if (options.customTextValue) textContent = options.customTextValue(value2); - if (textContent === null) textContent = value2.toString() + options.suffix; - return textContent; - }, updateButtonsVisibility = () => { - $btnDec.classList.toggle("bx-inactive", controlValue === MIN), $btnInc.classList.toggle("bx-inactive", controlValue === MAX); - }, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", { - "data-type": "dec", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", { - "data-type": "inc", - type: "button", - class: options.hideSlider ? "bx-focusable" : "", - tabindex: options.hideSlider ? 0 : -1 - }, "+")); - if (options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), $wrapper.disabled = !0, $wrapper; - if ($range = CE("input", { - id: `bx_setting_${key}`, - type: "range", - min: MIN, - max: MAX, - value, - step: STEPS, - tabindex: 0 - }), options.hideSlider && $range.classList.add("bx-gone"), $range.addEventListener("input", (e) => { - if (value = parseInt(e.target.value), controlValue === value) return; - controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value); - }), $wrapper.addEventListener("input", (e) => { - BxEvent.dispatch($range, "input"); - }), $wrapper.appendChild($range), options.ticks || options.exactTicks) { - const 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: i })); - } else for (let i = MIN + options.ticks;i < MAX; i += options.ticks) - $markers.appendChild(CE("option", { value: i })); - $wrapper.appendChild($markers); - } - updateButtonsVisibility(); - let interval, isHolding = !1; - const onClick = (e) => { - if (isHolding) { - e.preventDefault(), isHolding = !1; - return; - } - const $btn = e.target; - let value2 = parseInt(controlValue); - if ($btn.dataset.type === "dec") value2 = Math.max(MIN, value2 - STEPS); - else value2 = Math.min(MAX, value2 + STEPS); - controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, !e.ignoreOnChange && onChange && onChange(e, value2); - }, onMouseDown = (e) => { - e.preventDefault(), isHolding = !0; - const args = arguments; - interval && clearInterval(interval), interval = window.setInterval(() => { - e.target && BxEvent.dispatch(e.target, "click", { - arguments: args - }); - }, 200); - }, onMouseUp = (e) => { - e.preventDefault(), interval && clearInterval(interval), isHolding = !1; - }, onContextMenu = (e) => e.preventDefault(); - return $wrapper.setValue = (value2) => { - $text.textContent = renderTextValue(value2), $range.value = value2; - }, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { - focus: $range || $btnInc - }), $wrapper; - } - static #METHOD_MAP = { - options: SettingElement.#renderOptions, - "multiple-options": SettingElement.#renderMultipleOptions, - number: SettingElement.#renderNumber, - "number-stepper": SettingElement.#renderNumberStepper, - checkbox: SettingElement.#renderCheckbox - }; - static render(type, key, setting, currentValue, onChange, options) { - const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); - if (type !== "number-stepper") $control.id = `bx_setting_${key}`; - if (type === "options" || type === "multiple-options") $control.name = $control.id; - return $control; - } - static fromPref(key, storage, onChange, overrideParams = {}) { - const definition = storage.getDefinition(key); - let currentValue = storage.getSetting(key), type; - if ("type" in definition) type = definition.type; - else if ("options" in definition) type = "options"; - else if ("multipleOptions" in definition) type = "multiple-options"; - else if (typeof definition.default === "number") type = "number"; - 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) => { - storage.setSetting(key, value), onChange && onChange(e, value); - }, params); - } + }, 200); + }, onMouseUp = (e) => { + e.preventDefault(), interval && clearInterval(interval), isHolding = !1; + }, onContextMenu = (e) => e.preventDefault(); + return $wrapper.setValue = (value2) => { + $text.textContent = renderTextValue(value2), $range.value = value2; + }, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), setNearby($wrapper, { + focus: $range || $btnInc + }), $wrapper; + } + static #METHOD_MAP = { + options: SettingElement.#renderOptions, + "multiple-options": SettingElement.#renderMultipleOptions, + number: SettingElement.#renderNumber, + "number-stepper": SettingElement.#renderNumberStepper, + checkbox: SettingElement.#renderCheckbox + }; + static render(type, key, setting, currentValue, onChange, options) { + const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1)); + if (type !== "number-stepper") $control.id = `bx_setting_${key}`; + if (type === "options" || type === "multiple-options") $control.name = $control.id; + return $control; + } + static fromPref(key, storage, onChange, overrideParams = {}) { + const definition = storage.getDefinition(key); + let currentValue = storage.getSetting(key), type; + if ("type" in definition) type = definition.type; + else if ("options" in definition) type = "options"; + else if ("multipleOptions" in definition) type = "multiple-options"; + else if (typeof definition.default === "number") type = "number"; + 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) => { + storage.setSetting(key, value), onChange && onChange(e, value); + }, params); + } } class BaseSettingsStore { - storage; - storageKey; - _settings; - definitions; - constructor(storageKey, definitions) { - this.storage = window.localStorage, this.storageKey = storageKey; - let settingId; - for (settingId in definitions) { - const setting = definitions[settingId]; - if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; - setting.ready && setting.ready.call(this, setting); - } - this.definitions = definitions, this._settings = null; + storage; + storageKey; + _settings; + definitions; + constructor(storageKey, definitions) { + this.storage = window.localStorage, this.storageKey = storageKey; + let settingId; + for (settingId in definitions) { + const setting = definitions[settingId]; + if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants]; + setting.ready && setting.ready.call(this, setting); } - get settings() { - if (this._settings) return this._settings; - const settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); - return this._settings = settings, settings; + this.definitions = definitions, this._settings = null; + } + get settings() { + if (this._settings) return this._settings; + const settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}"); + return this._settings = settings, settings; + } + getDefinition(key) { + if (!this.definitions[key]) { + const error = "Request invalid definition: " + key; + throw alert(error), Error(error); } - getDefinition(key) { - if (!this.definitions[key]) { - const error = "Request invalid definition: " + key; - throw alert(error), Error(error); - } - return this.definitions[key]; + return this.definitions[key]; + } + getSetting(key, checkUnsupported = !0) { + if (typeof key === "undefined") { + debugger; + return; } - getSetting(key, checkUnsupported = !0) { - if (typeof key === "undefined") { - debugger; - return; - } - const definition = this.definitions[key]; - if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; - if (checkUnsupported && definition.unsupported) return definition.default; - if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); - return this.settings[key]; - } - setSetting(key, value, emitEvent = !1) { - return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { - storageKey: this.storageKey, - settingKey: key, - settingValue: value - }), value; - } - saveSettings() { - this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); - } - validateValue(key, value) { - const def = this.definitions[key]; - if (!def) return value; - if (typeof value === "undefined" || value === null) value = def.default; - if ("min" in def) value = Math.max(def.min, value); - if ("max" in def) value = Math.min(def.max, value); - if ("options" in def && !(value in def.options)) value = def.default; - else if ("multipleOptions" in def) { - if (value.length) { - const validOptions = Object.keys(def.multipleOptions); - value.forEach((item2, idx) => { - validOptions.indexOf(item2) === -1 && value.splice(idx, 1); - }); - } - if (!value.length) value = def.default; - } - return value; - } - getLabel(key) { - return this.definitions[key].label || key; - } - getValueText(key, value) { - const definition = this.definitions[key]; - if (definition.type === "number-stepper") { - const params = definition.params; - if (params.customTextValue) { - const text = params.customTextValue(value); - if (text) return text; - } - return value.toString(); - } else if ("options" in definition) { - const options = definition.options; - if (value in options) return options[value]; - } else if (typeof value === "boolean") return value ? t("on") : t("off"); - return value.toString(); + const definition = this.definitions[key]; + if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default; + if (checkUnsupported && definition.unsupported) return definition.default; + if (!(key in this.settings)) this.settings[key] = this.validateValue(key, null); + return this.settings[key]; + } + setSetting(key, value, emitEvent = !1) { + return value = this.validateValue(key, value), this.settings[key] = value, this.saveSettings(), emitEvent && BxEvent.dispatch(window, BxEvent.SETTINGS_CHANGED, { + storageKey: this.storageKey, + settingKey: key, + settingValue: value + }), value; + } + saveSettings() { + this.storage.setItem(this.storageKey, JSON.stringify(this.settings)); + } + validateValue(key, value) { + const def = this.definitions[key]; + if (!def) return value; + if (typeof value === "undefined" || value === null) value = def.default; + if ("min" in def) value = Math.max(def.min, value); + if ("max" in def) value = Math.min(def.max, value); + if ("options" in def && !(value in def.options)) value = def.default; + else if ("multipleOptions" in def) { + if (value.length) { + const validOptions = Object.keys(def.multipleOptions); + value.forEach((item2, idx) => { + validOptions.indexOf(item2) === -1 && value.splice(idx, 1); + }); + } + if (!value.length) value = def.default; } + return value; + } + getLabel(key) { + return this.definitions[key].label || key; + } + getValueText(key, value) { + const definition = this.definitions[key]; + if (definition.type === "number-stepper") { + const params = definition.params; + if (params.customTextValue) { + const text = params.customTextValue(value); + if (text) return text; + } + return value.toString(); + } else if ("options" in definition) { + const options = definition.options; + if (value in options) return options[value]; + } else if (typeof value === "boolean") return value ? t("on") : t("off"); + return value.toString(); + } } class StreamStatsCollector { - static instance; - static getInstance() { - if (!StreamStatsCollector.instance) StreamStatsCollector.instance = new StreamStatsCollector; - return StreamStatsCollector.instance; - } - 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(); - } - }, - jit: { - current: 0, - grades: [30, 40, 60], - toString() { - return `${this.current.toFixed(2)}ms`; - } - }, - fps: { - current: 0, - toString() { - return this.current.toString(); - } - }, - btr: { - current: 0, - toString() { - return `${this.current.toFixed(2)} Mbps`; - } - }, - fl: { - received: 0, - dropped: 0, - toString() { - const framesDroppedPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); - return framesDroppedPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`; - } - }, - pl: { - received: 0, - dropped: 0, - toString() { - const packetsLostPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); - return packetsLostPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`; - } - }, - dt: { - current: 0, - total: 0, - grades: [6, 9, 12], - toString() { - return isNaN(this.current) ? "??ms" : `${this.current.toFixed(2)}ms`; - } - }, - dl: { - total: 0, - toString() { - return humanFileSize(this.total); - } - }, - 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) { - const 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 - }); - } + static instance; + static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new 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(); + } + }, + jit: { + current: 0, + grades: [30, 40, 60], + toString() { + return `${this.current.toFixed(2)}ms`; + } + }, + fps: { + current: 0, + toString() { + return this.current.toString(); + } + }, + btr: { + current: 0, + toString() { + return `${this.current.toFixed(2)} Mbps`; + } + }, + fl: { + received: 0, + dropped: 0, + toString() { + const framesDroppedPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); + return framesDroppedPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${framesDroppedPercentage}%)`; + } + }, + pl: { + received: 0, + dropped: 0, + toString() { + const packetsLostPercentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(2); + return packetsLostPercentage === "0.00" ? this.dropped.toString() : `${this.dropped} (${packetsLostPercentage}%)`; + } + }, + dt: { + current: 0, + total: 0, + grades: [6, 9, 12], + toString() { + return isNaN(this.current) ? "??ms" : `${this.current.toFixed(2)}ms`; + } + }, + dl: { + total: 0, + toString() { + return humanFileSize(this.total); + } + }, + 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) { + const diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : ""; + text += ` (${sign}${diffLevel}%)`; } - }; - lastVideoStat; - async collect() { - const stats = await STATES.currentStream.peerConnection?.getStats(); - if (!stats) return; - stats.forEach((stat) => { - if (stat.type === "inbound-rtp" && stat.kind === "video") { - const fps = this.currentStats.fps; - fps.current = stat.framesPerSecond || 0; - const pl = this.currentStats.pl; - pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; - const fl = this.currentStats.fl; - if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { - this.lastVideoStat = stat; - return; - } - const 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; - const btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; - btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; - const dt = this.currentStats.dt; - dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; - const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; - dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; - } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { - const ping = this.currentStats.ping; - ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; - const dl = this.currentStats.dl; - dl.total = stat.bytesReceived; - const ul = this.currentStats.ul; - ul.total = stat.bytesSent; - } - }); - let batteryLevel = 100, isCharging = !1; - if (STATES.browser.capabilities.batteryApi) try { - const bm = await navigator.getBattery(); - isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); - } catch (e) {} - const battery = this.currentStats.batt; - battery.current = batteryLevel, battery.isCharging = isCharging; - const playTime = this.currentStats.play, now = +new Date; - playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); - } - getStat(kind) { - return this.currentStats[kind]; - } - reset() { - const 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() { - window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - StreamStatsCollector.getInstance().reset(); + return text; + } + }, + time: { + toString() { + return (new Date()).toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: !1 }); + } } + }; + lastVideoStat; + async collect() { + const stats = await STATES.currentStream.peerConnection?.getStats(); + if (!stats) return; + stats.forEach((stat) => { + if (stat.type === "inbound-rtp" && stat.kind === "video") { + const fps = this.currentStats.fps; + fps.current = stat.framesPerSecond || 0; + const pl = this.currentStats.pl; + pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived; + const fl = this.currentStats.fl; + if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) { + this.lastVideoStat = stat; + return; + } + const 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; + const btr = this.currentStats.btr, timeDiff = stat.timestamp - lastStat.timestamp; + btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; + const dt = this.currentStats.dt; + dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; + const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; + dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat; + } else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") { + const ping = this.currentStats.ping; + ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1; + const dl = this.currentStats.dl; + dl.total = stat.bytesReceived; + const ul = this.currentStats.ul; + ul.total = stat.bytesSent; + } + }); + let batteryLevel = 100, isCharging = !1; + if (STATES.browser.capabilities.batteryApi) try { + const bm = await navigator.getBattery(); + isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100); + } catch (e) {} + const battery = this.currentStats.batt; + battery.current = batteryLevel, battery.isCharging = isCharging; + const playTime = this.currentStats.play, now = +new Date; + playTime.seconds = Math.ceil((now - playTime.startTime) / 1000); + } + getStat(kind) { + return this.currentStats[kind]; + } + reset() { + const 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() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + StreamStatsCollector.getInstance().reset(); + }); + } } function getSupportedCodecProfiles() { - const options = { - default: t("default") - }; - if (!("getCapabilities" in RTCRtpReceiver)) return options; - let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; - const codecs = RTCRtpReceiver.getCapabilities("video").codecs; - for (let codec of codecs) { - if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; - const 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; + const options = { + default: t("default") + }; + if (!("getCapabilities" in RTCRtpReceiver)) return options; + let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1; + const codecs = RTCRtpReceiver.getCapabilities("video").codecs; + for (let codec of codecs) { + if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue; + const 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 BaseSettingsStore { - static DEFINITIONS = { - version_last_check: { - default: 0 - }, - version_latest: { - default: "" - }, - version_current: { - default: "" - }, - bx_locale: { - label: t("language"), - default: localStorage.getItem("better_xcloud_locale") || "en-US", - options: SUPPORTED_LANGUAGES - }, - server_region: { - label: t("region"), - default: "default" - }, - server_bypass_restriction: { - 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_preferred_locale: { - label: t("preferred-game-language"), - default: "default", - options: { - default: t("default"), - "ar-SA": "العربية", - "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)", - "ru-RU": "русский", - "sk-SK": "slovenčina", - "sv-SE": "svenska", - "tr-TR": "Türkçe", - "zh-CN": "中文(简体)", - "zh-TW": "中文 (繁體)" - } - }, - stream_target_resolution: { - label: t("target-resolution"), - default: "auto", - options: { - auto: t("default"), - "720p": "720p", - "1080p": "1080p" - }, - suggest: { - lowest: "720p", - highest: "1080p" - } - }, - stream_codec_profile: { - label: t("visual-quality"), - default: "default", - options: getSupportedCodecProfiles(), - ready: (setting) => { - const 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] - }; - } - }, - prefer_ipv6_server: { - label: t("prefer-ipv6-server"), - default: !1 - }, - screenshot_apply_filters: { - requiredVariants: "full", - label: t("screenshot-apply-filters"), - default: !1 - }, - skip_splash_video: { - label: t("skip-splash-video"), - default: !1 - }, - hide_dots_icon: { - label: t("hide-system-menu-icon"), - default: !1 - }, - stream_combine_sources: { - requiredVariants: "full", - label: t("combine-audio-video-streams"), - default: !1, - experimental: !0, - note: t("combine-audio-video-streams-summary") - }, - stream_touch_controller: { - requiredVariants: "full", - label: t("tc-availability"), - default: "all", - options: { - default: t("default"), - all: t("tc-all-games"), - off: t("off") - }, - unsupported: !STATES.userAgent.capabilities.touch, - ready: (setting) => { - if (setting.unsupported) setting.default = "default"; - } - }, - stream_touch_controller_auto_off: { - requiredVariants: "full", - label: t("tc-auto-off"), - default: !1, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_default_opacity: { - requiredVariants: "full", - type: "number-stepper", - label: t("tc-default-opacity"), - default: 100, - min: 10, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10, - hideSlider: !0 - }, - unsupported: !STATES.userAgent.capabilities.touch - }, - stream_touch_controller_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 - }, - stream_touch_controller_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 - }, - stream_simplify_menu: { - label: t("simplify-stream-menu"), - default: !1 - }, - mkb_hide_idle_cursor: { - requiredVariants: "full", - label: t("hide-idle-cursor"), - default: !1 - }, - stream_disable_feedback_dialog: { - requiredVariants: "full", - label: t("disable-post-stream-feedback-dialog"), - default: !1 - }, - bitrate_video_max: { - requiredVariants: "full", - type: "number-stepper", - label: t("bitrate-video-maximum"), - note: "⚠️ " + t("unexpected-behavior"), - default: 0, - min: 0, - max: 14336000, - steps: 102400, - params: { - exactTicks: 5120000, - customTextValue: (value) => { - if (value = parseInt(value), value === 0) return t("unlimited"); - else return (value / 1024000).toFixed(1) + " Mb/s"; - } - }, - suggest: { - highest: 0 - } - }, - game_bar_position: { - requiredVariants: "full", - label: t("position"), - default: "bottom-left", - options: { - "bottom-left": t("bottom-left"), - "bottom-right": t("bottom-right"), - off: t("off") - } - }, - local_co_op_enabled: { - requiredVariants: "full", - label: t("enable-local-co-op-support"), - default: !1, - note: CE("a", { - href: "https://github.com/redphx/better-xcloud/discussions/275", - target: "_blank" - }, t("enable-local-co-op-support-note")) - }, - controller_show_connection_status: { - label: t("show-controller-connection-status"), - default: !0 - }, - controller_enable_shortcuts: { - requiredVariants: "full", - default: !1 - }, - controller_enable_vibration: { - requiredVariants: "full", - label: t("controller-vibration"), - default: !0 - }, - controller_device_vibration: { - requiredVariants: "full", - label: t("device-vibration"), - default: "off", - options: { - on: t("on"), - auto: t("device-vibration-not-using-gamepad"), - off: t("off") - } - }, - controller_vibration_intensity: { - requiredVariants: "full", - label: t("vibration-intensity"), - type: "number-stepper", - default: 100, - min: 0, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - mkb_enabled: { - requiredVariants: "full", - label: t("enable-mkb"), - default: !1, - unsupported: !STATES.userAgent.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); - } - }, - native_mkb_enabled: { - requiredVariants: "full", - label: t("native-mkb"), - default: "default", - options: { - default: t("default"), - on: t("on"), - off: t("off") - }, - ready: (setting) => { - if (AppInterface) ; - else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; - else delete setting.options.on; - } - }, - native_mkb_scroll_x_sensitivity: { - requiredVariants: "full", - label: t("horizontal-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - native_mkb_scroll_y_sensitivity: { - requiredVariants: "full", - label: t("vertical-scroll-sensitivity"), - type: "number-stepper", - default: 0, - min: 0, - max: 1e4, - steps: 10, - params: { - exactTicks: 2000, - customTextValue: (value) => { - if (!value) return t("default"); - return (value / 100).toFixed(1) + "x"; - } - } - }, - mkb_default_preset_id: { - requiredVariants: "full", - default: 0 - }, - mkb_absolute_mouse: { - requiredVariants: "full", - default: !1 - }, - reduce_animations: { - label: t("reduce-animations"), - default: !1 - }, - ui_loading_screen_game_art: { - requiredVariants: "full", - label: t("show-game-art"), - default: !0 - }, - ui_loading_screen_wait_time: { - label: t("show-wait-time"), - default: !0 - }, - ui_loading_screen_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_controller_friendly: { - 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_scrollbar_hide: { - label: t("hide-scrollbar"), - default: !1 - }, - ui_home_context_menu_disabled: { - requiredVariants: "full", - label: t("disable-home-context-menu"), - default: STATES.browser.capabilities.touch - }, - ui_hide_sections: { - 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: 6 - } - }, - ui_game_card_show_wait_time: { - requiredVariants: "full", - label: t("show-wait-time-in-game-card"), - default: !1 - }, - block_social_features: { - label: t("disable-social-features"), - default: !1 - }, - block_tracking: { - label: t("disable-xcloud-analytics"), - default: !1 - }, - user_agent_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") - } - }, - 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_power_preference: { - 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_max_fps: { - label: t("max-fps"), - type: "number-stepper", - default: 60, - min: 10, - max: 60, - steps: 10, - params: { - exactTicks: 10, - customTextValue: (value) => { - return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; - } - } - }, - video_sharpness: { - label: t("sharpness"), - type: "number-stepper", - 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: t("aspect-ratio-note"), - default: "16:9", - options: { - "16:9": "16:9", - "18:9": "18:9", - "21:9": "21:9", - "16:10": "16:10", - "4:3": "4:3", - fill: t("stretch") - } - }, - video_saturation: { - label: t("saturation"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_contrast: { - label: t("contrast"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - video_brightness: { - label: t("brightness"), - type: "number-stepper", - default: 100, - min: 50, - max: 150, - params: { - suffix: "%", - ticks: 25 - } - }, - audio_mic_on_playing: { - label: t("enable-mic-on-startup"), - default: !1 - }, - audio_enable_volume_control: { - requiredVariants: "full", - label: t("enable-volume-control"), - default: !1 - }, - audio_volume: { - label: t("volume"), - type: "number-stepper", - default: 100, - min: 0, - max: 600, - steps: 10, - params: { - suffix: "%", - ticks: 100 - } - }, - stats_items: { - label: t("stats"), - default: ["ping", "fps", "btr", "dt", "pl", "fl"], - multipleOptions: { - time: `TIME: ${t("clock")}`, - play: `PLAY: ${t("playtime")}`, - batt: `BATT: ${t("battery")}`, - ping: `PING: ${t("stat-ping")}`, - jit: `JIT: ${t("jitter")}`, - fps: `FPS: ${t("stat-fps")}`, - btr: `BTR: ${t("stat-bitrate")}`, - dt: `DT: ${t("stat-decode-time")}`, - pl: `PL: ${t("stat-packets-lost")}`, - fl: `FL: ${t("stat-frames-lost")}`, - dl: `DL: ${t("downloaded")}`, - ul: `UL: ${t("uploaded")}` - }, - params: { - size: 6 - }, - ready: (setting) => { - const multipleOptions = setting.multipleOptions; - if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; - } - }, - stats_show_when_playing: { - label: t("show-stats-on-startup"), - default: !1 - }, - stats_quick_glance: { - 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_text_size: { - label: t("text-size"), - default: "0.9rem", - options: { - "0.9rem": t("small"), - "1.0rem": t("normal"), - "1.1rem": t("large") - } - }, - stats_transparent: { - label: t("transparent-background"), - default: !1 - }, - stats_opacity: { - label: t("opacity"), - type: "number-stepper", - default: 80, - min: 50, - max: 100, - steps: 10, - params: { - suffix: "%", - ticks: 10 - } - }, - stats_conditional_formatting: { - label: t("conditional-formatting"), - default: !1 - }, - xhome_enabled: { - requiredVariants: "full", - label: t("enable-remote-play-feature"), - default: !1 - }, - xhome_resolution: { - requiredVariants: "full", - default: "1080p", - options: { - "1080p": "1080p", - "720p": "720p" - } - }, - game_fortnite_force_console: { - requiredVariants: "full", - label: "🎮 " + t("fortnite-force-console-version"), - default: !1, - note: t("fortnite-allow-stw-mode") - }, - game_msfs2020_force_native_mkb: { - requiredVariants: "full", - label: "✈️ " + t("msfs2020-force-native-mkb"), - default: !1, - note: t("may-not-work-properly") + static DEFINITIONS = { + version_last_check: { + default: 0 + }, + version_latest: { + default: "" + }, + version_current: { + default: "" + }, + bx_locale: { + label: t("language"), + default: localStorage.getItem("better_xcloud_locale") || "en-US", + options: SUPPORTED_LANGUAGES + }, + server_region: { + label: t("region"), + default: "default" + }, + server_bypass_restriction: { + 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_preferred_locale: { + label: t("preferred-game-language"), + default: "default", + options: { + default: t("default"), + "ar-SA": "العربية", + "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)", + "ru-RU": "русский", + "sk-SK": "slovenčina", + "sv-SE": "svenska", + "tr-TR": "Türkçe", + "zh-CN": "中文(简体)", + "zh-TW": "中文 (繁體)" + } + }, + stream_target_resolution: { + label: t("target-resolution"), + default: "auto", + options: { + auto: t("default"), + "720p": "720p", + "1080p": "1080p" + }, + suggest: { + lowest: "720p", + highest: "1080p" + } + }, + stream_codec_profile: { + label: t("visual-quality"), + default: "default", + options: getSupportedCodecProfiles(), + ready: (setting) => { + const 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] + }; + } + }, + prefer_ipv6_server: { + label: t("prefer-ipv6-server"), + default: !1 + }, + screenshot_apply_filters: { + requiredVariants: "full", + label: t("screenshot-apply-filters"), + default: !1 + }, + skip_splash_video: { + label: t("skip-splash-video"), + default: !1 + }, + hide_dots_icon: { + label: t("hide-system-menu-icon"), + default: !1 + }, + stream_combine_sources: { + requiredVariants: "full", + label: t("combine-audio-video-streams"), + default: !1, + experimental: !0, + note: t("combine-audio-video-streams-summary") + }, + stream_touch_controller: { + requiredVariants: "full", + label: t("tc-availability"), + default: "all", + options: { + default: t("default"), + all: t("tc-all-games"), + off: t("off") + }, + unsupported: !STATES.userAgent.capabilities.touch, + ready: (setting) => { + if (setting.unsupported) setting.default = "default"; + } + }, + stream_touch_controller_auto_off: { + requiredVariants: "full", + label: t("tc-auto-off"), + default: !1, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_default_opacity: { + requiredVariants: "full", + type: "number-stepper", + label: t("tc-default-opacity"), + default: 100, + min: 10, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10, + hideSlider: !0 + }, + unsupported: !STATES.userAgent.capabilities.touch + }, + stream_touch_controller_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 + }, + stream_touch_controller_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 + }, + stream_simplify_menu: { + label: t("simplify-stream-menu"), + default: !1 + }, + mkb_hide_idle_cursor: { + requiredVariants: "full", + label: t("hide-idle-cursor"), + default: !1 + }, + stream_disable_feedback_dialog: { + requiredVariants: "full", + label: t("disable-post-stream-feedback-dialog"), + default: !1 + }, + bitrate_video_max: { + requiredVariants: "full", + type: "number-stepper", + label: t("bitrate-video-maximum"), + note: "⚠️ " + t("unexpected-behavior"), + default: 0, + min: 0, + max: 14336000, + steps: 102400, + params: { + exactTicks: 5120000, + customTextValue: (value) => { + if (value = parseInt(value), value === 0) return t("unlimited"); + else return (value / 1024000).toFixed(1) + " Mb/s"; } - }; - constructor() { - super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); + }, + suggest: { + highest: 0 + } + }, + game_bar_position: { + requiredVariants: "full", + label: t("position"), + default: "bottom-left", + options: { + "bottom-left": t("bottom-left"), + "bottom-right": t("bottom-right"), + off: t("off") + } + }, + local_co_op_enabled: { + requiredVariants: "full", + label: t("enable-local-co-op-support"), + default: !1, + note: CE("a", { + href: "https://github.com/redphx/better-xcloud/discussions/275", + target: "_blank" + }, t("enable-local-co-op-support-note")) + }, + controller_show_connection_status: { + label: t("show-controller-connection-status"), + default: !0 + }, + controller_enable_shortcuts: { + requiredVariants: "full", + default: !1 + }, + controller_enable_vibration: { + requiredVariants: "full", + label: t("controller-vibration"), + default: !0 + }, + controller_device_vibration: { + requiredVariants: "full", + label: t("device-vibration"), + default: "off", + options: { + on: t("on"), + auto: t("device-vibration-not-using-gamepad"), + off: t("off") + } + }, + controller_vibration_intensity: { + requiredVariants: "full", + label: t("vibration-intensity"), + type: "number-stepper", + default: 100, + min: 0, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + mkb_enabled: { + requiredVariants: "full", + label: t("enable-mkb"), + default: !1, + unsupported: !STATES.userAgent.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); + } + }, + native_mkb_enabled: { + requiredVariants: "full", + label: t("native-mkb"), + default: "default", + options: { + default: t("default"), + on: t("on"), + off: t("off") + }, + ready: (setting) => { + if (AppInterface) ; + else if (UserAgent.isMobile()) setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on; + else delete setting.options.on; + } + }, + native_mkb_scroll_x_sensitivity: { + requiredVariants: "full", + label: t("horizontal-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + native_mkb_scroll_y_sensitivity: { + requiredVariants: "full", + label: t("vertical-scroll-sensitivity"), + type: "number-stepper", + default: 0, + min: 0, + max: 1e4, + steps: 10, + params: { + exactTicks: 2000, + customTextValue: (value) => { + if (!value) return t("default"); + return (value / 100).toFixed(1) + "x"; + } + } + }, + mkb_default_preset_id: { + requiredVariants: "full", + default: 0 + }, + mkb_absolute_mouse: { + requiredVariants: "full", + default: !1 + }, + reduce_animations: { + label: t("reduce-animations"), + default: !1 + }, + ui_loading_screen_game_art: { + requiredVariants: "full", + label: t("show-game-art"), + default: !0 + }, + ui_loading_screen_wait_time: { + label: t("show-wait-time"), + default: !0 + }, + ui_loading_screen_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_controller_friendly: { + 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_scrollbar_hide: { + label: t("hide-scrollbar"), + default: !1 + }, + ui_home_context_menu_disabled: { + requiredVariants: "full", + label: t("disable-home-context-menu"), + default: STATES.browser.capabilities.touch + }, + ui_hide_sections: { + 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: 6 + } + }, + ui_game_card_show_wait_time: { + requiredVariants: "full", + label: t("show-wait-time-in-game-card"), + default: !1 + }, + block_social_features: { + label: t("disable-social-features"), + default: !1 + }, + block_tracking: { + label: t("disable-xcloud-analytics"), + default: !1 + }, + user_agent_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") + } + }, + 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_power_preference: { + 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_max_fps: { + label: t("max-fps"), + type: "number-stepper", + default: 60, + min: 10, + max: 60, + steps: 10, + params: { + exactTicks: 10, + customTextValue: (value) => { + return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps"; + } + } + }, + video_sharpness: { + label: t("sharpness"), + type: "number-stepper", + 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: t("aspect-ratio-note"), + default: "16:9", + options: { + "16:9": "16:9", + "18:9": "18:9", + "21:9": "21:9", + "16:10": "16:10", + "4:3": "4:3", + fill: t("stretch") + } + }, + video_saturation: { + label: t("saturation"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_contrast: { + label: t("contrast"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + video_brightness: { + label: t("brightness"), + type: "number-stepper", + default: 100, + min: 50, + max: 150, + params: { + suffix: "%", + ticks: 25 + } + }, + audio_mic_on_playing: { + label: t("enable-mic-on-startup"), + default: !1 + }, + audio_enable_volume_control: { + requiredVariants: "full", + label: t("enable-volume-control"), + default: !1 + }, + audio_volume: { + label: t("volume"), + type: "number-stepper", + default: 100, + min: 0, + max: 600, + steps: 10, + params: { + suffix: "%", + ticks: 100 + } + }, + stats_items: { + label: t("stats"), + default: ["ping", "fps", "btr", "dt", "pl", "fl"], + multipleOptions: { + time: `TIME: ${t("clock")}`, + play: `PLAY: ${t("playtime")}`, + batt: `BATT: ${t("battery")}`, + ping: `PING: ${t("stat-ping")}`, + jit: `JIT: ${t("jitter")}`, + fps: `FPS: ${t("stat-fps")}`, + btr: `BTR: ${t("stat-bitrate")}`, + dt: `DT: ${t("stat-decode-time")}`, + pl: `PL: ${t("stat-packets-lost")}`, + fl: `FL: ${t("stat-frames-lost")}`, + dl: `DL: ${t("downloaded")}`, + ul: `UL: ${t("uploaded")}` + }, + params: { + size: 6 + }, + ready: (setting) => { + const multipleOptions = setting.multipleOptions; + if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"]; + } + }, + stats_show_when_playing: { + label: t("show-stats-on-startup"), + default: !1 + }, + stats_quick_glance: { + 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_text_size: { + label: t("text-size"), + default: "0.9rem", + options: { + "0.9rem": t("small"), + "1.0rem": t("normal"), + "1.1rem": t("large") + } + }, + stats_transparent: { + label: t("transparent-background"), + default: !1 + }, + stats_opacity: { + label: t("opacity"), + type: "number-stepper", + default: 80, + min: 50, + max: 100, + steps: 10, + params: { + suffix: "%", + ticks: 10 + } + }, + stats_conditional_formatting: { + label: t("conditional-formatting"), + default: !1 + }, + xhome_enabled: { + requiredVariants: "full", + label: t("enable-remote-play-feature"), + default: !1 + }, + xhome_resolution: { + requiredVariants: "full", + default: "1080p", + options: { + "1080p": "1080p", + "720p": "720p" + } + }, + game_fortnite_force_console: { + requiredVariants: "full", + label: "🎮 " + t("fortnite-force-console-version"), + default: !1, + note: t("fortnite-allow-stw-mode") + }, + game_msfs2020_force_native_mkb: { + requiredVariants: "full", + label: "✈️ " + t("msfs2020-force-native-mkb"), + default: !1, + note: t("may-not-work-properly") } + }; + constructor() { + super("better_xcloud", GlobalSettingsStorage.DEFINITIONS); + } } var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings); STORAGE.Global = globalSettings; class Screenshot { - static #$canvas; - static #canvasContext; - static setup() { - if (Screenshot.#$canvas) return; - Screenshot.#$canvas = CE("canvas", { class: "bx-gone" }), Screenshot.#canvasContext = Screenshot.#$canvas.getContext("2d", { - alpha: !1, - willReadFrequently: !1 - }); - } - static updateCanvasSize(width, height) { - const $canvas = Screenshot.#$canvas; - if ($canvas) $canvas.width = width, $canvas.height = height; - } - static updateCanvasFilters(filters) { - Screenshot.#canvasContext && (Screenshot.#canvasContext.filter = filters); - } - static #onAnimationEnd(e) { - e.target.classList.remove("bx-taking-screenshot"); - } - static takeScreenshot(callback) { - const currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = Screenshot.#$canvas; - if (!streamPlayer || !$canvas) return; - let $player; - if (getPref("screenshot_apply_filters")) $player = streamPlayer.getPlayerElement(); - else $player = streamPlayer.getPlayerElement("default"); - if (!$player || !$player.isConnected) return; - $player.parentElement.addEventListener("animationend", this.#onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot"); - const canvasContext = Screenshot.#canvasContext; - if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().drawFrame(); - if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { - const 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 && $canvas.toBlob((blob) => { - const now = +new Date, $anchor = CE("a", { - download: `${currentStream.titleSlug}-${now}.png`, - href: URL.createObjectURL(blob) - }); - $anchor.click(), URL.revokeObjectURL($anchor.href), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); - }, "image/png"); + static #$canvas; + static #canvasContext; + static setup() { + if (Screenshot.#$canvas) return; + Screenshot.#$canvas = CE("canvas", { class: "bx-gone" }), Screenshot.#canvasContext = Screenshot.#$canvas.getContext("2d", { + alpha: !1, + willReadFrequently: !1 + }); + } + static updateCanvasSize(width, height) { + const $canvas = Screenshot.#$canvas; + if ($canvas) $canvas.width = width, $canvas.height = height; + } + static updateCanvasFilters(filters) { + Screenshot.#canvasContext && (Screenshot.#canvasContext.filter = filters); + } + static #onAnimationEnd(e) { + e.target.classList.remove("bx-taking-screenshot"); + } + static takeScreenshot(callback) { + const currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = Screenshot.#$canvas; + if (!streamPlayer || !$canvas) return; + let $player; + if (getPref("screenshot_apply_filters")) $player = streamPlayer.getPlayerElement(); + else $player = streamPlayer.getPlayerElement("default"); + if (!$player || !$player.isConnected) return; + $player.parentElement.addEventListener("animationend", this.#onAnimationEnd, { once: !0 }), $player.parentElement.classList.add("bx-taking-screenshot"); + const canvasContext = Screenshot.#canvasContext; + if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().drawFrame(); + if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) { + const 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 && $canvas.toBlob((blob) => { + const now = +new Date, $anchor = CE("a", { + download: `${currentStream.titleSlug}-${now}.png`, + href: URL.createObjectURL(blob) + }); + $anchor.click(), URL.revokeObjectURL($anchor.href), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback(); + }, "image/png"); + } } 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", "⇀"], - 11: ["R3", "↻"], - 200: ["Right Stick Up", "↿"], - 201: ["Right Stick Down", "⇃"], - 202: ["Right Stick Left", "↽"], - 203: ["Right Stick Right", "⇁"] + 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", "⇀"], + 11: ["R3", "↻"], + 200: ["Right Stick Up", "↿"], + 201: ["Right Stick Down", "⇃"], + 202: ["Right Stick Left", "↽"], + 203: ["Right Stick Right", "⇁"] }; var MouseMapTo; ((MouseMapTo2) => { - MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; - MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; - MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; + MouseMapTo2[MouseMapTo2.OFF = 0] = "OFF"; + MouseMapTo2[MouseMapTo2.LS = 1] = "LS"; + MouseMapTo2[MouseMapTo2.RS = 2] = "RS"; })(MouseMapTo ||= {}); class StreamStats { - static instance; - static getInstance() { - if (!StreamStats.instance) StreamStats.instance = new StreamStats; - return StreamStats.instance; + static instance; + static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new 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") } - 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() { - this.render(); + }; + $container; + quickGlanceObserver; + 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.bind(this), 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(); + } + onStoppedPlaying() { + 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; + const $uiContainer = document.querySelector("div[data-testid=ui-container]"); + if (!$uiContainer) return; + this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { + for (let record of mutationList) { + const $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; + } + async update(forceUpdate = !1) { + if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { + this.onStoppedPlaying(); + return; } - 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.bind(this), this.REFRESH_INTERVAL); + const PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"); + let grade = ""; + const statsCollector = StreamStatsCollector.getInstance(); + await statsCollector.collect(); + let statKey; + for (statKey in this.stats) { + grade = ""; + const 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; } - 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(); - } - onStoppedPlaying() { - 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; - const $uiContainer = document.querySelector("div[data-testid=ui-container]"); - if (!$uiContainer) return; - this.quickGlanceObserver = new MutationObserver((mutationList, observer) => { - for (let record of mutationList) { - const $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; - } - async update(forceUpdate = !1) { - if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) { - this.onStoppedPlaying(); - return; - } - const PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats_conditional_formatting"); - let grade = ""; - const statsCollector = StreamStatsCollector.getInstance(); - await statsCollector.collect(); - let statKey; - for (statKey in this.stats) { - grade = ""; - const 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() { - const PREF_ITEMS = getPref("stats_items"), $container = this.$container; - $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); - } - hideSettingsUi() { - if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); - } - async render() { - this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); - let statKey; - for (statKey in this.stats) { - const stat = this.stats[statKey], $div = CE("div", { - class: `bx-stat-${statKey}`, - title: stat.name - }, CE("label", {}, statKey.toUpperCase()), stat.$element); - this.$container.appendChild($div); - } - this.refreshStyles(), document.documentElement.appendChild(this.$container); - } - static setupEvents() { - window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - const PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), 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(); + } + refreshStyles() { + const PREF_ITEMS = getPref("stats_items"), $container = this.$container; + $container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats_position"), $container.dataset.transparent = getPref("stats_transparent"), $container.style.opacity = getPref("stats_opacity") + "%", $container.style.fontSize = getPref("stats_text_size"); + } + hideSettingsUi() { + if (this.isGlancing() && !getPref("stats_quick_glance")) this.stop(); + } + async render() { + this.$container = CE("div", { class: "bx-stats-bar bx-gone" }); + let statKey; + for (statKey in this.stats) { + const stat = this.stats[statKey], $div = CE("div", { + class: `bx-stat-${statKey}`, + title: stat.name + }, CE("label", {}, statKey.toUpperCase()), stat.$element); + this.$container.appendChild($div); } + this.refreshStyles(), document.documentElement.appendChild(this.$container); + } + static setupEvents() { + window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { + const PREF_STATS_QUICK_GLANCE = getPref("stats_quick_glance"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats_show_when_playing"), 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 Toast { - static $wrapper; - static $msg; - static $status; - static stack = []; - static isShowing = !1; - static timeout; - static DURATION = 3000; - static show(msg, status, options = {}) { - options = options || {}; - const args = Array.from(arguments); - if (options.instant) Toast.stack = [args], Toast.showNext(); - else Toast.stack.push(args), !Toast.isShowing && Toast.showNext(); - } - static showNext() { - if (!Toast.stack.length) { - Toast.isShowing = !1; - return; - } - Toast.isShowing = !0, Toast.timeout && clearTimeout(Toast.timeout), Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION); - const [msg, status, options] = Toast.stack.shift(); - if (options && options.html) Toast.$msg.innerHTML = msg; - else Toast.$msg.textContent = msg; - if (status) Toast.$status.classList.remove("bx-gone"), Toast.$status.textContent = status; - else Toast.$status.classList.add("bx-gone"); - const classList = Toast.$wrapper.classList; - classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); - } - static hide() { - Toast.timeout = null; - const classList = Toast.$wrapper.classList; - classList.remove("bx-show"), classList.add("bx-hide"); - } - static setup() { - Toast.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.$msg = CE("span", { class: "bx-toast-msg" }), Toast.$status = CE("span", { class: "bx-toast-status" })), Toast.$wrapper.addEventListener("transitionend", (e) => { - const classList = Toast.$wrapper.classList; - if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.showNext(); - }), document.documentElement.appendChild(Toast.$wrapper); + static $wrapper; + static $msg; + static $status; + static stack = []; + static isShowing = !1; + static timeout; + static DURATION = 3000; + static show(msg, status, options = {}) { + options = options || {}; + const args = Array.from(arguments); + if (options.instant) Toast.stack = [args], Toast.showNext(); + else Toast.stack.push(args), !Toast.isShowing && Toast.showNext(); + } + static showNext() { + if (!Toast.stack.length) { + Toast.isShowing = !1; + return; } + Toast.isShowing = !0, Toast.timeout && clearTimeout(Toast.timeout), Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION); + const [msg, status, options] = Toast.stack.shift(); + if (options && options.html) Toast.$msg.innerHTML = msg; + else Toast.$msg.textContent = msg; + if (status) Toast.$status.classList.remove("bx-gone"), Toast.$status.textContent = status; + else Toast.$status.classList.add("bx-gone"); + const classList = Toast.$wrapper.classList; + classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show"); + } + static hide() { + Toast.timeout = null; + const classList = Toast.$wrapper.classList; + classList.remove("bx-show"), classList.add("bx-hide"); + } + static setup() { + Toast.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.$msg = CE("span", { class: "bx-toast-msg" }), Toast.$status = CE("span", { class: "bx-toast-status" })), Toast.$wrapper.addEventListener("transitionend", (e) => { + const classList = Toast.$wrapper.classList; + if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.showNext(); + }), document.documentElement.appendChild(Toast.$wrapper); + } } class MicrophoneShortcut { - static toggle(showToast = !0) { - if (!window.BX_EXPOSED.streamSession) return !1; - const 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 StreamUiShortcut { - static showHideStreamMenu() { - window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); - } -} -function checkForUpdate() { - if (SCRIPT_VERSION.includes("beta")) return; - const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version_current"), lastCheck = getPref("version_last_check"), now = Math.round(+new Date / 1000); - if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; - setPref("version_last_check", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { - setPref("version_latest", json.tag_name.substring(1)), setPref("version_current", SCRIPT_VERSION); - }), 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++) { - const chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr, hash |= 0; - } - return hash; -} -function renderString(str, obj) { - return str.replace(/\$\{.+?\}/g, (match) => { - const key = match.substring(2, match.length - 1); - if (key in obj) return obj[key]; - return 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) { + static toggle(showToast = !0) { + if (!window.BX_EXPOSED.streamSession) return !1; + const enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !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 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 StreamUiShortcut { + static showHideStreamMenu() { + window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu(); + } +} +function checkForUpdate() { + if (SCRIPT_VERSION.includes("beta")) return; + const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref("version_current"), lastCheck = getPref("version_last_check"), now = Math.round(+new Date / 1000); + if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return; + setPref("version_last_check", now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => { + setPref("version_latest", json.tag_name.substring(1)), setPref("version_current", SCRIPT_VERSION); + }), 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++) { + const chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr, hash |= 0; + } + return hash; +} +function renderString(str, obj) { + return str.replace(/\$\{.+?\}/g, (match) => { + const key = match.substring(2, match.length - 1); + if (key in obj) return obj[key]; + return 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(); + return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase(); } class SoundShortcut { - static adjustGainNodeVolume(amount) { - if (!getPref("audio_enable_volume_control")) return 0; - const currentValue = getPref("audio_volume"); - let 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 = setPref("audio_volume", newValue, !0), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue; + static adjustGainNodeVolume(amount) { + if (!getPref("audio_enable_volume_control")) return 0; + const currentValue = getPref("audio_volume"); + let 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 = setPref("audio_volume", newValue, !0), 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 (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { + const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); + let targetValue; + if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); + 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 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: targetValue === 0 ? 1 : 0 + }); + return; } - static setGainNodeVolume(value) { - STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100); - } - static muteUnmute() { - if (getPref("audio_enable_volume_control") && STATES.currentStream.audioGainNode) { - const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio_volume"); - let targetValue; - if (settingValue === 0) targetValue = 100, setPref("audio_volume", targetValue, !0); - 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 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: targetValue === 0 ? 1 : 0 - }); - return; - } - let $media; - if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); - if ($media) { - $media.muted = !$media.muted; - const status = $media.muted ? t("muted") : t("unmuted"); - Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { - speakerState: $media.muted ? 1 : 0 - }); - } + let $media; + if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media) $media = document.querySelector("div[data-testid=media-container] video"); + if ($media) { + $media.muted = !$media.muted; + const status = $media.muted ? t("muted") : t("unmuted"); + Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEvent.dispatch(window, BxEvent.SPEAKER_STATE_CHANGED, { + speakerState: $media.muted ? 1 : 0 + }); } + } } class BxSelectElement { - static wrap($select) { - $select.removeAttribute("tabindex"); - const $btnPrev = createButton({ - label: "<", - style: 32 - }), $btnNext = createButton({ - label: ">", - style: 32 - }), isMultiple = $select.multiple; - let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; - if (isMultiple) $content = CE("button", { - class: "bx-select-value bx-focusable", - tabindex: 0 - }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { - $checkBox.click(); - }), $checkBox.addEventListener("input", (e) => { - const $option = getOptionAtIndex(visibleIndex); - $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); - }); - else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); - const getOptionAtIndex = (index) => { - return Array.from($select.querySelectorAll("option"))[index]; - }, render = (e) => { - if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; - visibleIndex = normalizeIndex(visibleIndex); - const $option = getOptionAtIndex(visibleIndex); - let content = ""; - if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { - $label.innerHTML = ""; - const fragment = document.createDocumentFragment(); - fragment.appendChild(CE("span", {}, $option.parentElement.label)), 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), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); - const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; - $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); - }, normalizeIndex = (index) => { - return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); - }, onPrevNext = (e) => { - if (!e.target) return; - const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; - let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; - if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; - if (isMultiple) render(); - else BxEvent.dispatch($select, "input"); - }; - $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { - mutationList.forEach((mutation) => { - if (mutation.type === "childList" || mutation.type === "attributes") render(); - }); - }).observe($select, { - subtree: !0, - childList: !0, - attributes: !0 - }), render(); - const $div = CE("div", { - class: "bx-select", - _nearby: { - orientation: "horizontal", - focus: $btnNext - } - }, $select, $btnPrev, $content, $btnNext); - return Object.defineProperty($div, "value", { - get() { - return $select.value; - }, - set(value) { - $div.setValue(value); - } - }), $div.addEventListener = function() { - $select.addEventListener.apply($select, arguments); - }, $div.removeEventListener = function() { - $select.removeEventListener.apply($select, arguments); - }, $div.dispatchEvent = function() { - return $select.dispatchEvent.apply($select, arguments); - }, $div.setValue = (value) => { - if ("setValue" in $select) $select.setValue(value); - else $select.value = value; - }, $div; - } + static wrap($select) { + $select.removeAttribute("tabindex"); + const $btnPrev = createButton({ + label: "<", + style: 32 + }), $btnNext = createButton({ + label: ">", + style: 32 + }), isMultiple = $select.multiple; + let $checkBox, $label, visibleIndex = $select.selectedIndex, $content; + if (isMultiple) $content = CE("button", { + class: "bx-select-value bx-focusable", + tabindex: 0 + }, $checkBox = CE("input", { type: "checkbox" }), $label = CE("span", {}, "")), $content.addEventListener("click", (e) => { + $checkBox.click(); + }), $checkBox.addEventListener("input", (e) => { + const $option = getOptionAtIndex(visibleIndex); + $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input"); + }); + else $content = CE("div", {}, $label = CE("label", { for: $select.id + "_checkbox" }, "")); + const getOptionAtIndex = (index) => { + return Array.from($select.querySelectorAll("option"))[index]; + }, render = (e) => { + if (e && e.manualTrigger) visibleIndex = $select.selectedIndex; + visibleIndex = normalizeIndex(visibleIndex); + const $option = getOptionAtIndex(visibleIndex); + let content = ""; + if ($option) if (content = $option.textContent || "", content && $option.parentElement.tagName === "OPTGROUP") { + $label.innerHTML = ""; + const fragment = document.createDocumentFragment(); + fragment.appendChild(CE("span", {}, $option.parentElement.label)), 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), isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content); + const disablePrev = visibleIndex <= 0, disableNext = visibleIndex === $select.querySelectorAll("option").length - 1; + $btnPrev.classList.toggle("bx-inactive", disablePrev), $btnNext.classList.toggle("bx-inactive", disableNext), disablePrev && !disableNext && document.activeElement === $btnPrev && $btnNext.focus(), disableNext && !disablePrev && document.activeElement === $btnNext && $btnPrev.focus(); + }, normalizeIndex = (index) => { + return Math.min(Math.max(index, 0), $select.querySelectorAll("option").length - 1); + }, onPrevNext = (e) => { + if (!e.target) return; + const goNext = e.target.closest("button") === $btnNext, currentIndex = visibleIndex; + let newIndex = goNext ? currentIndex + 1 : currentIndex - 1; + if (newIndex = normalizeIndex(newIndex), visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex; + if (isMultiple) render(); + else BxEvent.dispatch($select, "input"); + }; + $select.addEventListener("input", render), $btnPrev.addEventListener("click", onPrevNext), $btnNext.addEventListener("click", onPrevNext), new MutationObserver((mutationList, observer2) => { + mutationList.forEach((mutation) => { + if (mutation.type === "childList" || mutation.type === "attributes") render(); + }); + }).observe($select, { + subtree: !0, + childList: !0, + attributes: !0 + }), render(); + const $div = CE("div", { + class: "bx-select", + _nearby: { + orientation: "horizontal", + focus: $btnNext + } + }, $select, $btnPrev, $content, $btnNext); + return Object.defineProperty($div, "value", { + get() { + return $select.value; + }, + set(value) { + $div.setValue(value); + } + }), $div.addEventListener = function() { + $select.addEventListener.apply($select, arguments); + }, $div.removeEventListener = function() { + $select.removeEventListener.apply($select, arguments); + }, $div.dispatchEvent = function() { + return $select.dispatchEvent.apply($select, arguments); + }, $div.setValue = (value) => { + if ("setValue" in $select) $select.setValue(value); + else $select.value = value; + }, $div; + } } function onChangeVideoPlayerType() { - const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); - if (!$videoProcessing) return; - let isDisabled = !1; - const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); - if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); - else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $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"), updateVideoPlayer(); + const playerType = getPref("video_player_type"), $videoProcessing = document.getElementById(`bx_setting_${"video_processing"}`), $videoSharpness = document.getElementById(`bx_setting_${"video_sharpness"}`), $videoPowerPreference = document.getElementById(`bx_setting_${"video_power_preference"}`), $videoMaxFps = document.getElementById(`bx_setting_${"video_max_fps"}`); + if (!$videoProcessing) return; + let isDisabled = !1; + const $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`); + if (playerType === "webgl2") $optCas && ($optCas.disabled = !1); + else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $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"), updateVideoPlayer(); } -function limitVideoPlayerFps() { - const targetFps = getPref("video_max_fps"); - STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); +function limitVideoPlayerFps(targetFps) { + STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); } function updateVideoPlayer() { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - limitVideoPlayerFps(); - const options = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") - }; - streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + limitVideoPlayerFps(getPref("video_max_fps")); + const options = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") + }; + streamPlayer.setPlayerType(getPref("video_player_type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer(); } window.addEventListener("resize", updateVideoPlayer); class MkbPreset { - static MOUSE_SETTINGS = { - map_to: { - label: t("map-mouse-to"), - type: "options", - default: MouseMapTo[2], - options: { - [MouseMapTo[2]]: t("right-stick"), - [MouseMapTo[1]]: t("left-stick"), - [MouseMapTo[0]]: t("off") - } - }, - sensitivity_y: { - label: t("horizontal-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - sensitivity_x: { - label: t("vertical-sensitivity"), - type: "number-stepper", - default: 50, - min: 1, - max: 300, - params: { - suffix: "%", - exactTicks: 50 - } - }, - deadzone_counterweight: { - label: t("deadzone-counterweight"), - type: "number-stepper", - default: 20, - min: 1, - max: 50, - params: { - suffix: "%", - exactTicks: 10 - } - } - }; - static DEFAULT_PRESET = { - mapping: { - 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"], - 16: ["Backquote"], - 7: ["Mouse0"], - 6: ["Mouse2"], - 10: ["ShiftLeft"], - 11: ["KeyF"] - }, - mouse: { - map_to: MouseMapTo[2], - sensitivity_x: 100, - sensitivity_y: 100, - deadzone_counterweight: 20 - } - }; - static convert(preset) { - const obj = { - mapping: {}, - mouse: Object.assign({}, preset.mouse) - }; - for (let buttonIndex in preset.mapping) - for (let keyName of preset.mapping[parseInt(buttonIndex)]) - obj.mapping[keyName] = parseInt(buttonIndex); - const mouse = obj.mouse; - mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; - const mouseMapTo = MouseMapTo[mouse["map_to"]]; - if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; - else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; - return console.log(obj), obj; + static MOUSE_SETTINGS = { + map_to: { + label: t("map-mouse-to"), + type: "options", + default: MouseMapTo[2], + options: { + [MouseMapTo[2]]: t("right-stick"), + [MouseMapTo[1]]: t("left-stick"), + [MouseMapTo[0]]: t("off") + } + }, + sensitivity_y: { + label: t("horizontal-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + sensitivity_x: { + label: t("vertical-sensitivity"), + type: "number-stepper", + default: 50, + min: 1, + max: 300, + params: { + suffix: "%", + exactTicks: 50 + } + }, + deadzone_counterweight: { + label: t("deadzone-counterweight"), + type: "number-stepper", + default: 20, + min: 1, + max: 50, + params: { + suffix: "%", + exactTicks: 10 + } } + }; + static DEFAULT_PRESET = { + mapping: { + 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"], + 16: ["Backquote"], + 7: ["Mouse0"], + 6: ["Mouse2"], + 10: ["ShiftLeft"], + 11: ["KeyF"] + }, + mouse: { + map_to: MouseMapTo[2], + sensitivity_x: 100, + sensitivity_y: 100, + deadzone_counterweight: 20 + } + }; + static convert(preset) { + const obj = { + mapping: {}, + mouse: Object.assign({}, preset.mouse) + }; + for (let buttonIndex in preset.mapping) + for (let keyName of preset.mapping[parseInt(buttonIndex)]) + obj.mapping[keyName] = parseInt(buttonIndex); + const mouse = obj.mouse; + mouse["sensitivity_x"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["sensitivity_y"] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse["deadzone_counterweight"] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT; + const mouseMapTo = MouseMapTo[mouse["map_to"]]; + if (typeof mouseMapTo !== "undefined") mouse["map_to"] = mouseMapTo; + else mouse["map_to"] = MkbPreset.MOUSE_SETTINGS["map_to"].default; + return console.log(obj), obj; + } } class LocalDb { - static #instance; - static get INSTANCE() { - if (!LocalDb.#instance) LocalDb.#instance = new LocalDb; - return LocalDb.#instance; - } - static DB_NAME = "BetterXcloud"; - static DB_VERSION = 1; - static TABLE_PRESETS = "mkb_presets"; - #DB; - #open() { - return new Promise((resolve, reject) => { - if (this.#DB) { - resolve(); - return; - } - const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); - request.onupgradeneeded = (e) => { - const db = e.target.result; - switch (e.oldVersion) { - case 0: { - db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name"); - break; - } - } - }, request.onerror = (e) => { - console.log(e), alert(e.target.error.message), reject && reject(); - }, request.onsuccess = (e) => { - this.#DB = e.target.result, resolve(); - }; + static #instance; + static get INSTANCE() { + if (!LocalDb.#instance) LocalDb.#instance = new LocalDb; + return LocalDb.#instance; + } + static DB_NAME = "BetterXcloud"; + static DB_VERSION = 1; + static TABLE_PRESETS = "mkb_presets"; + #DB; + #open() { + return new Promise((resolve, reject) => { + if (this.#DB) { + resolve(); + return; + } + const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION); + request.onupgradeneeded = (e) => { + const db = e.target.result; + switch (e.oldVersion) { + case 0: { + db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name"); + break; + } + } + }, request.onerror = (e) => { + console.log(e), alert(e.target.error.message), reject && reject(); + }, request.onsuccess = (e) => { + this.#DB = e.target.result, resolve(); + }; + }); + } + #table(name, type) { + const table = this.#DB.transaction(name, type || "readonly").objectStore(name); + return new Promise((resolve) => resolve(table)); + } + #call(method) { + const table = arguments[1]; + return new Promise((resolve) => { + const request = method.call(table, ...Array.from(arguments).slice(2)); + request.onsuccess = (e) => { + resolve([table, e.target.result]); + }; + }); + } + #count(table) { + return this.#call(table.count, ...arguments); + } + #add(table, data) { + return this.#call(table.add, ...arguments); + } + #put(table, data) { + return this.#call(table.put, ...arguments); + } + #delete(table, data) { + return this.#call(table.delete, ...arguments); + } + #get(table, id2) { + return this.#call(table.get, ...arguments); + } + #getAll(table) { + return this.#call(table.getAll, ...arguments); + } + newPreset(name, data) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id2]) => new Promise((resolve) => resolve(id2))); + } + updatePreset(preset) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id2]) => new Promise((resolve) => resolve(id2))); + } + deletePreset(id2) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id2)).then(([table, id3]) => new Promise((resolve) => resolve(id3))); + } + getPreset(id2) { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id2)).then(([table, preset]) => new Promise((resolve) => resolve(preset))); + } + getPresets() { + return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => { + if (count > 0) return new Promise((resolve) => { + this.#getAll(table).then(([table2, items]) => { + const presets = {}; + items.forEach((item2) => presets[item2.id] = item2), resolve(presets); + }); }); - } - #table(name, type) { - const table = this.#DB.transaction(name, type || "readonly").objectStore(name); - return new Promise((resolve) => resolve(table)); - } - #call(method) { - const table = arguments[1]; - return new Promise((resolve) => { - const request = method.call(table, ...Array.from(arguments).slice(2)); - request.onsuccess = (e) => { - resolve([table, e.target.result]); - }; + const preset = { + name: t("default"), + data: MkbPreset.DEFAULT_PRESET + }; + return new Promise((resolve) => { + this.#add(table, preset).then(([table2, id2]) => { + preset.id = id2, setPref("mkb_default_preset_id", id2), resolve({ [id2]: preset }); }); - } - #count(table) { - return this.#call(table.count, ...arguments); - } - #add(table, data) { - return this.#call(table.add, ...arguments); - } - #put(table, data) { - return this.#call(table.put, ...arguments); - } - #delete(table, data) { - return this.#call(table.delete, ...arguments); - } - #get(table, id2) { - return this.#call(table.get, ...arguments); - } - #getAll(table) { - return this.#call(table.getAll, ...arguments); - } - newPreset(name, data) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id2]) => new Promise((resolve) => resolve(id2))); - } - updatePreset(preset) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id2]) => new Promise((resolve) => resolve(id2))); - } - deletePreset(id2) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id2)).then(([table, id3]) => new Promise((resolve) => resolve(id3))); - } - getPreset(id2) { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id2)).then(([table, preset]) => new Promise((resolve) => resolve(preset))); - } - getPresets() { - return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => { - if (count > 0) return new Promise((resolve) => { - this.#getAll(table).then(([table2, items]) => { - const presets = {}; - items.forEach((item2) => presets[item2.id] = item2), resolve(presets); - }); - }); - const preset = { - name: t("default"), - data: MkbPreset.DEFAULT_PRESET - }; - return new Promise((resolve) => { - this.#add(table, preset).then(([table2, id2]) => { - preset.id = id2, setPref("mkb_default_preset_id", id2), resolve({ [id2]: preset }); - }); - }); - }); - } + }); + }); + } } class KeyHelper { - static #NON_PRINTABLE_KEYS = { - Backquote: "`", - 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, name; - if (e instanceof KeyboardEvent) code = e.code || e.key; - 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) name = KeyHelper.codeToKeyName(code); - return code ? { code, name } : null; - } - static codeToKeyName(code) { - return 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; - } + static #NON_PRINTABLE_KEYS = { + Backquote: "`", + 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, name; + if (e instanceof KeyboardEvent) code = e.code || e.key; + 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) name = KeyHelper.codeToKeyName(code); + return code ? { code, name } : null; + } + static codeToKeyName(code) { + return 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; + } } var LOG_TAG = "PointerClient"; class PointerClient { - static instance; - static getInstance() { - if (!PointerClient.instance) PointerClient.instance = new PointerClient; - return PointerClient.instance; - } - socket; - mkbHandler; - 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(LOG_TAG, "connected"); - }), this.socket.addEventListener("error", (event) => { - BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event); - }), this.socket.addEventListener("close", (event) => { - this.socket = null; - }), this.socket.addEventListener("message", (event) => { - const dataView = new DataView(event.data); - let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT; - switch (messageType) { - 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); - } - }); - } - onMove(dataView, offset) { - const x = dataView.getInt16(offset); - offset += Int16Array.BYTES_PER_ELEMENT; - const y = dataView.getInt16(offset); - this.mkbHandler?.handleMouseMove({ - movementX: x, - movementY: y - }); - } - onPress(messageType, dataView, offset) { - const button = dataView.getUint8(offset); - this.mkbHandler?.handleMouseClick({ - pointerButton: button, - pressed: messageType === 2 - }); - } - onScroll(dataView, offset) { - const vScroll = dataView.getInt16(offset); - offset += Int16Array.BYTES_PER_ELEMENT; - const 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; - } + static instance; + static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient); + socket; + mkbHandler; + 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(LOG_TAG, "connected"); + }), this.socket.addEventListener("error", (event) => { + BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event); + }), this.socket.addEventListener("close", (event) => { + this.socket = null; + }), this.socket.addEventListener("message", (event) => { + const dataView = new DataView(event.data); + let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT; + switch (messageType) { + 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); + } + }); + } + onMove(dataView, offset) { + const x = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const y = dataView.getInt16(offset); + this.mkbHandler?.handleMouseMove({ + movementX: x, + movementY: y + }); + } + onPress(messageType, dataView, offset) { + const button = dataView.getUint8(offset); + this.mkbHandler?.handleMouseClick({ + pointerButton: button, + pressed: messageType === 2 + }); + } + onScroll(dataView, offset) { + const vScroll = dataView.getInt16(offset); + offset += Int16Array.BYTES_PER_ELEMENT; + const 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; - } + mkbHandler; + constructor(handler) { + this.mkbHandler = handler; + } } class MkbHandler {} class NativeMkbHandler extends MkbHandler { - static instance; - #pointerClient; - #enabled = !1; - #mouseButtonsPressed = 0; - #mouseWheelX = 0; - #mouseWheelY = 0; - #mouseVerticalMultiply = 0; - #mouseHorizontalMultiply = 0; - #inputSink; - #$message; - static getInstance() { - if (!NativeMkbHandler.instance) NativeMkbHandler.instance = new NativeMkbHandler; - return NativeMkbHandler.instance; + static instance; + static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler); + #pointerClient; + #enabled = !1; + #mouseButtonsPressed = 0; + #mouseWheelX = 0; + #mouseWheelY = 0; + #mouseVerticalMultiply = 0; + #mouseHorizontalMultiply = 0; + #inputSink; + #$message; + #onKeyboardEvent(e) { + if (e.type === "keyup" && e.code === "F8") { + e.preventDefault(), this.toggle(); + return; } - #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) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage() { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg" }, CE("div", {}, CE("p", {}, t("native-mkb")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "native" }, createButton({ + style: 1 | 64 | 256, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), createButton({ + style: 4 | 64, + label: t("ignore"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.#$message?.classList.add("bx-gone"); } + }))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + } + handleEvent(event) { + switch (event.type) { + case "keyup": + this.#onKeyboardEvent(event); + break; + case BxEvent.XCLOUD_DIALOG_SHOWN: + this.#onDialogShown(); + 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; } - #onPointerLockRequested(e) { - AppInterface.requestPointerCapture(), this.start(); - } - #onPointerLockExited(e) { - AppInterface.releasePointerCapture(), this.stop(); - } - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { - document.pointerLockElement && document.exitPointerLock(); - }; - #initMessage() { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg" }, CE("div", {}, CE("p", {}, t("native-mkb")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "native" }, createButton({ - style: 1 | 64 | 256, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), createButton({ - style: 4 | 64, - label: t("ignore"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.#$message?.classList.add("bx-gone"); - } - }))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - } - handleEvent(event) { - switch (event.type) { - case "keyup": - this.#onKeyboardEvent(event); - break; - case BxEvent.XCLOUD_DIALOG_SHOWN: - this.#onDialogShown(); - 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.#inputSink = window.BX_EXPOSED.inputSink, this.#updateInputConfigurationAsync(!1); - try { - this.#pointerClient.start(STATES.pointerServerPort, this); - } catch (e) { - Toast.show("Cannot enable Mouse & Keyboard feature"); - } - if (this.#mouseVerticalMultiply = getPref("native_mkb_scroll_y_sensitivity"), this.#mouseHorizontalMultiply = getPref("native_mkb_scroll_x_sensitivity"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#initMessage(), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("native-mkb"), { html: !0 }), this.#$message?.classList.add("bx-gone"); - else this.#$message?.classList.remove("bx-gone"); - } - 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.#$message?.classList.add("bx-gone"), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); - } - stop() { - this.#resetMouseInput(), this.#enabled = !1, this.#updateInputConfigurationAsync(!1), this.#$message?.classList.remove("bx-gone"); - } - destroy() { - this.#pointerClient?.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#$message?.classList.add("bx-gone"); - } - handleMouseMove(data) { - this.#sendMouseInput({ - X: data.movementX, - Y: data.movementY, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY - }); - } - handleMouseClick(data) { - const { 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: this.#mouseWheelX, - WheelY: this.#mouseWheelY - }); - } - handleMouseWheel(data) { - const { vertical, horizontal } = data; - if (this.#mouseWheelX = horizontal, this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) this.#mouseWheelX *= this.#mouseHorizontalMultiply; - if (this.#mouseWheelY = vertical, this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) this.#mouseWheelY *= this.#mouseVerticalMultiply; - return this.#sendMouseInput({ - X: 0, - Y: 0, - Buttons: this.#mouseButtonsPressed, - WheelX: this.#mouseWheelX, - WheelY: this.#mouseWheelY - }), !0; - } - setVerticalScrollMultiplier(vertical) { - this.#mouseVerticalMultiply = vertical; - } - setHorizontalScrollMultiplier(horizontal) { - this.#mouseHorizontalMultiply = horizontal; - } - waitForMouseData(enabled) {} - isEnabled() { - return this.#enabled; - } - #sendMouseInput(data) { - data.Type = 0, this.#inputSink?.onMouseInput(data); - } - #resetMouseInput() { - this.#mouseButtonsPressed = 0, this.#mouseWheelX = 0, this.#mouseWheelY = 0, this.#sendMouseInput({ - X: 0, - Y: 0, - Buttons: 0, - WheelX: 0, - WheelY: 0 - }); + } + init() { + this.#pointerClient = PointerClient.getInstance(), this.#inputSink = window.BX_EXPOSED.inputSink, this.#updateInputConfigurationAsync(!1); + try { + this.#pointerClient.start(STATES.pointerServerPort, this); + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); } + if (this.#mouseVerticalMultiply = getPref("native_mkb_scroll_y_sensitivity"), this.#mouseHorizontalMultiply = getPref("native_mkb_scroll_x_sensitivity"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#initMessage(), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("native-mkb"), { html: !0 }), this.#$message?.classList.add("bx-gone"); + else this.#$message?.classList.remove("bx-gone"); + } + 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.#$message?.classList.add("bx-gone"), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 }); + } + stop() { + this.#resetMouseInput(), this.#enabled = !1, this.#updateInputConfigurationAsync(!1), this.#$message?.classList.remove("bx-gone"); + } + destroy() { + this.#pointerClient?.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#$message?.classList.add("bx-gone"); + } + handleMouseMove(data) { + this.#sendMouseInput({ + X: data.movementX, + Y: data.movementY, + Buttons: this.#mouseButtonsPressed, + WheelX: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }); + } + handleMouseClick(data) { + const { 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: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }); + } + handleMouseWheel(data) { + const { vertical, horizontal } = data; + if (this.#mouseWheelX = horizontal, this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1) this.#mouseWheelX *= this.#mouseHorizontalMultiply; + if (this.#mouseWheelY = vertical, this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1) this.#mouseWheelY *= this.#mouseVerticalMultiply; + return this.#sendMouseInput({ + X: 0, + Y: 0, + Buttons: this.#mouseButtonsPressed, + WheelX: this.#mouseWheelX, + WheelY: this.#mouseWheelY + }), !0; + } + setVerticalScrollMultiplier(vertical) { + this.#mouseVerticalMultiply = vertical; + } + setHorizontalScrollMultiplier(horizontal) { + this.#mouseHorizontalMultiply = horizontal; + } + waitForMouseData(enabled) {} + isEnabled() { + return this.#enabled; + } + #sendMouseInput(data) { + data.Type = 0, this.#inputSink?.onMouseInput(data); + } + #resetMouseInput() { + this.#mouseButtonsPressed = 0, this.#mouseWheelX = 0, this.#mouseWheelY = 0, this.#sendMouseInput({ + X: 0, + Y: 0, + Buttons: 0, + WheelX: 0, + WheelY: 0 + }); + } } var LOG_TAG2 = "MkbHandler", PointerToMouseButton = { - 1: 0, - 2: 2, - 4: 1 + 1: 0, + 2: 2, + 4: 1 }, VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller"; class WebSocketMouseDataProvider extends MouseDataProvider { - #pointerClient; - #connected = !1; - init() { - this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; - try { - this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; - } catch (e) { - Toast.show("Cannot enable Mouse & Keyboard feature"); - } - } - start() { - this.#connected && AppInterface.requestPointerCapture(); - } - stop() { - this.#connected && AppInterface.releasePointerCapture(); - } - destroy() { - this.#connected && this.#pointerClient?.stop(); + #pointerClient; + #connected = !1; + init() { + this.#pointerClient = PointerClient.getInstance(), this.#connected = !1; + try { + this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0; + } catch (e) { + Toast.show("Cannot enable Mouse & Keyboard feature"); } + } + start() { + this.#connected && AppInterface.requestPointerCapture(); + } + stop() { + this.#connected && AppInterface.releasePointerCapture(); + } + destroy() { + this.#connected && this.#pointerClient?.stop(); + } } class PointerLockMouseDataProvider extends MouseDataProvider { - init() {} - 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); - } - destroy() {} - #onMouseMoveEvent = (e) => { - this.mkbHandler.handleMouseMove({ - movementX: e.movementX, - movementY: e.movementY - }); + init() {} + 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); + } + destroy() {} + #onMouseMoveEvent = (e) => { + this.mkbHandler.handleMouseMove({ + movementX: e.movementX, + movementY: e.movementY + }); + }; + #onMouseEvent = (e) => { + e.preventDefault(); + const isMouseDown = e.type === "mousedown", data = { + mouseButton: e.button, + pressed: isMouseDown }; - #onMouseEvent = (e) => { - e.preventDefault(); - const isMouseDown = e.type === "mousedown", data = { - mouseButton: e.button, - pressed: isMouseDown - }; - this.mkbHandler.handleMouseClick(data); + this.mkbHandler.handleMouseClick(data); + }; + #onWheelEvent = (e) => { + if (!KeyHelper.getKeyFromEvent(e)) return; + const data = { + vertical: e.deltaY, + horizontal: e.deltaX }; - #onWheelEvent = (e) => { - if (!KeyHelper.getKeyFromEvent(e)) return; - const data = { - vertical: e.deltaY, - horizontal: e.deltaX - }; - if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); - }; - #disableContextMenu = (e) => e.preventDefault(); + if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault(); + }; + #disableContextMenu = (e) => e.preventDefault(); } class EmulatedMkbHandler extends MkbHandler { - static #instance; - static getInstance() { - if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler; - return EmulatedMkbHandler.#instance; + static instance; + static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler); + #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); + static DEFAULT_PANNING_SENSITIVITY = 0.001; + static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; + static MAXIMUM_STICK_RANGE = 1.1; + #VIRTUAL_GAMEPAD = { + id: VIRTUAL_GAMEPAD_ID, + index: 3, + 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 = window.navigator.getGamepads.bind(window.navigator); + #enabled = !1; + #mouseDataProvider; + #isPolling = !1; + #prevWheelCode = null; + #wheelStoppedTimeout; + #detectMouseStoppedTimeout; + #$message; + #escKeyDownTime = -1; + #STICK_MAP; + #LEFT_STICK_X = []; + #LEFT_STICK_Y = []; + #RIGHT_STICK_X = []; + #RIGHT_STICK_Y = []; + constructor() { + super(); + this.#STICK_MAP = { + 102: [this.#LEFT_STICK_X, 0, -1], + 103: [this.#LEFT_STICK_X, 0, 1], + 100: [this.#LEFT_STICK_Y, 1, -1], + 101: [this.#LEFT_STICK_Y, 1, 1], + 202: [this.#RIGHT_STICK_X, 2, -1], + 203: [this.#RIGHT_STICK_X, 2, 1], + 200: [this.#RIGHT_STICK_Y, 3, -1], + 201: [this.#RIGHT_STICK_Y, 3, 1] + }; + } + isEnabled = () => this.#enabled; + #patchedGetGamepads = () => { + const gamepads = this.#nativeGetGamepads() || []; + return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + }; + #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; + #updateStick(stick, x, y) { + const virtualGamepad = this.#getVirtualGamepad(); + virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); + } + #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); + #resetGamepad = () => { + const gamepad = this.#getVirtualGamepad(); + gamepad.axes = [0, 0, 0, 0]; + for (let button of gamepad.buttons) + button.pressed = !1, button.value = 0; + gamepad.timestamp = performance.now(); + }; + #pressButton = (buttonIndex, pressed) => { + const virtualGamepad = this.#getVirtualGamepad(); + if (buttonIndex >= 100) { + let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; + valueArr = valueArr, axisIndex = axisIndex; + 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]][2]; + else value = 0; + virtualGamepad.axes[axisIndex] = value; + } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; + virtualGamepad.timestamp = performance.now(); + }; + #onKeyboardEvent = (e) => { + const isKeyDown = e.type === "keydown"; + if (e.code === "F8") { + if (!isKeyDown) e.preventDefault(), this.toggle(); + return; } - #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); - static DEFAULT_PANNING_SENSITIVITY = 0.001; - static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; - static MAXIMUM_STICK_RANGE = 1.1; - #VIRTUAL_GAMEPAD = { - id: VIRTUAL_GAMEPAD_ID, - index: 3, - 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 = window.navigator.getGamepads.bind(window.navigator); - #enabled = !1; - #mouseDataProvider; - #isPolling = !1; - #prevWheelCode = null; - #wheelStoppedTimeout; - #detectMouseStoppedTimeout; - #$message; - #escKeyDownTime = -1; - #STICK_MAP; - #LEFT_STICK_X = []; - #LEFT_STICK_Y = []; - #RIGHT_STICK_X = []; - #RIGHT_STICK_Y = []; - constructor() { - super(); - this.#STICK_MAP = { - 102: [this.#LEFT_STICK_X, 0, -1], - 103: [this.#LEFT_STICK_X, 0, 1], - 100: [this.#LEFT_STICK_Y, 1, -1], - 101: [this.#LEFT_STICK_Y, 1, 1], - 202: [this.#RIGHT_STICK_X, 2, -1], - 203: [this.#RIGHT_STICK_X, 2, 1], - 200: [this.#RIGHT_STICK_Y, 3, -1], - 201: [this.#RIGHT_STICK_Y, 3, 1] - }; + 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; } - isEnabled = () => this.#enabled; - #patchedGetGamepads = () => { - const gamepads = this.#nativeGetGamepads() || []; - return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads; + if (!this.#isPolling) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; + if (typeof buttonIndex === "undefined") return; + if (e.repeat) return; + e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); + }; + #onMouseStopped = () => { + this.#detectMouseStoppedTimeout = null; + const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 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]; + const keyCode = "Mouse" + mouseButton, key = { + code: keyCode, + name: KeyHelper.codeToKeyName(keyCode) }; - #getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD; - #updateStick(stick, x, y) { - const virtualGamepad = this.#getVirtualGamepad(); - virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now(); - } - #vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2); - #resetGamepad = () => { - const gamepad = this.#getVirtualGamepad(); - gamepad.axes = [0, 0, 0, 0]; - for (let button of gamepad.buttons) - button.pressed = !1, button.value = 0; - gamepad.timestamp = performance.now(); - }; - #pressButton = (buttonIndex, pressed) => { - const virtualGamepad = this.#getVirtualGamepad(); - if (buttonIndex >= 100) { - let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex]; - valueArr = valueArr, axisIndex = axisIndex; - 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]][2]; - else value = 0; - virtualGamepad.axes[axisIndex] = value; - } else virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0; - virtualGamepad.timestamp = performance.now(); - }; - #onKeyboardEvent = (e) => { - const isKeyDown = e.type === "keydown"; - if (e.code === "F8") { - if (!isKeyDown) e.preventDefault(), this.toggle(); - return; + if (!key.name) return; + const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return; + this.#pressButton(buttonIndex, data.pressed); + }; + handleMouseMove = (data) => { + const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; + if (mouseMapTo === 0) return; + this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); + const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; + let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); + if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; + else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; + const 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; + const key = { + code, + name: KeyHelper.codeToKeyName(code) + }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; + if (typeof buttonIndex === "undefined") return !1; + if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); + return this.#wheelStoppedTimeout = window.setTimeout(() => { + this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); + }, 20), !0; + }; + toggle = (force) => { + if (typeof force !== "undefined") this.#enabled = force; + else this.#enabled = !this.#enabled; + if (this.#enabled) document.body.requestPointerLock(); + else document.pointerLockElement && document.exitPointerLock(); + }; + #getCurrentPreset = () => { + return new Promise((resolve) => { + const presetId = getPref("mkb_default_preset_id"); + LocalDb.INSTANCE.getPreset(presetId).then((preset) => { + resolve(preset); + }); + }); + }; + refreshPresetData = () => { + this.#getCurrentPreset().then((preset) => { + this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); + }); + }; + waitForMouseData = (wait) => { + this.#$message && this.#$message.classList.toggle("bx-gone", !wait); + }; + #onPollingModeChanged = (e) => { + if (!this.#$message) return; + if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); + else this.#$message.classList.add("bx-offscreen"); + }; + #onDialogShown = () => { + document.pointerLockElement && document.exitPointerLock(); + }; + #initMessage = () => { + if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ + style: 1 | 256 | 64, + label: t("activate"), + onClick: ((e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!0); + }).bind(this) + }), CE("div", {}, createButton({ + label: t("ignore"), + style: 4, + onClick: (e) => { + e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); } - 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) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]; - if (typeof buttonIndex === "undefined") return; - if (e.repeat) return; - e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown); - }; - #onMouseStopped = () => { - this.#detectMouseStoppedTimeout = null; - const analog = this.#CURRENT_PRESET_DATA.mouse["map_to"] === 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]; - const keyCode = "Mouse" + mouseButton, key = { - code: keyCode, - name: KeyHelper.codeToKeyName(keyCode) - }; - if (!key.name) return; - const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return; - this.#pressButton(buttonIndex, data.pressed); - }; - handleMouseMove = (data) => { - const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse["map_to"]; - if (mouseMapTo === 0) return; - this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50); - const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse["deadzone_counterweight"]; - let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse["sensitivity_x"], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse["sensitivity_y"], length = this.#vectorLength(x, y); - if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length; - else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE) x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length; - const 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; - const key = { - code, - name: KeyHelper.codeToKeyName(code) - }, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]; - if (typeof buttonIndex === "undefined") return !1; - if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0); - return this.#wheelStoppedTimeout = window.setTimeout(() => { - this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1); - }, 20), !0; - }; - toggle = (force) => { - if (typeof force !== "undefined") this.#enabled = force; - else this.#enabled = !this.#enabled; - if (this.#enabled) document.body.requestPointerLock(); - else document.pointerLockElement && document.exitPointerLock(); - }; - #getCurrentPreset = () => { - return new Promise((resolve) => { - const presetId = getPref("mkb_default_preset_id"); - LocalDb.INSTANCE.getPreset(presetId).then((preset) => { - resolve(preset); - }); - }); - }; - refreshPresetData = () => { - this.#getCurrentPreset().then((preset) => { - this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad(); - }); - }; - waitForMouseData = (wait) => { - this.#$message && this.#$message.classList.toggle("bx-gone", !wait); - }; - #onPollingModeChanged = (e) => { - if (!this.#$message) return; - if (e.mode === "none") this.#$message.classList.remove("bx-offscreen"); - else this.#$message.classList.add("bx-offscreen"); - }; - #onDialogShown = () => { - document.pointerLockElement && document.exitPointerLock(); - }; - #initMessage = () => { - if (!this.#$message) this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({ - style: 1 | 256 | 64, - label: t("activate"), - onClick: ((e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!0); - }).bind(this) - }), CE("div", {}, createButton({ - label: t("ignore"), - style: 4, - onClick: (e) => { - e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1); - } - }), createButton({ - label: t("edit"), - onClick: (e) => { - e.preventDefault(), e.stopPropagation(); - const dialog = SettingsNavigationDialog.getInstance(); - dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); - } - })))); - if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); - }; - #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; + }), createButton({ + label: t("edit"), + onClick: (e) => { + e.preventDefault(), e.stopPropagation(); + const dialog = SettingsNavigationDialog.getInstance(); + dialog.focusTab("mkb"), NavigationDialogManager.getInstance().show(dialog); } + })))); + if (!this.#$message.isConnected) document.documentElement.appendChild(this.#$message); + }; + #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 (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), window.addEventListener(BxEvent.XCLOUD_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 (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); - else this.waitForMouseData(!0); - }; - destroy = () => { - if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && 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), window.removeEventListener(BxEvent.XCLOUD_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, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); - const 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; - const virtualGamepad = this.#getVirtualGamepad(); - if (virtualGamepad.connected) this.#resetGamepad(), 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() { - window.addEventListener(BxEvent.STREAM_PLAYING, () => { - if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { - if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); - } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); - }); - } + } + init = () => { + if (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), window.addEventListener(BxEvent.XCLOUD_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 (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface) Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1); + else this.waitForMouseData(!0); + }; + destroy = () => { + if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && 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), window.removeEventListener(BxEvent.XCLOUD_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, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start(); + const 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; + const virtualGamepad = this.#getVirtualGamepad(); + if (virtualGamepad.connected) this.#resetGamepad(), 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() { + window.addEventListener(BxEvent.STREAM_PLAYING, () => { + if (STATES.currentStream.titleInfo?.details.hasMkbSupport) { + if (AppInterface && getPref("native_mkb_enabled") === "on") AppInterface && NativeMkbHandler.getInstance().init(); + } else if (getPref("mkb_enabled") && (AppInterface || !UserAgent.isMobile())) BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init(); + }); + } } class NavigationDialog { - dialogManager; - constructor() { - this.dialogManager = NavigationDialogManager.getInstance(); - } - show() { - if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); - } - hide() { - NavigationDialogManager.getInstance().hide(); - } - getFocusedElement() { - const $activeElement = document.activeElement; - if (!$activeElement) return null; - if (this.$container.contains($activeElement)) return $activeElement; - return null; - } - onBeforeMount() {} - onMounted() {} - onBeforeUnmount() {} - onUnmounted() {} - handleKeyPress(key) { - return !1; - } - handleGamepad(button) { - return !1; - } + dialogManager; + constructor() { + this.dialogManager = NavigationDialogManager.getInstance(); + } + show() { + if (NavigationDialogManager.getInstance().show(this), !this.getFocusedElement()) this.focusIfNeeded(); + } + hide() { + NavigationDialogManager.getInstance().hide(); + } + getFocusedElement() { + const $activeElement = document.activeElement; + if (!$activeElement) return null; + if (this.$container.contains($activeElement)) return $activeElement; + return null; + } + onBeforeMount() {} + onMounted() {} + onBeforeUnmount() {} + onUnmounted() {} + handleKeyPress(key) { + return !1; + } + handleGamepad(button) { + return !1; + } } class NavigationDialogManager { - static instance; - static getInstance() { - if (!NavigationDialogManager.instance) NavigationDialogManager.instance = new NavigationDialogManager; - return NavigationDialogManager.instance; + static instance; + static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager); + static GAMEPAD_POLLING_INTERVAL = 50; + static GAMEPAD_KEYS = [ + 12, + 13, + 14, + 15, + 0, + 1, + 4, + 5, + 6, + 7 + ]; + 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" } - static GAMEPAD_POLLING_INTERVAL = 50; - static GAMEPAD_KEYS = [ - 12, - 13, - 14, - 15, - 0, - 1, - 4, - 5, - 6, - 7 - ]; - 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; + constructor() { + if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { + e.preventDefault(), e.stopPropagation(), 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()), getPref("ui_controller_friendly")) + new MutationObserver((mutationList) => { + if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; + const $dialog = mutationList[0].addedNodes[0]; + if (!$dialog || !($dialog instanceof HTMLElement)) return; + this.calculateSelectBoxes($dialog); + }).observe(this.$container, { childList: !0 }); + } + calculateSelectBoxes($root) { + $root.querySelectorAll(".bx-select:not([data-calculated]) select").forEach(($select) => { + const $parent = $select.parentElement; + if ($parent.classList.contains("bx-full-width")) { + $parent.dataset.calculated = "true"; + return; + } + const rect = $select.getBoundingClientRect(); + let $label, width = Math.ceil(rect.width); + if (!width) return; + if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; + else $label = $parent.querySelector("div"); + $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; + }); + } + handleEvent(event) { + switch (event.type) { + case "keydown": + const $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key; + let handled = this.dialog?.handleKeyPress(keyCode); + if (handled) { + event.preventDefault(), event.stopPropagation(); + return; } - }; - gamepadPollingIntervalId = null; - gamepadLastStates = []; - gamepadHoldingIntervalId = null; - $overlay; - $container; - dialog = null; - constructor() { - if (this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => { - e.preventDefault(), e.stopPropagation(), 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()), getPref("ui_controller_friendly")) - new MutationObserver((mutationList) => { - if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return; - const $dialog = mutationList[0].addedNodes[0]; - if (!$dialog || !($dialog instanceof HTMLElement)) return; - this.calculateSelectBoxes($dialog); - }).observe(this.$container, { childList: !0 }); + 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")); + } else if (keyCode === "Escape") handled = !0, this.hide(); + if (handled) event.preventDefault(), event.stopPropagation(); + break; } - calculateSelectBoxes($root) { - $root.querySelectorAll(".bx-select:not([data-calculated]) select").forEach(($select) => { - const $parent = $select.parentElement; - if ($parent.classList.contains("bx-full-width")) { - $parent.dataset.calculated = "true"; + } + isShowing() { + return this.$container && !this.$container.classList.contains("bx-gone"); + } + pollGamepad() { + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) { + if (!gamepad || !gamepad.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + const { axes, buttons } = gamepad; + let 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) { + const 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(() => { + const lastState2 = this.gamepadLastStates[gamepad.index]; + if (lastState2) { + if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { + this.handleGamepad(gamepad, heldButton); return; + } } - const rect = $select.getBoundingClientRect(); - let $label, width = Math.ceil(rect.width); - if (!width) return; - if ($select.multiple) $label = $parent.querySelector(".bx-select-value"), width += 20; - else $label = $parent.querySelector("div"); - $label.style.minWidth = width + "px", $parent.dataset.calculated = "true"; - }); + this.clearGamepadHoldingInterval(); + }, 200); + continue; + } + if (releasedButton === null) { + this.clearGamepadHoldingInterval(); + continue; + } + if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; + if (releasedButton === 0) { + document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click")); + return; + } else if (releasedButton === 1) { + this.hide(); + return; + } + if (this.handleGamepad(gamepad, releasedButton)) return; } - handleEvent(event) { - switch (event.type) { - case "keydown": - const $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key; - let 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")); - } else if (keyCode === "Escape") handled = !0, this.hide(); - if (handled) event.preventDefault(), event.stopPropagation(); - break; - } + } + 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") { + const $range = document.activeElement; + if (direction === 4 || direction === 2) $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input")), handled = !0; } - isShowing() { - return this.$container && !this.$container.classList.contains("bx-gone"); + 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) { + if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); + this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); + } + hide() { + this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_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.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; + } + 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) { + const nearby = $elm.nearby || {}; + if (nearby.selfOrientation) return nearby.selfOrientation; + let orientation, $current = $elm.parentElement; + while ($current !== this.$container) { + const tmp = $current.nearby?.orientation; + if ($current.nearby && tmp) { + orientation = tmp; + break; + } + $current = $current.parentElement; } - pollGamepad() { - const gamepads = window.navigator.getGamepads(); - for (let gamepad of gamepads) { - if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - const { axes, buttons } = gamepad; - let 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) { - const 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(() => { - const lastState2 = this.gamepadLastStates[gamepad.index]; - if (lastState2) { - if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) { - this.handleGamepad(gamepad, heldButton); - return; - } - } - this.clearGamepadHoldingInterval(); - }, 200); - continue; - } - if (releasedButton === null) { - this.clearGamepadHoldingInterval(); - continue; - } - if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return; - if (releasedButton === 0) { - document.activeElement && document.activeElement.dispatchEvent(new MouseEvent("click")); - return; - } else if (releasedButton === 1) { - this.hide(); - return; - } - if (this.handleGamepad(gamepad, releasedButton)) return; - } + 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; + const $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); + let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; + if (siblingProperty) { + let $sibling = $target; + while ($sibling[siblingProperty]) { + $sibling = $sibling[siblingProperty]; + const $focusable = this.findFocusableElement($sibling, direction); + if ($focusable) return $focusable; + } } - 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") { - const $range = document.activeElement; - if (direction === 4 || direction === 2) $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; + if (nearby.loop) { + if (nearby.loop(direction)) return null; } - clearGamepadHoldingInterval() { - this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = 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; + const 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; + } } - show(dialog) { - if (this.clearGamepadHoldingInterval(), BxEvent.dispatch(window, BxEvent.XCLOUD_DIALOG_SHOWN), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.$overlay.classList.remove("bx-gone"), STATES.isPlaying) this.$overlay.classList.add("bx-invisible"); - this.unmountCurrentDialog(), this.dialog = dialog, dialog.onBeforeMount(), this.$container.appendChild(dialog.getContent()), dialog.onMounted(), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling(); - } - hide() { - this.clearGamepadHoldingInterval(), document.body.classList.remove("bx-no-scroll"), BxEvent.dispatch(window, BxEvent.XCLOUD_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.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1; - } - 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) { - const nearby = $elm.nearby || {}; - if (nearby.selfOrientation) return nearby.selfOrientation; - let orientation, $current = $elm.parentElement; - while ($current !== this.$container) { - const 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; - const $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target); - let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction]; - if (siblingProperty) { - let $sibling = $target; - while ($sibling[siblingProperty]) { - $sibling = $sibling[siblingProperty]; - const $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; - const 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; - } - } - const 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; - const $target = this.findFocusableElement($child, direction); - if ($target) return $target; - } - return null; - } - startGamepadPolling() { - this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); - } - stopGamepadPolling() { - this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; - } - focusDirection(direction) { - const dialog = this.dialog; - if (!dialog) return; - const $focusing = dialog.getFocusedElement(); - if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; - const $target = this.findNextTarget($focusing, direction, !0); - this.focus($target); - } - unmountCurrentDialog() { - const dialog = this.dialog; - dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + const 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; + const $target = this.findFocusableElement($child, direction); + if ($target) return $target; } + return null; + } + startGamepadPolling() { + this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad.bind(this), NavigationDialogManager.GAMEPAD_POLLING_INTERVAL); + } + stopGamepadPolling() { + this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null; + } + focusDirection(direction) { + const dialog = this.dialog; + if (!dialog) return; + const $focusing = dialog.getFocusedElement(); + if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null; + const $target = this.findNextTarget($focusing, direction, !0); + this.focus($target); + } + unmountCurrentDialog() { + const dialog = this.dialog; + dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null; + } } var BxIcon = { - BETTER_XCLOUD: "", - TRUE_ACHIEVEMENTS: "", - STREAM_SETTINGS: "", - STREAM_STATS: "", - CLOSE: "", - COMMAND: "", - CONTROLLER: "", - CREATE_SHORTCUT: "", - DISPLAY: "", - EYE: "", - EYE_SLASH: "", - HOME: "", - NATIVE_MKB: "", - NEW: "", - COPY: "", - TRASH: "", - CURSOR_TEXT: "", - POWER: "", - QUESTION: "", - REFRESH: "", - VIRTUAL_CONTROLLER: "", - REMOTE_PLAY: "", - CARET_LEFT: "", - CARET_RIGHT: "", - SCREENSHOT: "", - SPEAKER_MUTED: "", - TOUCH_CONTROL_ENABLE: "", - TOUCH_CONTROL_DISABLE: "", - MICROPHONE: "", - MICROPHONE_MUTED: "", - BATTERY: "", - PLAYTIME: "", - SERVER: "", - DOWNLOAD: "", - UPLOAD: "", - AUDIO: "" + BETTER_XCLOUD: "", + TRUE_ACHIEVEMENTS: "", + STREAM_SETTINGS: "", + STREAM_STATS: "", + CLOSE: "", + COMMAND: "", + CONTROLLER: "", + CREATE_SHORTCUT: "", + DISPLAY: "", + EYE: "", + EYE_SLASH: "", + HOME: "", + NATIVE_MKB: "", + NEW: "", + COPY: "", + TRASH: "", + CURSOR_TEXT: "", + POWER: "", + QUESTION: "", + REFRESH: "", + VIRTUAL_CONTROLLER: "", + REMOTE_PLAY: "", + CARET_LEFT: "", + CARET_RIGHT: "", + SCREENSHOT: "", + SPEAKER_MUTED: "", + TOUCH_CONTROL_ENABLE: "", + TOUCH_CONTROL_DISABLE: "", + MICROPHONE: "", + MICROPHONE_MUTED: "", + BATTERY: "", + PLAYTIME: "", + SERVER: "", + DOWNLOAD: "", + UPLOAD: "", + AUDIO: "" }; class Dialog { - $dialog; - $title; - $content; - $overlay; - onClose; - constructor(options) { - const { - title, - className, - content, - hideCloseButton, - onClose, - helpUrl - } = options, $overlay = document.querySelector(".bx-dialog-overlay"); - if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay); - else this.$overlay = $overlay; - let $close; - this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: 4, - title: t("help"), - url: helpUrl - })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => { - this.hide(e); - }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); - } - show(newOptions) { - if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone"); - this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); - } - hide(e) { - this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e); - } - toggle() { - this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone"); - } + $dialog; + $title; + $content; + $overlay; + onClose; + constructor(options) { + const { + title, + className, + content, + hideCloseButton, + onClose, + helpUrl + } = options, $overlay = document.querySelector(".bx-dialog-overlay"); + if (!$overlay) this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay); + else this.$overlay = $overlay; + let $close; + this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 4, + title: t("help"), + url: helpUrl + })), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => { + this.hide(e); + }), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog); + } + show(newOptions) { + if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title) this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone"); + this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll"); + } + hide(e) { + this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e); + } + toggle() { + this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone"); + } } class MkbRemapper { - #BUTTON_ORDERS = [ - 12, - 13, - 14, - 15, - 0, - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 16, - 10, - 100, - 101, - 102, - 103, - 11, - 200, - 201, - 202, - 203 - ]; - static #instance; - static get INSTANCE() { - if (!MkbRemapper.#instance) MkbRemapper.#instance = new MkbRemapper; - return MkbRemapper.#instance; + #BUTTON_ORDERS = [ + 12, + 13, + 14, + 15, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 16, + 10, + 100, + 101, + 102, + 103, + 11, + 200, + 201, + 202, + 203 + ]; + static #instance; + static get INSTANCE() { + if (!MkbRemapper.#instance) MkbRemapper.#instance = new MkbRemapper; + return MkbRemapper.#instance; + } + #STATE = { + currentPresetId: 0, + presets: {}, + editingPresetData: null, + isEditing: !1 + }; + #$ = { + wrapper: null, + presetsSelect: null, + activateButton: null, + currentBindingKey: null, + allKeyElements: [], + allMouseElements: {} + }; + bindingDialog; + constructor() { + this.#STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({ + className: "bx-binding-dialog", + content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))), + hideCloseButton: !0 + }); + } + #clearEventListeners = () => { + window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); + }; + #bindKey = ($elm, key) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + if ($elm.getAttribute("data-key-code") === key.code) return; + for (let $otherElm of this.#$.allKeyElements) + if ($otherElm.getAttribute("data-key-code") === key.code) this.#unbindKey($otherElm); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); + }; + #unbindKey = ($elm) => { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); + this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); + }; + #onWheel = (e) => { + e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onMouseDown = (e) => { + e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onKeyDown = (e) => { + if (e.preventDefault(), e.stopPropagation(), this.#clearEventListeners(), e.code !== "Escape") this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)); + window.setTimeout(() => this.bindingDialog.hide(), 200); + }; + #onBindingKey = (e) => { + if (!this.#STATE.isEditing || e.button !== 0) return; + console.log(e), this.#$.currentBindingKey = e.target, window.addEventListener("keydown", this.#onKeyDown), window.addEventListener("mousedown", this.#onMouseDown), window.addEventListener("wheel", this.#onWheel), this.bindingDialog.show({ title: this.#$.currentBindingKey.getAttribute("data-prompt") }); + }; + #onContextMenu = (e) => { + if (e.preventDefault(), !this.#STATE.isEditing) return; + this.#unbindKey(e.target); + }; + #getPreset = (presetId) => { + return this.#STATE.presets[presetId]; + }; + #getCurrentPreset = () => { + return this.#getPreset(this.#STATE.currentPresetId); + }; + #switchPreset = (presetId) => { + this.#STATE.currentPresetId = presetId; + const presetData = this.#getCurrentPreset().data; + for (let $elm of this.#$.allKeyElements) { + const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; + if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); + else $elm.textContent = "", $elm.removeAttribute("data-key-code"); } - #STATE = { - currentPresetId: 0, - presets: {}, - editingPresetData: null, - isEditing: !1 - }; - #$ = { - wrapper: null, - presetsSelect: null, - activateButton: null, - currentBindingKey: null, - allKeyElements: [], - allMouseElements: {} - }; - bindingDialog; - constructor() { - this.#STATE.currentPresetId = getPref("mkb_default_preset_id"), this.bindingDialog = new Dialog({ - className: "bx-binding-dialog", - content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))), - hideCloseButton: !0 + let key; + for (key in this.#$.allMouseElements) { + const $elm = this.#$.allMouseElements[key]; + let value = presetData.mouse[key]; + if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; + "setValue" in $elm && $elm.setValue(value); + } + const activated = getPref("mkb_default_preset_id") === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); + }; + #refresh() { + while (this.#$.presetsSelect.firstChild) + this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); + LocalDb.INSTANCE.getPresets().then((presets) => { + this.#STATE.presets = presets; + const $fragment = document.createDocumentFragment(); + let defaultPresetId; + if (this.#STATE.currentPresetId === 0) this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); + else defaultPresetId = getPref("mkb_default_preset_id"); + for (let id2 in presets) { + let name = presets[id2].name; + if (id2 === defaultPresetId) name = "🎮 " + name; + const $options = CE("option", { value: id2 }, name); + $options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options); + } + this.#$.presetsSelect.appendChild($fragment); + const activated = defaultPresetId === this.#STATE.currentPresetId; + this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); + }); + } + #toggleEditing = (force) => { + if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data); + else this.#STATE.editingPresetData = null; + const childElements = this.#$.wrapper.querySelectorAll("select, button, input"); + for (let $elm of Array.from(childElements)) { + if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue; + let disable = !this.#STATE.isEditing; + if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable; + $elm.disabled = disable; + } + }; + render() { + this.#$.wrapper = CE("div", { class: "bx-mkb-settings" }), this.#$.presetsSelect = CE("select", { tabindex: -1 }), this.#$.presetsSelect.addEventListener("change", (e) => { + this.#switchPreset(parseInt(e.target.value)); + }); + const promptNewName = (value) => { + let newName = ""; + while (!newName) { + if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1; + newName = newName.trim(); + } + return newName ? newName : !1; + }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ + title: t("rename"), + icon: BxIcon.CURSOR_TEXT, + tabIndex: -1, + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(preset.name); + if (!newName || newName === preset.name) return; + preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh()); + } + }), createButton({ + icon: BxIcon.NEW, + title: t("new"), + tabIndex: -1, + onClick: (e) => { + let newName = promptNewName(""); + if (!newName) return; + LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); }); - } - #clearEventListeners = () => { - window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel); - }; - #bindKey = ($elm, key) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - if ($elm.getAttribute("data-key-code") === key.code) return; - for (let $otherElm of this.#$.allKeyElements) - if ($otherElm.getAttribute("data-key-code") === key.code) this.#unbindKey($otherElm); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code); - }; - #unbindKey = ($elm) => { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")); - this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code"); - }; - #onWheel = (e) => { - e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onMouseDown = (e) => { - e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onKeyDown = (e) => { - if (e.preventDefault(), e.stopPropagation(), this.#clearEventListeners(), e.code !== "Escape") this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)); - window.setTimeout(() => this.bindingDialog.hide(), 200); - }; - #onBindingKey = (e) => { - if (!this.#STATE.isEditing || e.button !== 0) return; - console.log(e), this.#$.currentBindingKey = e.target, window.addEventListener("keydown", this.#onKeyDown), window.addEventListener("mousedown", this.#onMouseDown), window.addEventListener("wheel", this.#onWheel), this.bindingDialog.show({ title: this.#$.currentBindingKey.getAttribute("data-prompt") }); - }; - #onContextMenu = (e) => { - if (e.preventDefault(), !this.#STATE.isEditing) return; - this.#unbindKey(e.target); - }; - #getPreset = (presetId) => { - return this.#STATE.presets[presetId]; - }; - #getCurrentPreset = () => { - return this.#getPreset(this.#STATE.currentPresetId); - }; - #switchPreset = (presetId) => { - this.#STATE.currentPresetId = presetId; - const presetData = this.#getCurrentPreset().data; - for (let $elm of this.#$.allKeyElements) { - const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex]; - if (buttonKeys && buttonKeys[keySlot]) $elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]); - else $elm.textContent = "", $elm.removeAttribute("data-key-code"); - } - let key; - for (key in this.#$.allMouseElements) { - const $elm = this.#$.allMouseElements[key]; - let value = presetData.mouse[key]; - if (typeof value === "undefined") value = MkbPreset.MOUSE_SETTINGS[key].default; - "setValue" in $elm && $elm.setValue(value); - } - const activated = getPref("mkb_default_preset_id") === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"); - }; - #refresh() { - while (this.#$.presetsSelect.firstChild) - this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild); - LocalDb.INSTANCE.getPresets().then((presets) => { - this.#STATE.presets = presets; - const $fragment = document.createDocumentFragment(); - let defaultPresetId; - if (this.#STATE.currentPresetId === 0) this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref("mkb_default_preset_id", defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(); - else defaultPresetId = getPref("mkb_default_preset_id"); - for (let id2 in presets) { - let name = presets[id2].name; - if (id2 === defaultPresetId) name = "🎮 " + name; - const $options = CE("option", { value: id2 }, name); - $options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options); - } - this.#$.presetsSelect.appendChild($fragment); - const activated = defaultPresetId === this.#STATE.currentPresetId; - this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId); + } + }), createButton({ + icon: BxIcon.COPY, + title: t("copy"), + tabIndex: -1, + onClick: (e) => { + const preset = this.#getCurrentPreset(); + let newName = promptNewName(`${preset.name} (2)`); + if (!newName) return; + LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => { + this.#STATE.currentPresetId = id2, this.#refresh(); }); - } - #toggleEditing = (force) => { - if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing) this.#STATE.editingPresetData = deepClone(this.#getCurrentPreset().data); - else this.#STATE.editingPresetData = null; - const childElements = this.#$.wrapper.querySelectorAll("select, button, input"); - for (let $elm of Array.from(childElements)) { - if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons")) continue; - let disable = !this.#STATE.isEditing; - if ($elm.parentElement.classList.contains("bx-mkb-preset-tools")) disable = !disable; - $elm.disabled = disable; - } - }; - render() { - this.#$.wrapper = CE("div", { class: "bx-mkb-settings" }), this.#$.presetsSelect = CE("select", { tabindex: -1 }), this.#$.presetsSelect.addEventListener("change", (e) => { - this.#switchPreset(parseInt(e.target.value)); + } + }), createButton({ + icon: BxIcon.TRASH, + style: 2, + title: t("delete"), + tabIndex: -1, + onClick: (e) => { + if (!confirm(t("confirm-delete-preset"))) return; + LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => { + this.#STATE.currentPresetId = 0, this.#refresh(); }); - const promptNewName = (value) => { - let newName = ""; - while (!newName) { - if (newName = prompt(t("prompt-preset-name"), value), newName === null) return !1; - newName = newName.trim(); - } - return newName ? newName : !1; - }, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({ - title: t("rename"), - icon: BxIcon.CURSOR_TEXT, - tabIndex: -1, - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(preset.name); - if (!newName || newName === preset.name) return; - preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh()); - } - }), createButton({ - icon: BxIcon.NEW, - title: t("new"), - tabIndex: -1, - onClick: (e) => { - let newName = promptNewName(""); - if (!newName) return; - LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => { - this.#STATE.currentPresetId = id2, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.COPY, - title: t("copy"), - tabIndex: -1, - onClick: (e) => { - const preset = this.#getCurrentPreset(); - let newName = promptNewName(`${preset.name} (2)`); - if (!newName) return; - LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => { - this.#STATE.currentPresetId = id2, this.#refresh(); - }); - } - }), createButton({ - icon: BxIcon.TRASH, - style: 2, - title: t("delete"), - tabIndex: -1, - onClick: (e) => { - if (!confirm(t("confirm-delete-preset"))) return; - LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => { - this.#STATE.currentPresetId = 0, this.#refresh(); - }); - } - })); - this.#$.wrapper.appendChild($header); - const $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2; - for (let buttonIndex of this.#BUTTON_ORDERS) { - const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; - let $elm; - const $fragment = document.createDocumentFragment(); - for (let i = 0;i < keysPerButton; i++) - $elm = CE("button", { - type: "button", - "data-prompt": buttonPrompt, - "data-button-index": buttonIndex, - "data-key-slot": i - }, " "), $elm.addEventListener("mouseup", this.#onBindingKey), $elm.addEventListener("contextmenu", this.#onContextMenu), $fragment.appendChild($elm), this.#$.allKeyElements.push($elm); - const $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment); - $rows.appendChild($keyRow); - } - $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings"))); - const $mouseSettings = document.createDocumentFragment(); - for (let key in MkbPreset.MOUSE_SETTINGS) { - const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; - let $elm; - const onChange = (e, value2) => { - this.#STATE.editingPresetData.mouse[key] = value2; - }, $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${key}` - }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params)); - $mouseSettings.appendChild($row), this.#$.allMouseElements[key] = $elm; - } - $rows.appendChild($mouseSettings), this.#$.wrapper.appendChild($rows); - const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ - label: t("edit"), - tabIndex: -1, - onClick: (e) => this.#toggleEditing(!0) - }), this.#$.activateButton = createButton({ - label: t("activate"), - style: 1, - tabIndex: -1, - onClick: (e) => { - setPref("mkb_default_preset_id", this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); - } - })), CE("div", {}, createButton({ - label: t("cancel"), - style: 4, - tabIndex: -1, - onClick: (e) => { - this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); - } - }), createButton({ - label: t("save"), - style: 1, - tabIndex: -1, - onClick: (e) => { - const updatedPreset = deepClone(this.#getCurrentPreset()); - updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => { - if (id2 === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); - this.#toggleEditing(!1), this.#refresh(); - }); - } - }))); - return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; + } + })); + this.#$.wrapper.appendChild($header); + const $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2; + for (let buttonIndex of this.#BUTTON_ORDERS) { + const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex]; + let $elm; + const $fragment = document.createDocumentFragment(); + for (let i = 0;i < keysPerButton; i++) + $elm = CE("button", { + type: "button", + "data-prompt": buttonPrompt, + "data-button-index": buttonIndex, + "data-key-slot": i + }, " "), $elm.addEventListener("mouseup", this.#onBindingKey), $elm.addEventListener("contextmenu", this.#onContextMenu), $fragment.appendChild($elm), this.#$.allKeyElements.push($elm); + const $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment); + $rows.appendChild($keyRow); } + $rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings"))); + const $mouseSettings = document.createDocumentFragment(); + for (let key in MkbPreset.MOUSE_SETTINGS) { + const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default; + let $elm; + const onChange = (e, value2) => { + this.#STATE.editingPresetData.mouse[key] = value2; + }, $row = CE("label", { + class: "bx-settings-row", + for: `bx_setting_${key}` + }, CE("span", { class: "bx-settings-label" }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params)); + $mouseSettings.appendChild($row), this.#$.allMouseElements[key] = $elm; + } + $rows.appendChild($mouseSettings), this.#$.wrapper.appendChild($rows); + const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({ + label: t("edit"), + tabIndex: -1, + onClick: (e) => this.#toggleEditing(!0) + }), this.#$.activateButton = createButton({ + label: t("activate"), + style: 1, + tabIndex: -1, + onClick: (e) => { + setPref("mkb_default_preset_id", this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh(); + } + })), CE("div", {}, createButton({ + label: t("cancel"), + style: 4, + tabIndex: -1, + onClick: (e) => { + this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1); + } + }), createButton({ + label: t("save"), + style: 1, + tabIndex: -1, + onClick: (e) => { + const updatedPreset = deepClone(this.#getCurrentPreset()); + updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => { + if (id2 === getPref("mkb_default_preset_id")) EmulatedMkbHandler.getInstance().refreshPresetData(); + this.#toggleEditing(!1), this.#refresh(); + }); + } + }))); + return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper; + } } var LOG_TAG3 = "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 #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() { + const 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; + const $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) { + const 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; + } + const baseUrl = "https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts", url = `${baseUrl}/${xboxTitleId}.json`; + try { + const json = await (await NATIVE_FETCH(url)).json(), layouts = {}; + json.layouts.forEach(async (layoutName) => { + let baseLayouts = {}; + if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; + else try { + const layoutUrl = `${baseUrl}/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) { + const listener = (e) => { + if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); + }; + window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); + return; + } + const xboxTitleId = TouchController.#xboxTitleId; + if (!xboxTitleId) { + BxLogger.error(LOG_TAG3, "Invalid xboxTitleId"); + return; + } + if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; + if (!layoutId) { + BxLogger.error(LOG_TAG3, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); + return; + } + const layoutChanged = TouchController.#currentLayoutId !== layoutId; + TouchController.#currentLayoutId = layoutId; + const layoutData = TouchController.#customLayouts[xboxTitleId]; + if (!xboxTitleId || !layoutId || !layoutData) { + TouchController.#enabled && TouchController.#showDefault(); + return; + } + const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; + if (!layout) return; + let msg, html = !1; + if (layout.author) { + const 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 = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => { + TouchController.#customList = json, window.localStorage.setItem("better_xcloud_custom_touch_layouts", JSON.stringify(json)); }); - 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() { - const 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; - const $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) { - const xboxTitleId = TouchController.#xboxTitleId; - if (!xboxTitleId) return; - if (xboxTitleId in TouchController.#customLayouts) { - TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]); - return; + } + static getCustomList() { + return TouchController.#customList; + } + static setup() { + window.testTouchLayout = (layout) => { + const { touchLayoutManager } = window.BX_EXPOSED; + touchLayoutManager && touchLayoutManager.changeLayoutForScope({ + type: "showLayout", + scope: "" + TouchController.#xboxTitleId, + subscope: "base", + layout: { + id: "System.Standard", + displayName: "Custom", + layoutFile: layout } - if (retries = retries || 1, retries > 2) { - TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000); - return; + }); + }; + const $style = document.createElement("style"); + document.documentElement.appendChild($style), TouchController.#$style = $style; + const PREF_STYLE_STANDARD = getPref("stream_touch_controller_style_standard"), PREF_STYLE_CUSTOM = getPref("stream_touch_controller_style_custom"); + window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || 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; } - const baseUrl = "https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts", url = `${baseUrl}/${xboxTitleId}.json`; try { - const json = await (await NATIVE_FETCH(url)).json(), layouts = {}; - json.layouts.forEach(async (layoutName) => { - let baseLayouts = {}; - if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName]; - else try { - const layoutUrl = `${baseUrl}/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); + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content); + if (focused = json.focused, !json.focused) TouchController.#show(); + TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); + } + } catch (e2) { + BxLogger.error(LOG_TAG3, "Load custom layout", e2); } - } - static applyCustomLayout(layoutId, delay = 0) { - if (!window.BX_EXPOSED.touchLayoutManager) { - const listener = (e) => { - if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0); - }; - window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener); - return; - } - const xboxTitleId = TouchController.#xboxTitleId; - if (!xboxTitleId) { - BxLogger.error(LOG_TAG3, "Invalid xboxTitleId"); - return; - } - if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; - if (!layoutId) { - BxLogger.error(LOG_TAG3, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); - return; - } - const layoutChanged = TouchController.#currentLayoutId !== layoutId; - TouchController.#currentLayoutId = layoutId; - const layoutData = TouchController.#customLayouts[xboxTitleId]; - if (!xboxTitleId || !layoutId || !layoutData) { - TouchController.#enabled && TouchController.#showDefault(); - return; - } - const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout]; - if (!layout) return; - let msg, html = !1; - if (layout.author) { - const 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 = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => { - TouchController.#customList = json, window.localStorage.setItem("better_xcloud_custom_touch_layouts", JSON.stringify(json)); - }); - } - static getCustomList() { - return TouchController.#customList; - } - static setup() { - window.testTouchLayout = (layout) => { - const { touchLayoutManager } = window.BX_EXPOSED; - touchLayoutManager && touchLayoutManager.changeLayoutForScope({ - type: "showLayout", - scope: "" + TouchController.#xboxTitleId, - subscope: "base", - layout: { - id: "System.Standard", - displayName: "Custom", - layoutFile: layout - } - }); - }; - const $style = document.createElement("style"); - document.documentElement.appendChild($style), TouchController.#$style = $style; - const PREF_STYLE_STANDARD = getPref("stream_touch_controller_style_standard"), PREF_STYLE_CUSTOM = getPref("stream_touch_controller_style_custom"); - window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || 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")) { - const json = JSON.parse(JSON.parse(msg.data).content); - if (focused = json.focused, !json.focused) TouchController.#show(); - TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString()); - } - } catch (e2) { - BxLogger.error(LOG_TAG3, "Load custom layout", e2); - } - }); - }); - } + }); + }); + } } var VIBRATION_DATA_MAP = { - gamepadIndex: 8, - leftMotorPercent: 8, - rightMotorPercent: 8, - leftTriggerMotorPercent: 8, - rightTriggerMotorPercent: 8, - durationMs: 16 + gamepadIndex: 8, + leftMotorPercent: 8, + rightMotorPercent: 8, + leftTriggerMotorPercent: 8, + rightTriggerMotorPercent: 8, + durationMs: 16 }; class VibrationManager { - static #playDeviceVibration(data) { - if (AppInterface) { - AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); - return; + static #playDeviceVibration(data) { + if (AppInterface) { + AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY); + return; + } + const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; + if (intensity === 0 || intensity === 100) { + window.navigator.vibrate(intensity ? data.durationMs : 0); + return; + } + const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); + window.navigator.vibrate(pulses); + } + static supportControllerVibration() { + return Gamepad.prototype.hasOwnProperty("vibrationActuator"); + } + static supportDeviceVibration() { + return !!window.navigator.vibrate; + } + static updateGlobalVars(stopVibration = !0) { + if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { + window.BX_ENABLE_DEVICE_VIBRATION = !1; + return; + } + stopVibration && window.navigator.vibrate(0); + const value = getPref("controller_device_vibration"); + let enabled; + if (value === "on") enabled = !0; + else if (value === "auto") { + enabled = !0; + const gamepads = window.navigator.getGamepads(); + for (let gamepad of gamepads) + if (gamepad) { + enabled = !1; + break; } - const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY; - if (intensity === 0 || intensity === 100) { - window.navigator.vibrate(intensity ? data.durationMs : 0); - return; - } - const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat(); - window.navigator.vibrate(pulses); - } - static supportControllerVibration() { - return Gamepad.prototype.hasOwnProperty("vibrationActuator"); - } - static supportDeviceVibration() { - return !!window.navigator.vibrate; - } - static updateGlobalVars(stopVibration = !0) { - if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref("controller_enable_vibration") : !1, window.BX_VIBRATION_INTENSITY = getPref("controller_vibration_intensity") / 100, !VibrationManager.supportDeviceVibration()) { - window.BX_ENABLE_DEVICE_VIBRATION = !1; - return; - } - stopVibration && window.navigator.vibrate(0); - const value = getPref("controller_device_vibration"); - let enabled; - if (value === "on") enabled = !0; - else if (value === "auto") { - enabled = !0; - const gamepads = window.navigator.getGamepads(); - for (let gamepad of gamepads) - if (gamepad) { - enabled = !1; - break; - } - } else enabled = !1; - window.BX_ENABLE_DEVICE_VIBRATION = enabled; - } - static #onMessage(e) { - if (!window.BX_ENABLE_DEVICE_VIBRATION) return; - if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; - const dataView = new DataView(e.data); - let 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; - const vibrationType = dataView.getUint8(offset); - if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; - const data = {}; - let 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; - VibrationManager.#playDeviceVibration(data); - } - static initialSetup() { - window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "input") return; - dataChannel.addEventListener("message", VibrationManager.#onMessage); - }); - } + } else enabled = !1; + window.BX_ENABLE_DEVICE_VIBRATION = enabled; + } + static #onMessage(e) { + if (!window.BX_ENABLE_DEVICE_VIBRATION) return; + if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return; + const dataView = new DataView(e.data); + let 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; + const vibrationType = dataView.getUint8(offset); + if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return; + const data = {}; + let 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; + VibrationManager.#playDeviceVibration(data); + } + static initialSetup() { + window.addEventListener("gamepadconnected", (e) => VibrationManager.updateGlobalVars()), window.addEventListener("gamepaddisconnected", (e) => VibrationManager.updateGlobalVars()), VibrationManager.updateGlobalVars(!1), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "input") return; + dataChannel.addEventListener("message", VibrationManager.#onMessage); + }); + } } var controller_shortcuts_default = "if (window.BX_EXPOSED.disableGamepadPolling) {\nthis.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(50) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, 50);\nreturn;\n}\n\nconst currentGamepad = ${gamepadVar};\n\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\nwindow.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\nif (!this.bxHomeStates) {\nthis.bxHomeStates = {};\n}\n\nlet intervalMs = 0;\nlet hijack = false;\n\nif (btnHome.pressed) {\nhijack = true;\nintervalMs = 16;\nthis.gamepadIsIdle.set(currentGamepad.index, false);\n\nif (this.bxHomeStates[currentGamepad.index]) {\nconst lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\nif (currentGamepad.timestamp !== lastTimestamp) {\nthis.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\nconst handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\nif (handled) {\nthis.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n}\n}\n} else {\nwindow.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\nthis.bxHomeStates[currentGamepad.index] = {\nshortcutPressed: 0,\ntimestamp: currentGamepad.timestamp,\n};\n}\n} else if (this.bxHomeStates[currentGamepad.index]) {\nhijack = true;\nconst info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\nthis.bxHomeStates[currentGamepad.index] = null;\n\nif (info.shortcutPressed === 0) {\nconst fakeGamepadMappings = [{\nGamepadIndex: currentGamepad.index,\nA: 0,\nB: 0,\nX: 0,\nY: 0,\nLeftShoulder: 0,\nRightShoulder: 0,\nLeftTrigger: 0,\nRightTrigger: 0,\nView: 0,\nMenu: 0,\nLeftThumb: 0,\nRightThumb: 0,\nDPadUp: 0,\nDPadDown: 0,\nDPadLeft: 0,\nDPadRight: 0,\nNexus: 1,\nLeftThumbXAxis: 0,\nLeftThumbYAxis: 0,\nRightThumbXAxis: 0,\nRightThumbYAxis: 0,\nPhysicalPhysicality: 0,\nVirtualPhysicality: 0,\nDirty: true,\nVirtual: false,\n}];\n\nconst isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\nintervalMs = isLongPress ? 500 : 100;\n\nthis.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n} else {\nintervalMs = 4;\n}\n}\n\nif (hijack && intervalMs) {\nthis.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\nreturn;\n}\n}\n"; var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\norgSetMicrophoneState(state);\n\nconst evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\nevt.microphoneState = state;\n\nwindow.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\nlet updateDimensionsStr = this.updateDimensions.toString();\n\nif (updateDimensionsStr.startsWith('function ')) {\nupdateDimensionsStr = updateDimensionsStr.substring(9);\n}\n\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = 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`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n"; @@ -3939,3979 +3920,3942 @@ var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \" var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\ntry {\nthis.sendKeepAlive();\nreturn;\n} catch (ex) { console.log(ex); }\n}\n"; var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\nreturn void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity === 0) {\nreturn void(0);\n}\n\nif (intensity < 1) {\ne.leftMotorPercent *= intensity;\ne.rightMotorPercent *= intensity;\ne.leftTriggerMotorPercent *= intensity;\ne.rightTriggerMotorPercent *= intensity;\n}\n"; var FeatureGates = { - PwaPrompt: !1, - EnableWifiWarnings: !1, - EnableUpdateRequiredPage: !1, - ShowForcedUpdateScreen: !1 + PwaPrompt: !1, + EnableWifiWarnings: !1, + EnableUpdateRequiredPage: !1, + ShowForcedUpdateScreen: !1 }; if (getPref("ui_home_context_menu_disabled")) FeatureGates.EnableHomeContextMenu = !1; if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1; if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); class PatcherUtils { - static indexOf(txt, searchString, startIndex, maxRange) { - const index = txt.indexOf(searchString, startIndex); - if (index < 0 || maxRange && index - startIndex > maxRange) return -1; - return index; - } - static lastIndexOf(txt, searchString, startIndex, maxRange) { - const index = txt.lastIndexOf(searchString, startIndex); - if (index < 0 || maxRange && startIndex - index > maxRange) return -1; - return 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 indexOf(txt, searchString, startIndex, maxRange) { + const index = txt.indexOf(searchString, startIndex); + if (index < 0 || maxRange && index - startIndex > maxRange) return -1; + return index; + } + static lastIndexOf(txt, searchString, startIndex, maxRange) { + const index = txt.lastIndexOf(searchString, startIndex); + if (index < 0 || maxRange && startIndex - index > maxRange) return -1; + return 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); + } } var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG4 = "Patcher", PATCHES = { - disableAiTrack(str) { - let text = ".track=function("; - const 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; - const 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; - const layout = getPref("ui_layout") === "tv" ? "tv" : "default"; - return str.replace(text, `?"${layout}":"${layout}"`); - }, - remotePlayDirectConnectUrl(str) { - const 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; - return str.replace(text, remote_play_enable_default); - }, - remotePlayDisableAchievementToast(str) { - let text = ".AchievementUnlock:{"; - if (!str.includes(text)) return !1; - const 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; - const newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;"; - return str.replace(text, text + newCode); - }, - blockWebRtcStatsCollector(str) { - let text = "this.shouldCollectStats=!0"; - if (!str.includes(text)) return !1; - return str.replace(text, "this.shouldCollectStats=!1"); - }, - patchPollGamepads(str) { - const index = str.indexOf("},this.pollGamepads=()=>{"); - if (index < 0) return !1; - const nextIndex = str.indexOf("setTimeout(this.pollGamepads", index); - if (nextIndex === -1) return !1; - let codeBlock = str.substring(index, nextIndex); - if (getPref("block_tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); - const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); - if (match) { - const gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, { - gamepadVar - }); - codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"); - } - return str.substring(0, index) + codeBlock + str.substring(nextIndex); - }, - enableXcloudLogger(str) { - let text = "this.telemetryProvider=e}log(e,t,r){"; - if (!str.includes(text)) return !1; - const newCode = ` + disableAiTrack(str) { + let text = ".track=function("; + const 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; + const 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; + const layout = getPref("ui_layout") === "tv" ? "tv" : "default"; + return str.replace(text, `?"${layout}":"${layout}"`); + }, + remotePlayDirectConnectUrl(str) { + const 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; + return str.replace(text, remote_play_enable_default); + }, + remotePlayDisableAchievementToast(str) { + let text = ".AchievementUnlock:{"; + if (!str.includes(text)) return !1; + const 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; + const newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;"; + return str.replace(text, text + newCode); + }, + blockWebRtcStatsCollector(str) { + let text = "this.shouldCollectStats=!0"; + if (!str.includes(text)) return !1; + return str.replace(text, "this.shouldCollectStats=!1"); + }, + patchPollGamepads(str) { + const index = str.indexOf("},this.pollGamepads=()=>{"); + if (index < 0) return !1; + const nextIndex = str.indexOf("setTimeout(this.pollGamepads", index); + if (nextIndex === -1) return !1; + let codeBlock = str.substring(index, nextIndex); + if (getPref("block_tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", ""); + const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/); + if (match) { + const gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, { + gamepadVar + }); + codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"); + } + return str.substring(0, index) + codeBlock + str.substring(nextIndex); + }, + enableXcloudLogger(str) { + let text = "this.telemetryProvider=e}log(e,t,r){"; + if (!str.includes(text)) return !1; + const 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 VibrationManager.updateGlobalVars(), str = str.replaceAll(text, text + vibration_adjust_default), str; - }, - overrideSettings(str) { - const index = str.indexOf(",EnableStreamGate:"); - if (index < 0) return !1; - const endIndex = str.indexOf("},", index); - let newSettings = JSON.stringify(FeatureGates); - newSettings = newSettings.substring(1, newSettings.length - 1); - const newCode = newSettings; - return str = str.substring(0, endIndex) + "," + newCode + str.substring(endIndex), str; - }, - disableGamepadDisconnectedScreen(str) { - const index = str.indexOf('"GamepadDisconnected_Title",'); - if (index < 0) return !1; - const 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; - const newCode = "e.enableTouchInput = true;"; - return str = str.replace(text, text + newCode), str; - }, - loadingEndingChunks(str) { - let text = '"FamilySagaManager"'; - if (!str.includes(text)) return !1; - return BxLogger.info(LOG_TAG4, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; - }, - disableStreamGate(str) { - const index = str.indexOf('case"partially-ready":'); - if (index < 0) return !1; - const 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; - const 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 VibrationManager.updateGlobalVars(), str = str.replaceAll(text, text + vibration_adjust_default), str; + }, + overrideSettings(str) { + const index = str.indexOf(",EnableStreamGate:"); + if (index < 0) return !1; + const endIndex = str.indexOf("},", index); + let newSettings = JSON.stringify(FeatureGates); + newSettings = newSettings.substring(1, newSettings.length - 1); + const newCode = newSettings; + return str = str.substring(0, endIndex) + "," + newCode + str.substring(endIndex), str; + }, + disableGamepadDisconnectedScreen(str) { + const index = str.indexOf('"GamepadDisconnected_Title",'); + if (index < 0) return !1; + const 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; + const newCode = "e.enableTouchInput = true;"; + return str = str.replace(text, text + newCode), str; + }, + loadingEndingChunks(str) { + let text = '"FamilySagaManager"'; + if (!str.includes(text)) return !1; + return BxLogger.info(LOG_TAG4, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str; + }, + disableStreamGate(str) { + const index = str.indexOf('case"partially-ready":'); + if (index < 0) return !1; + const 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; + const 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; - const newCode = ` + return str = str.replace(text, newCode + text), str; + }, + patchBabylonRendererClass(str) { + let index = str.indexOf(".current.render(),"); + if (index < 0) return !1; + index -= 1; + const 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; + 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; - const 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; - const 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 (getPref("stream_touch_controller") === "off") autoOffCode = "return;"; - else if (getPref("stream_touch_controller_auto_off")) autoOffCode = ` + return str = str.substring(0, index) + newCode + str.substring(index), str; + }, + supportLocalCoOp(str) { + let text = "this.gamepadMappingsToSend=[],"; + if (!str.includes(text)) return !1; + const 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; + const 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 (getPref("stream_touch_controller") === "off") autoOffCode = "return;"; + else if (getPref("stream_touch_controller_auto_off")) autoOffCode = ` const gamepads = window.navigator.getGamepads(); let gamepadFound = false; for (let gamepad of gamepads) { - if (gamepad && gamepad.connected) { - gamepadFound = true; - break; - } + if (gamepad && gamepad.connected) { + gamepadFound = true; + break; + } } if (gamepadFound) { - return; + return; } `; - const newCode = ` + const newCode = ` ${autoOffCode} const titleInfo = window.BX_EXPOSED.getTitleInfo(); if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) { - return; + 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 = ` + 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 (getPref("stream_touch_controller") === "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; - const newCode = ` + if (getPref("stream_touch_controller") === "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; + const newCode = ` BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e.toLowerCase()}); `; - 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); - const titleInfoVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].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); + const titleInfoVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].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); - const configsVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[1], newCode = ` + 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); + const configsVar = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[1], newCode = ` Object.assign(${configsVar}.inputConfiguration, { - enableMouseInput: false, - enableKeyboardInput: false, - enableAbsoluteMouse: false, + 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; - const 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; - const 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; - const newCode = `opacityMultiplier: ${(getPref("stream_touch_controller_default_opacity") / 100).toFixed(1)}`; - return str = str.replace(text, newCode), str; - }, - patchShowSensorControls(str) { - let text = "{shouldShowSensorControls:"; - if (!str.includes(text)) return !1; - const 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; - const 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; + const 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; + const 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; + const newCode = `opacityMultiplier: ${(getPref("stream_touch_controller_default_opacity") / 100).toFixed(1)}`; + return str = str.replace(text, newCode), str; + }, + patchShowSensorControls(str) { + let text = "{shouldShowSensorControls:"; + if (!str.includes(text)) return !1; + const 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; + const newCode = `; ${expose_stream_session_default} true` + text; - return str = str.replace(text, newCode), str; - }, - skipFeedbackDialog(str) { - let text = "&&this.shouldTransitionToFeedback("; - if (!str.includes(text)) return !1; - return str = str.replace(text, "&& false " + text), str; - }, - enableNativeMkb(str) { - let text = "e.mouseSupported&&e.keyboardSupported&&e.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; - }, - exposeInputSink(str) { - let text = "this.controlChannel=null,this.inputChannel=null"; - if (!str.includes(text)) return !1; - const newCode = "window.BX_EXPOSED.inputSink = this;"; - return str = str.replace(text, newCode + text), 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; - const match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100)); - if (!match) return !1; - const 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; - }, - 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; - const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), siglIds = [], sections = { - "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", - "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" - }; - PREF_HIDE_SECTIONS.forEach((section) => { - const galleryId = sections[section]; - galleryId && siglIds.push(galleryId); - }); - const 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; - const 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; - const commaIndex = str.indexOf(",", index - 10); - return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str; - }, - patchSetCurrentlyFocusedInteractable(str) { - let index = str.indexOf(".setCurrentlyFocusedInteractable=("); - if (index < 0) return !1; - return index = str.indexOf("{", index) + 1, str = str.substring(0, index) + set_currently_focused_interactable_default + str.substring(index), str; - }, - detectProductDetailsPage(str) { - let index = str.indexOf('{location:"ProductDetailPage",'); - if (index < 0) return !1; - if (index = str.indexOf("return", index - 40), index < 0) return !1; - return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index), str; - }, - detectBrowserRouterReady(str) { - let text = "BrowserRouter:()=>"; - if (!str.includes(text)) return !1; - let index = str.indexOf("{history:this.history,"); - if (index < 0) return !1; - if (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; - } -}, PATCH_ORDERS = [ - ...getPref("native_mkb_enabled") === "on" ? [ - "enableNativeMkb", - "patchMouseAndKeyboardEnabled", - "disableNativeRequestPointerLock", - "exposeInputSink" - ] : [], - "detectBrowserRouterReady", - "patchRequestInfoCrash", - "disableStreamGate", - "overrideSettings", - "broadcastPollingMode", - "patchGamepadPolling", - "exposeStreamSession", - "exposeDialogRoutes", - "guideAchievementsDefaultLocked", - "enableTvRoutes", - AppInterface && "detectProductDetailsPage", - "overrideStorageGetSettings", - getPref("ui_game_card_show_wait_time") && "patchSetCurrentlyFocusedInteractable", - getPref("ui_layout") !== "default" && "websiteLayout", - getPref("local_co_op_enabled") && "supportLocalCoOp", - getPref("game_fortnite_force_console") && "forceFortniteConsole", - getPref("ui_hide_sections").includes("friends") && "ignorePlayWithFriendsSection", - getPref("ui_hide_sections").includes("all-games") && "ignoreAllGamesSection", - getPref("ui_hide_sections").includes("touch") && "ignorePlayWithTouchSection", - (getPref("ui_hide_sections").includes("native-mkb") || getPref("ui_hide_sections").includes("most-popular")) && "ignoreSiglSections", - ...getPref("block_tracking") ? [ - "disableAiTrack", - "disableTelemetry", - "blockWebRtcStatsCollector", - "disableIndexDbLogging", - "disableTelemetryProvider" - ] : [], - ...getPref("xhome_enabled") ? [ - "remotePlayKeepAlive", - "remotePlayDirectConnectUrl", - "remotePlayDisableAchievementToast", - "remotePlayRecentlyUsedTitleIds", - STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync" - ] : [], - ...BX_FLAGS.EnableXcloudLogging ? [ - "enableConsoleLogging", - "enableXcloudLogger" - ] : [] -].filter((item2) => !!item2), PLAYING_PATCH_ORDERS = [ - "patchXcloudTitleInfo", - "disableGamepadDisconnectedScreen", - "patchStreamHud", - "playVibration", - "alwaysShowStreamHud", - getPref("audio_enable_volume_control") && !getPref("stream_combine_sources") && "patchAudioMediaStream", - getPref("audio_enable_volume_control") && getPref("stream_combine_sources") && "patchCombinedAudioVideoMediaStream", - getPref("stream_disable_feedback_dialog") && "skipFeedbackDialog", - ...STATES.userAgent.capabilities.touch ? [ - getPref("stream_touch_controller") === "all" && "patchShowSensorControls", - getPref("stream_touch_controller") === "all" && "exposeTouchLayoutManager", - (getPref("stream_touch_controller") === "off" || getPref("stream_touch_controller_auto_off") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", - getPref("stream_touch_controller_default_opacity") !== 100 && "patchTouchControlDefaultOpacity", - "patchBabylonRendererClass" - ] : [], - BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging", - "patchPollGamepads", - getPref("stream_combine_sources") && "streamCombineSources", - ...getPref("xhome_enabled") ? [ - "patchRemotePlayMkb", - "remotePlayConnectMode" - ] : [] -].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; -class Patcher { - static #patchFunctionBind() { - const 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 (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG4, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; - const orgFunc = this, newFunc = (a, item2) => { - Patcher.patch(item2), orgFunc(a, item2); - }; - return nativeBind.apply(newFunc, arguments); - }; - } - static patch(item) { - let patchesToCheck, appliedPatches; - const patchesMap = {}; - for (let id in item[1]) { - appliedPatches = []; - const cachedPatches = PatcherCache.getPatches(id); - if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS); - else patchesToCheck = PATCH_ORDERS.slice(0); - if (!patchesToCheck.length) continue; - const func = item[1][id], funcStr = func.toString(); - let patchedFuncStr = funcStr, modified = !1; - for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) { - const patchName = patchesToCheck[patchIndex]; - if (appliedPatches.indexOf(patchName) > -1) continue; - if (!PATCHES[patchName]) continue; - const tmpStr = PATCHES[patchName].call(null, patchedFuncStr); - if (!tmpStr) continue; - modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG4, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); - } - if (modified) try { - item[1][id] = eval(patchedFuncStr); - } catch (e) { - if (e instanceof Error) BxLogger.error(LOG_TAG4, "Error", appliedPatches, e.message, patchedFuncStr); - } - if (appliedPatches.length) patchesMap[id] = appliedPatches; - } - if (Object.keys(patchesMap).length) PatcherCache.saveToCache(patchesMap); - } - static init() { - Patcher.#patchFunctionBind(); - } -} -class PatcherCache { - static #KEY_CACHE = "better_xcloud_patches_cache"; - static #KEY_SIGNATURE = "better_xcloud_patches_cache_signature"; - static #CACHE; - static #isInitialized = !1; - static #getSignature() { - const scriptVersion = SCRIPT_VERSION, webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content, patches = JSON.stringify(ALL_PATCHES); - return hashCode(scriptVersion + webVersion + patches); - } - static clear() { - window.localStorage.removeItem(PatcherCache.#KEY_CACHE), PatcherCache.#CACHE = {}; - } - static checkSignature() { - const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature(); - if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG4, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear(); - else BxLogger.info(LOG_TAG4, "Signature unchanged"); - } - static #cleanupPatches(patches) { - return patches.filter((item2) => { - for (let id2 in PatcherCache.#CACHE) - if (PatcherCache.#CACHE[id2].includes(item2)) return !1; - return !0; - }); - } - static getPatches(id2) { - return PatcherCache.#CACHE[id2]; - } - static saveToCache(subCache) { - for (let id2 in subCache) { - const patchNames = subCache[id2]; - let data = PatcherCache.#CACHE[id2]; - if (!data) PatcherCache.#CACHE[id2] = patchNames; - else for (let patchName of patchNames) - if (!data.includes(patchName)) data.push(patchName); - } - window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE)); - } - static init() { - if (PatcherCache.#isInitialized) return; - if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG4, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); - else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); - PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG4, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG4, PLAYING_PATCH_ORDERS.slice(0)); - } -} -class FullscreenText { - static instance; - static getInstance() { - if (!FullscreenText.instance) FullscreenText.instance = new FullscreenText; - return FullscreenText.instance; - } - $text; - 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 SettingsNavigationDialog extends NavigationDialog { - static instance; - static getInstance() { - if (!SettingsNavigationDialog.instance) SettingsNavigationDialog.instance = new SettingsNavigationDialog; - return SettingsNavigationDialog.instance; - } - $container; - $tabs; - $settings; - $btnReload; - $btnGlobalReload; - $noteGlobalReload; - $btnSuggestion; - renderFullSettings; - suggestedSettings = { - recommended: {}, - default: {}, - lowest: {}, - highest: {} + return str = str.replace(text, newCode), str; + }, + skipFeedbackDialog(str) { + let text = "&&this.shouldTransitionToFeedback("; + if (!str.includes(text)) return !1; + return str = str.replace(text, "&& false " + text), str; + }, + enableNativeMkb(str) { + let text = "e.mouseSupported&&e.keyboardSupported&&e.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; + }, + exposeInputSink(str) { + let text = "this.controlChannel=null,this.inputChannel=null"; + if (!str.includes(text)) return !1; + const newCode = "window.BX_EXPOSED.inputSink = this;"; + return str = str.replace(text, newCode + text), 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; + const match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100)); + if (!match) return !1; + const 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; + }, + 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; + const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), siglIds = [], sections = { + "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486", + "most-popular": "e7590b22-e299-44db-ae22-25c61405454c" }; - suggestedSettingLabels = {}; - settingElements = {}; - TAB_GLOBAL_ITEMS = [{ - group: "general", - label: t("better-xcloud"), - helpUrl: "https://better-xcloud.github.io/features/", - items: [ - ($parent) => { - const PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; - if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { - const opts = { - label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), - style: 1 | 32 | 64 - }; - 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: 64 | 32, - onClick: (e) => { - AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); - } - })); - else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ - label: "🔥 " + t("install-android"), - style: 64 | 32, - url: "https://better-xcloud.github.io/android" - })); - this.$btnGlobalReload = createButton({ - label: t("settings-reload"), - classes: ["bx-settings-reload-button", "bx-gone"], - style: 32 | 64, - 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", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); - const $div = CE("div", { - class: "bx-top-buttons", - _nearby: { - orientation: "vertical" - } - }, ...topButtons); - $parent.appendChild($div); - }, - "bx_locale", - "server_bypass_restriction", - "ui_controller_friendly", - "xhome_enabled" - ] - }, { - group: "server", - label: t("server"), - items: [ - "server_region", - "stream_preferred_locale", - "prefer_ipv6_server" - ] - }, { - group: "stream", - label: t("stream"), - items: [ - "stream_target_resolution", - "stream_codec_profile", - "bitrate_video_max", - "audio_enable_volume_control", - "stream_disable_feedback_dialog", - "screenshot_apply_filters", - "audio_mic_on_playing", - "game_fortnite_force_console", - "stream_combine_sources" - ] - }, { - requiredVariants: "full", - group: "co-op", - label: t("local-co-op"), - items: [ - "local_co_op_enabled" - ] - }, { - requiredVariants: "full", - group: "mkb", - label: t("mouse-and-keyboard"), - unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { - href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", - target: "_blank" - }, "⚠️ " + t("browser-unsupported-feature")) : null, - unsupported: !STATES.userAgent.capabilities.mkb, - items: [ - "native_mkb_enabled", - "game_msfs2020_force_native_mkb", - "mkb_enabled", - "mkb_hide_idle_cursor" - ] - }, { - requiredVariants: "full", - group: "touch-control", - label: t("touch-controller"), - unsupported: !STATES.userAgent.capabilities.touch, - unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, - items: [ - "stream_touch_controller", - "stream_touch_controller_auto_off", - "stream_touch_controller_default_opacity", - "stream_touch_controller_style_standard", - "stream_touch_controller_style_custom" - ] - }, { - group: "ui", - label: t("ui"), - items: [ - "ui_layout", - "ui_game_card_show_wait_time", - "ui_home_context_menu_disabled", - "controller_show_connection_status", - "stream_simplify_menu", - "skip_splash_video", - !AppInterface && "ui_scrollbar_hide", - "hide_dots_icon", - "reduce_animations", - "block_social_features", - "ui_hide_sections" - ] - }, { - requiredVariants: "full", - group: "game-bar", - label: t("game-bar"), - items: [ - "game_bar_position" - ] - }, { - group: "loading-screen", - label: t("loading-screen"), - items: [ - "ui_loading_screen_game_art", - "ui_loading_screen_wait_time", - "ui_loading_screen_rocket" - ] - }, { - group: "other", - label: t("other"), - items: [ - "block_tracking" - ] - }, { - group: "advanced", - label: t("advanced"), - items: [ - { - pref: "user_agent_profile", - onCreated: (setting, $control) => { - const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { - id: `bx_setting_inp_${setting.pref}`, - type: "text", - placeholder: defaultUserAgent, - autocomplete: "off", - class: "bx-settings-custom-user-agent", - tabindex: 0 - }); - $inpCustomUserAgent.addEventListener("input", (e) => { - const 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) => { - $parent.appendChild(CE("a", { - class: "bx-donation-link", - href: "https://ko-fi.com/redphx", - target: "_blank", - tabindex: 0 - }, `❤️ ${t("support-better-xcloud")}`)); - }, - ($parent) => { - try { - const 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) => { - const debugInfo = deepClone(BX_FLAGS.DeviceInfo); - debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); - const $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ - label: "Debug info", - style: 4 | 64 | 32, - onClick: (e) => { - const $pre = e.target.closest("button")?.nextElementSibling; - $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); - } - }), CE("pre", { - class: "bx-focusable bx-gone", - tabindex: 0, - on: { - click: async (e) => { - await copyToClipboard(e.target.innerText); - } - } - }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); - $parent.appendChild($debugInfo); - } - ] - }]; - TAB_DISPLAY_ITEMS = [{ - requiredVariants: "full", - group: "audio", - label: t("audio"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", - items: [{ - pref: "audio_volume", - onChange: (e, value) => { - SoundShortcut.setGainNodeVolume(value); - }, - params: { - disabled: !getPref("audio_enable_volume_control") - }, - onCreated: (setting, $elm) => { - const $range = $elm.querySelector("input[type=range"); - window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { - const { storageKey, settingKey, settingValue } = e; - if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; - $range.value = settingValue, BxEvent.dispatch($range, "input", { - ignoreOnChange: !0 - }); - }); - } - }] - }, { - group: "video", - label: t("video"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#video", - items: [{ - pref: "video_player_type", - onChange: onChangeVideoPlayerType - }, { - pref: "video_max_fps", - onChange: limitVideoPlayerFps - }, { - pref: "video_power_preference", - onChange: () => { - const streamPlayer = STATES.currentStream.streamPlayer; - if (!streamPlayer) return; - streamPlayer.reloadPlayer(), updateVideoPlayer(); - } - }, { - pref: "video_processing", - onChange: updateVideoPlayer - }, { - pref: "video_ratio", - onChange: updateVideoPlayer - }, { - pref: "video_sharpness", - onChange: updateVideoPlayer - }, { - pref: "video_saturation", - onChange: updateVideoPlayer - }, { - pref: "video_contrast", - onChange: updateVideoPlayer - }, { - pref: "video_brightness", - onChange: updateVideoPlayer - }] - }]; - TAB_CONTROLLER_ITEMS = [ - { - group: "controller", - label: t("controller"), - helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", - items: [{ - pref: "controller_enable_vibration", - unsupported: !VibrationManager.supportControllerVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, { - pref: "controller_device_vibration", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { - pref: "controller_vibration_intensity", - unsupported: !VibrationManager.supportDeviceVibration(), - onChange: () => VibrationManager.updateGlobalVars() - }] - }, - STATES.userAgent.capabilities.touch && { - group: "touch-control", - label: t("touch-controller"), - items: [{ - label: t("layout"), - content: CE("select", { - disabled: !0 - }, CE("option", {}, t("default"))), - onCreated: (setting, $elm) => { - $elm.addEventListener("input", (e) => { - TouchController.applyCustomLayout($elm.value, 1000); - }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { - const 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; - } - const $fragment = document.createDocumentFragment(); - for (let key in customLayouts.layouts) { - const layout = customLayouts.layouts[key]; - let name; - if (layout.author) name = `${layout.name} (${layout.author})`; - else name = layout.name; - const $option = CE("option", { value: key }, name); - $fragment.appendChild($option); - } - $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; - }); - } - }] - } - ]; - TAB_VIRTUAL_CONTROLLER_ITEMS = [{ - group: "mkb", - label: t("virtual-controller"), - helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", - content: MkbRemapper.INSTANCE.render() - }]; - TAB_NATIVE_MKB_ITEMS = [{ - requiredVariants: "full", - group: "native-mkb", - label: t("native-mkb"), - items: [{ - pref: "native_mkb_scroll_y_sensitivity", - onChange: (e, value) => { - NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); - } - }, { - pref: "native_mkb_scroll_x_sensitivity", - onChange: (e, value) => { - NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); - } - }] - }]; - TAB_SHORTCUTS_ITEMS = [{ - requiredVariants: "full", - group: "controller-shortcuts", - label: t("controller-shortcuts"), - content: ControllerShortcut.renderSettings() - }]; - TAB_STATS_ITEMS = [{ - group: "stats", - label: t("stream-stats"), - helpUrl: "https://better-xcloud.github.io/stream-stats/", - items: [ - { - pref: "stats_show_when_playing" - }, - { - pref: "stats_quick_glance", - onChange: (e) => { - const streamStats = StreamStats.getInstance(); - e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); - } - }, - { - pref: "stats_items", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_position", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_text_size", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_opacity", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_transparent", - onChange: StreamStats.refreshStyles - }, - { - pref: "stats_conditional_formatting", - onChange: StreamStats.refreshStyles - } - ] - }]; - SETTINGS_UI = [ - { - icon: BxIcon.HOME, - group: "global", - items: this.TAB_GLOBAL_ITEMS - }, - { - icon: BxIcon.DISPLAY, - group: "stream", - items: this.TAB_DISPLAY_ITEMS - }, - { - icon: BxIcon.CONTROLLER, - group: "controller", - items: this.TAB_CONTROLLER_ITEMS, - requiredVariants: "full" - }, - getPref("mkb_enabled") && { - icon: BxIcon.VIRTUAL_CONTROLLER, - group: "mkb", - items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, - requiredVariants: "full" - }, - AppInterface && getPref("native_mkb_enabled") === "on" && { - icon: BxIcon.NATIVE_MKB, - group: "native-mkb", - items: this.TAB_NATIVE_MKB_ITEMS, - requiredVariants: "full" - }, - { - icon: BxIcon.COMMAND, - group: "shortcuts", - items: this.TAB_SHORTCUTS_ITEMS, - requiredVariants: "full" - }, - { - icon: BxIcon.STREAM_STATS, - group: "stats", - items: this.TAB_STATS_ITEMS - } - ]; - constructor() { - super(); - this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); - } - getDialog() { - return this; - } - getContent() { - return this.$container; - } - onMounted() { - if (!this.renderFullSettings) return; - if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); - const $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); - if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; - } - reloadPage() { - this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); - } - async getRecommendedSettings(deviceCode) { - try { - const json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {}; - if (json.schema_version !== 1) return null; - const 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) {} + PREF_HIDE_SECTIONS.forEach((section) => { + const galleryId = sections[section]; + galleryId && siglIds.push(galleryId); + }); + const newCode = ` +if (e && e.id) { + const siglId = e.id; + if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) { return null; } - addDefaultSuggestedSetting(prefKey, value) { - let key; - for (key in this.suggestedSettings) - if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; +} +`; + return str = PatcherUtils.insertAt(str, index, newCode), str; + }, + overrideStorageGetSettings(str) { + let text = "}getSetting(e){"; + if (!str.includes(text)) return !1; + const 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]; } - 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] = getPrefDefinition(prefKey).default; +} +`; + 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; + const commaIndex = str.indexOf(",", index - 10); + return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str; + }, + patchSetCurrentlyFocusedInteractable(str) { + let index = str.indexOf(".setCurrentlyFocusedInteractable=("); + if (index < 0) return !1; + return index = str.indexOf("{", index) + 1, str = str.substring(0, index) + set_currently_focused_interactable_default + str.substring(index), str; + }, + detectProductDetailsPage(str) { + let index = str.indexOf('{location:"ProductDetailPage",'); + if (index < 0) return !1; + if (index = str.indexOf("return", index - 40), index < 0) return !1; + return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, {component: "product-details"});' + str.substring(index), str; + }, + detectBrowserRouterReady(str) { + let text = "BrowserRouter:()=>"; + if (!str.includes(text)) return !1; + let index = str.indexOf("{history:this.history,"); + if (index < 0) return !1; + if (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; + } +}, PATCH_ORDERS = [ + ...getPref("native_mkb_enabled") === "on" ? [ + "enableNativeMkb", + "patchMouseAndKeyboardEnabled", + "disableNativeRequestPointerLock", + "exposeInputSink" + ] : [], + "detectBrowserRouterReady", + "patchRequestInfoCrash", + "disableStreamGate", + "overrideSettings", + "broadcastPollingMode", + "patchGamepadPolling", + "exposeStreamSession", + "exposeDialogRoutes", + "guideAchievementsDefaultLocked", + "enableTvRoutes", + AppInterface && "detectProductDetailsPage", + "overrideStorageGetSettings", + getPref("ui_game_card_show_wait_time") && "patchSetCurrentlyFocusedInteractable", + getPref("ui_layout") !== "default" && "websiteLayout", + getPref("local_co_op_enabled") && "supportLocalCoOp", + getPref("game_fortnite_force_console") && "forceFortniteConsole", + getPref("ui_hide_sections").includes("friends") && "ignorePlayWithFriendsSection", + getPref("ui_hide_sections").includes("all-games") && "ignoreAllGamesSection", + getPref("ui_hide_sections").includes("touch") && "ignorePlayWithTouchSection", + (getPref("ui_hide_sections").includes("native-mkb") || getPref("ui_hide_sections").includes("most-popular")) && "ignoreSiglSections", + ...getPref("block_tracking") ? [ + "disableAiTrack", + "disableTelemetry", + "blockWebRtcStatsCollector", + "disableIndexDbLogging", + "disableTelemetryProvider" + ] : [], + ...getPref("xhome_enabled") ? [ + "remotePlayKeepAlive", + "remotePlayDirectConnectUrl", + "remotePlayDisableAchievementToast", + "remotePlayRecentlyUsedTitleIds", + STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync" + ] : [], + ...BX_FLAGS.EnableXcloudLogging ? [ + "enableConsoleLogging", + "enableXcloudLogger" + ] : [] +].filter((item2) => !!item2), PLAYING_PATCH_ORDERS = [ + "patchXcloudTitleInfo", + "disableGamepadDisconnectedScreen", + "patchStreamHud", + "playVibration", + "alwaysShowStreamHud", + getPref("audio_enable_volume_control") && !getPref("stream_combine_sources") && "patchAudioMediaStream", + getPref("audio_enable_volume_control") && getPref("stream_combine_sources") && "patchCombinedAudioVideoMediaStream", + getPref("stream_disable_feedback_dialog") && "skipFeedbackDialog", + ...STATES.userAgent.capabilities.touch ? [ + getPref("stream_touch_controller") === "all" && "patchShowSensorControls", + getPref("stream_touch_controller") === "all" && "exposeTouchLayoutManager", + (getPref("stream_touch_controller") === "off" || getPref("stream_touch_controller_auto_off") || !STATES.userAgent.capabilities.touch) && "disableTakRenderer", + getPref("stream_touch_controller_default_opacity") !== 100 && "patchTouchControlDefaultOpacity", + "patchBabylonRendererClass" + ] : [], + BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging", + "patchPollGamepads", + getPref("stream_combine_sources") && "streamCombineSources", + ...getPref("xhome_enabled") ? [ + "patchRemotePlayMkb", + "remotePlayConnectMode" + ] : [] +].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS]; +class Patcher { + static #patchFunctionBind() { + const 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 (PatcherCache.init(), typeof arguments[1] === "function") BxLogger.info(LOG_TAG4, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind; + const orgFunc = this, newFunc = (a, item2) => { + Patcher.patch(item2), orgFunc(a, item2); + }; + return nativeBind.apply(newFunc, arguments); + }; + } + static patch(item) { + let patchesToCheck, appliedPatches; + const patchesMap = {}; + for (let id in item[1]) { + appliedPatches = []; + const cachedPatches = PatcherCache.getPatches(id); + if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS); + else patchesToCheck = PATCH_ORDERS.slice(0); + if (!patchesToCheck.length) continue; + const func = item[1][id], funcStr = func.toString(); + let patchedFuncStr = funcStr, modified = !1; + for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) { + const patchName = patchesToCheck[patchIndex]; + if (appliedPatches.indexOf(patchName) > -1) continue; + if (!PATCHES[patchName]) continue; + const tmpStr = PATCHES[patchName].call(null, patchedFuncStr); + if (!tmpStr) continue; + modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG4, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName); + } + if (modified) try { + item[1][id] = eval(patchedFuncStr); + } catch (e) { + if (e instanceof Error) BxLogger.error(LOG_TAG4, "Error", appliedPatches, e.message, patchedFuncStr); } + if (appliedPatches.length) patchesMap[id] = appliedPatches; } - isSupportedVariant(requiredVariants) { - if (typeof requiredVariants === "undefined") return !0; - return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + if (Object.keys(patchesMap).length) PatcherCache.saveToCache(patchesMap); + } + static init() { + Patcher.#patchFunctionBind(); + } +} +class PatcherCache { + static #KEY_CACHE = "better_xcloud_patches_cache"; + static #KEY_SIGNATURE = "better_xcloud_patches_cache_signature"; + static #CACHE; + static #isInitialized = !1; + static #getSignature() { + const scriptVersion = SCRIPT_VERSION, webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content, patches = JSON.stringify(ALL_PATCHES); + return hashCode(scriptVersion + webVersion + patches); + } + static clear() { + window.localStorage.removeItem(PatcherCache.#KEY_CACHE), PatcherCache.#CACHE = {}; + } + static checkSignature() { + const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature(); + if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG4, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear(); + else BxLogger.info(LOG_TAG4, "Signature unchanged"); + } + static #cleanupPatches(patches) { + return patches.filter((item2) => { + for (let id2 in PatcherCache.#CACHE) + if (PatcherCache.#CACHE[id2].includes(item2)) return !1; + return !0; + }); + } + static getPatches(id2) { + return PatcherCache.#CACHE[id2]; + } + static saveToCache(subCache) { + for (let id2 in subCache) { + const patchNames = subCache[id2]; + let data = PatcherCache.#CACHE[id2]; + if (!data) PatcherCache.#CACHE[id2] = patchNames; + else for (let patchName of patchNames) + if (!data.includes(patchName)) data.push(patchName); } - async renderSuggestions(e) { - const $btnSuggest = e.target.closest("div"); - $btnSuggest.toggleAttribute("bx-open"); - let $content = $btnSuggest.nextElementSibling; - if ($content) { - BxEvent.dispatch($content.querySelector("select"), "input"); - return; + window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE)); + } + static init() { + if (PatcherCache.#isInitialized) return; + if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG4, PatcherCache.#CACHE), window.location.pathname.includes("/play/")) PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS); + else PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME); + PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG4, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG4, PLAYING_PATCH_ORDERS.slice(0)); + } +} +class FullscreenText { + static instance; + static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText); + $text; + 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 SettingsNavigationDialog extends NavigationDialog { + static instance; + static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog); + $container; + $tabs; + $settings; + $btnReload; + $btnGlobalReload; + $noteGlobalReload; + $btnSuggestion; + renderFullSettings; + suggestedSettings = { + recommended: {}, + default: {}, + lowest: {}, + highest: {} + }; + suggestedSettingLabels = {}; + settingElements = {}; + TAB_GLOBAL_ITEMS = [{ + group: "general", + label: t("better-xcloud"), + helpUrl: "https://better-xcloud.github.io/features/", + items: [ + ($parent) => { + const PREF_LATEST_VERSION = getPref("version_latest"), topButtons = []; + if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) { + const opts = { + label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }), + style: 1 | 32 | 64 + }; + if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript(); + else opts.url = "https://github.com/redphx/better-xcloud/releases/latest"; + topButtons.push(createButton(opts)); } - for (let settingTab of this.SETTINGS_UI) { - if (!settingTab || !settingTab.items) continue; - for (let settingTabContent of settingTab.items) { - if (!settingTabContent || !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.suggestedSettingLabels[prefKey] = settingTabContent.label; - } - } - } - let recommendedDevice = ""; - if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { - if (BX_FLAGS.DeviceInfo.androidInfo) { - const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board; - recommendedDevice = await this.getRecommendedSettings(deviceCode); - } - } - const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; - if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); - else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); - else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); - this.generateDefaultSuggestedSettings(); - const $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, 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) => { - const profile = $select.value; - removeChildElements($suggestedSettings); - const fragment = document.createDocumentFragment(); - let 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)); - const settings = this.suggestedSettings[profile]; - let prefKey; - for (prefKey in settings) { - const currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue; - let $child, $value; - if (isSameValue) $value = currentValueText; - else { - const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); - $value = currentValueText + " ➔ " + suggestedValueText; - } - let $checkbox; - const breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(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: `bx_suggest_${prefKey}` - }), CE("label", { - for: `bx_suggest_${prefKey}` - }, 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"); - const onClickApply = () => { - const profile = $select.value, settings = this.suggestedSettings[profile]; - let prefKey; - for (prefKey in settings) { - const suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); - if (!$checkBox.checked || $checkBox.disabled) continue; - const $control = this.settingElements[prefKey]; - if (!$control) { - setPref(prefKey, suggestedValue); - continue; - } - 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"), + if (AppInterface) topButtons.push(createButton({ + label: t("app-settings"), + icon: BxIcon.STREAM_SETTINGS, style: 64 | 32, - onClick: onClickApply - }); - $content = CE("div", { - class: "bx-suggest-box", - _nearby: { - orientation: "vertical" - } - }, BxSelectElement.wrap($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); - } - renderTab(settingTab) { - const $svg = createSvgIcon(settingTab.icon); - return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => { - for (let $child of Array.from(this.$settings.children)) - if ($child.getAttribute("data-tab-group") === settingTab.group) { - if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); - } else $child.classList.add("bx-gone"); - for (let $child of Array.from(this.$tabs.children)) - $child.classList.remove("bx-active"); - $svg.classList.add("bx-active"); - }), $svg; - } - onGlobalSettingChanged(e) { - PatcherCache.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"); - } - renderServerSetting(setting) { - let selectedValue; - const $control = CE("select", { - id: `bx_setting_${setting.pref}`, - title: setting.label, - tabindex: 0 - }); - $control.name = $control.id, $control.addEventListener("input", (e) => { - setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e); - }), selectedValue = getPref("server_region"), setting.options = {}; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - let value = regionName, label = `${region.shortName} - ${regionName}`; - if (region.isDefault) { - if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; - } - setting.options[value] = label; - } - for (let value in setting.options) { - const label = setting.options[value], $option = CE("option", { value }, label); - $control.appendChild($option); - } - return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; - } - renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { - if (typeof setting === "string") setting = { - pref: setting - }; - const pref = setting.pref; - let $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, STORAGE.Global, async (e) => { - const newLocale = e.target.value; - if (getPref("ui_controller_friendly")) { - 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 === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { - const value = e.target.value; - let isCustom = value === "custom", userAgent2 = UserAgent.get(value); - UserAgent.updateStorage(value); - const $inp = $control.nextElementSibling; - $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); - }); - else { - let onChange = setting.onChange; - if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); - $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); - } - if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); - pref && (this.settingElements[pref] = $control); - } - let prefDefinition = null; - if (pref) prefDefinition = getPrefDefinition(pref); - if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; - let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote; - const experimental = prefDefinition?.experimental || setting.experimental; - 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 $label; - const $row = CE("label", { - class: "bx-settings-row", - for: `bx_setting_${pref}`, - "data-type": settingTabContent.group, - _nearby: { - orientation: "horizontal" - } - }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); - if ($link) $link.classList.add("bx-focusable"), setNearby($label, { - focus: $link - }); - $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); - } - setupDialog() { - let $tabs, $settings; - const $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", {}, this.$btnReload = createButton({ - icon: BxIcon.REFRESH, - style: 32 | 16, onClick: (e) => { - this.reloadPage(); + AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide(); } - }), createButton({ - icon: BxIcon.CLOSE, - style: 32 | 16, - onClick: (e) => { - this.dialogManager.hide(); + })); + else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({ + label: "🔥 " + t("install-android"), + style: 64 | 32, + url: "https://better-xcloud.github.io/android" + })); + this.$btnGlobalReload = createButton({ + label: t("settings-reload"), + classes: ["bx-settings-reload-button", "bx-gone"], + style: 32 | 64, + 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", {}, t("suggest-settings")), CE("span", {}, "❯")), this.$btnSuggestion.addEventListener("click", this.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion); + const $div = CE("div", { + class: "bx-top-buttons", + _nearby: { + orientation: "vertical" + } + }, ...topButtons); + $parent.appendChild($div); + }, + "bx_locale", + "server_bypass_restriction", + "ui_controller_friendly", + "xhome_enabled" + ] + }, { + group: "server", + label: t("server"), + items: [ + "server_region", + "stream_preferred_locale", + "prefer_ipv6_server" + ] + }, { + group: "stream", + label: t("stream"), + items: [ + "stream_target_resolution", + "stream_codec_profile", + "bitrate_video_max", + "audio_enable_volume_control", + "stream_disable_feedback_dialog", + "screenshot_apply_filters", + "audio_mic_on_playing", + "game_fortnite_force_console", + "stream_combine_sources" + ] + }, { + requiredVariants: "full", + group: "co-op", + label: t("local-co-op"), + items: [ + "local_co_op_enabled" + ] + }, { + requiredVariants: "full", + group: "mkb", + label: t("mouse-and-keyboard"), + unsupportedNote: !STATES.userAgent.capabilities.mkb ? CE("a", { + href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657", + target: "_blank" + }, "⚠️ " + t("browser-unsupported-feature")) : null, + unsupported: !STATES.userAgent.capabilities.mkb, + items: [ + "native_mkb_enabled", + "game_msfs2020_force_native_mkb", + "mkb_enabled", + "mkb_hide_idle_cursor" + ] + }, { + requiredVariants: "full", + group: "touch-control", + label: t("touch-controller"), + unsupported: !STATES.userAgent.capabilities.touch, + unsupportedNote: !STATES.userAgent.capabilities.touch ? "⚠️ " + t("device-unsupported-touch") : null, + items: [ + "stream_touch_controller", + "stream_touch_controller_auto_off", + "stream_touch_controller_default_opacity", + "stream_touch_controller_style_standard", + "stream_touch_controller_style_custom" + ] + }, { + group: "ui", + label: t("ui"), + items: [ + "ui_layout", + "ui_game_card_show_wait_time", + "ui_home_context_menu_disabled", + "controller_show_connection_status", + "stream_simplify_menu", + "skip_splash_video", + !AppInterface && "ui_scrollbar_hide", + "hide_dots_icon", + "reduce_animations", + "block_social_features", + "ui_hide_sections" + ] + }, { + requiredVariants: "full", + group: "game-bar", + label: t("game-bar"), + items: [ + "game_bar_position" + ] + }, { + group: "loading-screen", + label: t("loading-screen"), + items: [ + "ui_loading_screen_game_art", + "ui_loading_screen_wait_time", + "ui_loading_screen_rocket" + ] + }, { + group: "other", + label: t("other"), + items: [ + "block_tracking" + ] + }, { + group: "advanced", + label: t("advanced"), + items: [ + { + pref: "user_agent_profile", + onCreated: (setting, $control) => { + const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", { + id: `bx_setting_inp_${setting.pref}`, + type: "text", + placeholder: defaultUserAgent, + autocomplete: "off", + class: "bx-settings-custom-user-agent", + tabindex: 0 + }); + $inpCustomUserAgent.addEventListener("input", (e) => { + const 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) => { + $parent.appendChild(CE("a", { + class: "bx-donation-link", + href: "https://ko-fi.com/redphx", + target: "_blank", + tabindex: 0 + }, `❤️ ${t("support-better-xcloud")}`)); + }, + ($parent) => { + try { + const 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) => { + const debugInfo = deepClone(BX_FLAGS.DeviceInfo); + debugInfo.settings = JSON.parse(window.localStorage.getItem("better_xcloud") || "{}"); + const $debugInfo = CE("div", { class: "bx-debug-info" }, createButton({ + label: "Debug info", + style: 4 | 64 | 32, + onClick: (e) => { + const $pre = e.target.closest("button")?.nextElementSibling; + $pre.classList.toggle("bx-gone"), $pre.scrollIntoView(); + } + }), CE("pre", { + class: "bx-focusable bx-gone", + tabindex: 0, + on: { + click: async (e) => { + await copyToClipboard(e.target.innerText); } - }))), $settings = CE("div", { - class: "bx-settings-tab-contents", - _nearby: { - orientation: "vertical", - focus: () => this.jumpToSettingGroup("next"), - loop: (direction) => { - if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; - return !1; - } - } - })); - this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => { - if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + } + }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```")); + $parent.appendChild($debugInfo); + } + ] + }]; + TAB_DISPLAY_ITEMS = [{ + requiredVariants: "full", + group: "audio", + label: t("audio"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#audio", + items: [{ + pref: "audio_volume", + onChange: (e, value) => { + SoundShortcut.setGainNodeVolume(value); + }, + params: { + disabled: !getPref("audio_enable_volume_control") + }, + onCreated: (setting, $elm) => { + const $range = $elm.querySelector("input[type=range"); + window.addEventListener(BxEvent.SETTINGS_CHANGED, (e) => { + const { storageKey, settingKey, settingValue } = e; + if (storageKey !== "better_xcloud" || settingKey !== "audio_volume") return; + $range.value = settingValue, BxEvent.dispatch($range, "input", { + ignoreOnChange: !0 + }); }); - for (let settingTab of this.SETTINGS_UI) { - if (!settingTab) continue; - if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; - if (settingTab.group !== "global" && !this.renderFullSettings) continue; - const $svg = this.renderTab(settingTab); - $tabs.appendChild($svg); - const $tabContent = CE("div", { - class: "bx-gone", - "data-tab-group": settingTab.group - }); - for (let settingTabContent of settingTab.items) { - if (settingTabContent === !1) continue; - if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; - if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; - let label = settingTabContent.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: 1024 | 8 | 32 - }); - } - if (label) { - const $title = CE("h2", { - _nearby: { - orientation: "horizontal" - } - }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ - icon: BxIcon.QUESTION, - style: 4 | 32, - url: settingTabContent.helpUrl, - title: t("help") - })); - $tabContent.appendChild($title); - } - if (settingTabContent.unsupportedNote) { - const $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); - $tabContent.appendChild($note); - } - if (settingTabContent.unsupported) continue; - if (settingTabContent.content) { - $tabContent.appendChild(settingTabContent.content); - continue; - } - settingTabContent.items = settingTabContent.items || []; - for (let setting of settingTabContent.items) { - if (setting === !1) continue; - if (typeof setting === "function") { - setting.apply(this, [$tabContent]); - continue; - } - this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); - } + } + }] + }, { + group: "video", + label: t("video"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#video", + items: [{ + pref: "video_player_type", + onChange: onChangeVideoPlayerType + }, { + pref: "video_max_fps", + onChange: (e) => { + limitVideoPlayerFps(parseInt(e.target.value)); + } + }, { + pref: "video_power_preference", + onChange: () => { + const streamPlayer = STATES.currentStream.streamPlayer; + if (!streamPlayer) return; + streamPlayer.reloadPlayer(), updateVideoPlayer(); + } + }, { + pref: "video_processing", + onChange: updateVideoPlayer + }, { + pref: "video_ratio", + onChange: updateVideoPlayer + }, { + pref: "video_sharpness", + onChange: updateVideoPlayer + }, { + pref: "video_saturation", + onChange: updateVideoPlayer + }, { + pref: "video_contrast", + onChange: updateVideoPlayer + }, { + pref: "video_brightness", + onChange: updateVideoPlayer + }] + }]; + TAB_CONTROLLER_ITEMS = [ + { + group: "controller", + label: t("controller"), + helpUrl: "https://better-xcloud.github.io/ingame-features/#controller", + items: [{ + pref: "controller_enable_vibration", + unsupported: !VibrationManager.supportControllerVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, { + pref: "controller_device_vibration", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }, (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && { + pref: "controller_vibration_intensity", + unsupported: !VibrationManager.supportDeviceVibration(), + onChange: () => VibrationManager.updateGlobalVars() + }] + }, + STATES.userAgent.capabilities.touch && { + group: "touch-control", + label: t("touch-controller"), + items: [{ + label: t("layout"), + content: CE("select", { + disabled: !0 + }, CE("option", {}, t("default"))), + onCreated: (setting, $elm) => { + $elm.addEventListener("input", (e) => { + TouchController.applyCustomLayout($elm.value, 1000); + }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => { + const 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; } - $settings.appendChild($tabContent); - } - $tabs.firstElementChild.dispatchEvent(new Event("click")); - } - focusTab(tabId) { - const $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); - $tab && $tab.dispatchEvent(new Event("click")); - } - focusIfNeeded() { - this.jumpToSettingGroup("next"); - } - focusActiveTab() { - const $currentTab = this.$tabs.querySelector(".bx-active"); - return $currentTab && $currentTab.focus(), !0; - } - focusVisibleSetting(type = "first") { - const controls = Array.from(this.$settings.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; - const $focusable = this.dialogManager.findFocusableElement($control); - if ($focusable) { - if (this.dialogManager.focus($focusable)) return !0; + const $fragment = document.createDocumentFragment(); + for (let key in customLayouts.layouts) { + const layout = customLayouts.layouts[key]; + let name; + if (layout.author) name = `${layout.name} (${layout.author})`; + else name = layout.name; + const $option = CE("option", { value: key }, name); + $fragment.appendChild($option); } + $elm.appendChild($fragment), $elm.value = customLayouts.default_layout; + }); } - return !1; + }] } - focusVisibleTab(type = "first") { - const 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; + ]; + TAB_VIRTUAL_CONTROLLER_ITEMS = [{ + group: "mkb", + label: t("virtual-controller"), + helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/", + content: MkbRemapper.INSTANCE.render() + }]; + TAB_NATIVE_MKB_ITEMS = [{ + requiredVariants: "full", + group: "native-mkb", + label: t("native-mkb"), + items: [{ + pref: "native_mkb_scroll_y_sensitivity", + onChange: (e, value) => { + NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100); + } + }, { + pref: "native_mkb_scroll_x_sensitivity", + onChange: (e, value) => { + NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100); + } + }] + }]; + TAB_SHORTCUTS_ITEMS = [{ + requiredVariants: "full", + group: "controller-shortcuts", + label: t("controller-shortcuts"), + content: ControllerShortcut.renderSettings() + }]; + TAB_STATS_ITEMS = [{ + group: "stats", + label: t("stream-stats"), + helpUrl: "https://better-xcloud.github.io/stream-stats/", + items: [ + { + pref: "stats_show_when_playing" + }, + { + pref: "stats_quick_glance", + onChange: (e) => { + const streamStats = StreamStats.getInstance(); + e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop(); + } + }, + { + pref: "stats_items", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_position", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_text_size", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_opacity", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_transparent", + onChange: StreamStats.refreshStyles + }, + { + pref: "stats_conditional_formatting", + onChange: StreamStats.refreshStyles + } + ] + }]; + SETTINGS_UI = [ + { + icon: BxIcon.HOME, + group: "global", + items: this.TAB_GLOBAL_ITEMS + }, + { + icon: BxIcon.DISPLAY, + group: "stream", + items: this.TAB_DISPLAY_ITEMS + }, + { + icon: BxIcon.CONTROLLER, + group: "controller", + items: this.TAB_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + getPref("mkb_enabled") && { + icon: BxIcon.VIRTUAL_CONTROLLER, + group: "mkb", + items: this.TAB_VIRTUAL_CONTROLLER_ITEMS, + requiredVariants: "full" + }, + AppInterface && getPref("native_mkb_enabled") === "on" && { + icon: BxIcon.NATIVE_MKB, + group: "native-mkb", + items: this.TAB_NATIVE_MKB_ITEMS, + requiredVariants: "full" + }, + { + icon: BxIcon.COMMAND, + group: "shortcuts", + items: this.TAB_SHORTCUTS_ITEMS, + requiredVariants: "full" + }, + { + icon: BxIcon.STREAM_STATS, + group: "stats", + items: this.TAB_STATS_ITEMS } - jumpToSettingGroup(direction) { - const $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)"); - if (!$tabContent) return !1; - let $header; - const $focusing = document.activeElement; - if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + ]; + constructor() { + super(); + this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(); + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + onMounted() { + if (!this.renderFullSettings) return; + if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED); + const $selectUserAgent = document.querySelector(`#bx_setting_${"user_agent_profile"}`); + if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1; + } + reloadPage() { + this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload(); + } + async getRecommendedSettings(deviceCode) { + try { + const json = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/devices/${deviceCode.toLowerCase()}.json`)).json(), recommended = {}; + if (json.schema_version !== 1) return null; + const 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; + } + addDefaultSuggestedSetting(prefKey, value) { + let key; + for (key in this.suggestedSettings) + if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value; + } + 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] = getPrefDefinition(prefKey).default; + } + } + isSupportedVariant(requiredVariants) { + if (typeof requiredVariants === "undefined") return !0; + return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT); + } + async renderSuggestions(e) { + const $btnSuggest = e.target.closest("div"); + $btnSuggest.toggleAttribute("bx-open"); + let $content = $btnSuggest.nextElementSibling; + if ($content) { + BxEvent.dispatch($content.querySelector("select"), "input"); + return; + } + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab || !settingTab.items) continue; + for (let settingTabContent of settingTab.items) { + if (!settingTabContent || !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.suggestedSettingLabels[prefKey] = settingTabContent.label; + } + } + } + let recommendedDevice = ""; + if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) { + if (BX_FLAGS.DeviceInfo.androidInfo) { + const deviceCode = BX_FLAGS.DeviceInfo.androidInfo.board; + recommendedDevice = await this.getRecommendedSettings(deviceCode); + } + } + const hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType; + if (deviceType === "android-handheld") this.addDefaultSuggestedSetting("stream_touch_controller", "off"), this.addDefaultSuggestedSetting("controller_device_vibration", "on"); + else if (deviceType === "android") this.addDefaultSuggestedSetting("controller_device_vibration", "auto"); + else if (deviceType === "android-tv") this.addDefaultSuggestedSetting("stream_touch_controller", "off"); + this.generateDefaultSuggestedSettings(); + const $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", {}, 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) => { + const profile = $select.value; + removeChildElements($suggestedSettings); + const fragment = document.createDocumentFragment(); + let 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)); + const settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const currentValue = getPref(prefKey, !1), suggestedValue = settings[prefKey], currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue; + let $child, $value; + if (isSameValue) $value = currentValueText; else { - const $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling"; - let $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]; + const suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue); + $value = currentValueText + " ➔ " + suggestedValueText; + } + let $checkbox; + const breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(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: `bx_suggest_${prefKey}` + }), CE("label", { + for: `bx_suggest_${prefKey}` + }, 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"); + const onClickApply = () => { + const profile = $select.value, settings = this.suggestedSettings[profile]; + let prefKey; + for (prefKey in settings) { + const suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${prefKey}`); + if (!$checkBox.checked || $checkBox.disabled) continue; + const $control = this.settingElements[prefKey]; + if (!$control) { + setPref(prefKey, suggestedValue); + continue; + } + 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: 64 | 32, + onClick: onClickApply + }); + $content = CE("div", { + class: "bx-suggest-box", + _nearby: { + orientation: "vertical" + } + }, BxSelectElement.wrap($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); + } + renderTab(settingTab) { + const $svg = createSvgIcon(settingTab.icon); + return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", (e) => { + for (let $child of Array.from(this.$settings.children)) + if ($child.getAttribute("data-tab-group") === settingTab.group) { + if ($child.classList.remove("bx-gone"), getPref("ui_controller_friendly")) this.dialogManager.calculateSelectBoxes($child); + } else $child.classList.add("bx-gone"); + for (let $child of Array.from(this.$tabs.children)) + $child.classList.remove("bx-active"); + $svg.classList.add("bx-active"); + }), $svg; + } + onGlobalSettingChanged(e) { + PatcherCache.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"); + } + renderServerSetting(setting) { + let selectedValue; + const $control = CE("select", { + id: `bx_setting_${setting.pref}`, + title: setting.label, + tabindex: 0 + }); + $control.name = $control.id, $control.addEventListener("input", (e) => { + setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e); + }), selectedValue = getPref("server_region"), setting.options = {}; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + let value = regionName, label = `${region.shortName} - ${regionName}`; + if (region.isDefault) { + if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default"; + } + setting.options[value] = label; + } + for (let value in setting.options) { + const label = setting.options[value], $option = CE("option", { value }, label); + $control.appendChild($option); + } + return $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control; + } + renderSettingRow(settingTab, $tabContent, settingTabContent, setting) { + if (typeof setting === "string") setting = { + pref: setting + }; + const pref = setting.pref; + let $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, STORAGE.Global, async (e) => { + const newLocale = e.target.value; + if (getPref("ui_controller_friendly")) { + 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 === "user_agent_profile") $control = SettingElement.fromPref("user_agent_profile", STORAGE.Global, (e) => { + const value = e.target.value; + let isCustom = value === "custom", userAgent2 = UserAgent.get(value); + UserAgent.updateStorage(value); + const $inp = $control.nextElementSibling; + $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e); + }); + else { + let onChange = setting.onChange; + if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged.bind(this); + $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params); + } + if ($control instanceof HTMLSelectElement && getPref("ui_controller_friendly")) $control = BxSelectElement.wrap($control); + pref && (this.settingElements[pref] = $control); + } + let prefDefinition = null; + if (pref) prefDefinition = getPrefDefinition(pref); + if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return; + let label = prefDefinition?.label || setting.label, note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote; + const experimental = prefDefinition?.experimental || setting.experimental; + 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 $label; + const $row = CE("label", { + class: "bx-settings-row", + for: `bx_setting_${pref}`, + "data-type": settingTabContent.group, + _nearby: { + orientation: "horizontal" + } + }, $label = CE("span", { class: "bx-settings-label" }, label, $note), !prefDefinition?.unsupported && $control), $link = $label.querySelector("a"); + if ($link) $link.classList.add("bx-focusable"), setNearby($label, { + focus: $link + }); + $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control); + } + setupDialog() { + let $tabs, $settings; + const $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", {}, this.$btnReload = createButton({ + icon: BxIcon.REFRESH, + style: 32 | 16, + onClick: (e) => { + this.reloadPage(); + } + }), createButton({ + icon: BxIcon.CLOSE, + style: 32 | 16, + onClick: (e) => { + this.dialogManager.hide(); + } + }))), $settings = CE("div", { + class: "bx-settings-tab-contents", + _nearby: { + orientation: "vertical", + focus: () => this.jumpToSettingGroup("next"), + loop: (direction) => { + if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0; + return !1; + } + } + })); + this.$container = $container, this.$tabs = $tabs, this.$settings = $settings, $container.addEventListener("click", (e) => { + if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide(); + }); + for (let settingTab of this.SETTINGS_UI) { + if (!settingTab) continue; + if (!this.isSupportedVariant(settingTab.requiredVariants)) continue; + if (settingTab.group !== "global" && !this.renderFullSettings) continue; + const $svg = this.renderTab(settingTab); + $tabs.appendChild($svg); + const $tabContent = CE("div", { + class: "bx-gone", + "data-tab-group": settingTab.group + }); + for (let settingTabContent of settingTab.items) { + if (settingTabContent === !1) continue; + if (!this.isSupportedVariant(settingTabContent.requiredVariants)) continue; + if (!this.renderFullSettings && settingTab.group === "global" && settingTabContent.group !== "general" && settingTabContent.group !== "footer") continue; + let label = settingTabContent.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: 1024 | 8 | 32 + }); + } + if (label) { + const $title = CE("h2", { + _nearby: { + orientation: "horizontal" } + }, CE("span", {}, label), settingTabContent.helpUrl && createButton({ + icon: BxIcon.QUESTION, + style: 4 | 32, + url: settingTabContent.helpUrl, + title: t("help") + })); + $tabContent.appendChild($title); } - let $target; - if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1); - if ($target) return this.dialogManager.focus($target); - return !1; - } - 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; - default: - handled = !1; - break; + if (settingTabContent.unsupportedNote) { + const $note = CE("b", { class: "bx-note-unsupported" }, settingTabContent.unsupportedNote); + $tabContent.appendChild($note); } - return handled; - } - handleGamepad(button) { - let handled = !0; - switch (button) { - case 4: - case 5: - this.focusActiveTab(); - break; - case 6: - this.jumpToSettingGroup("previous"); - break; - case 7: - this.jumpToSettingGroup("next"); - break; - default: - handled = !1; - break; + if (settingTabContent.unsupported) continue; + if (settingTabContent.content) { + $tabContent.appendChild(settingTabContent.content); + continue; } - return handled; + settingTabContent.items = settingTabContent.items || []; + for (let setting of settingTabContent.items) { + if (setting === !1) continue; + if (typeof setting === "function") { + setting.apply(this, [$tabContent]); + continue; + } + this.renderSettingRow(settingTab, $tabContent, settingTabContent, setting); + } + } + $settings.appendChild($tabContent); } + $tabs.firstElementChild.dispatchEvent(new Event("click")); + } + focusTab(tabId) { + const $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`); + $tab && $tab.dispatchEvent(new Event("click")); + } + focusIfNeeded() { + this.jumpToSettingGroup("next"); + } + focusActiveTab() { + const $currentTab = this.$tabs.querySelector(".bx-active"); + return $currentTab && $currentTab.focus(), !0; + } + focusVisibleSetting(type = "first") { + const controls = Array.from(this.$settings.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; + const $focusable = this.dialogManager.findFocusableElement($control); + if ($focusable) { + if (this.dialogManager.focus($focusable)) return !0; + } + } + return !1; + } + focusVisibleTab(type = "first") { + const 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) { + const $tabContent = this.$settings.querySelector("div[data-tab-group]:not(.bx-gone)"); + if (!$tabContent) return !1; + let $header; + const $focusing = document.activeElement; + if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2"); + else { + const $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling"; + let $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; + } + 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; + default: + handled = !1; + break; + } + return handled; + } + handleGamepad(button) { + let handled = !0; + switch (button) { + case 4: + case 5: + this.focusActiveTab(); + break; + case 6: + this.jumpToSettingGroup("previous"); + break; + case 7: + this.jumpToSettingGroup("next"); + break; + default: + handled = !1; + break; + } + return handled; + } } class ControllerShortcut { - static STORAGE_KEY = "better_xcloud_controller_shortcuts"; - static buttonsCache = {}; - static buttonsStatus = {}; - static $selectProfile; - static $selectActions = {}; - static $container; - static ACTIONS = null; - static reset(index) { - ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; + static STORAGE_KEY = "better_xcloud_controller_shortcuts"; + static buttonsCache = {}; + static buttonsStatus = {}; + static $selectProfile; + static $selectActions = {}; + static $container; + static ACTIONS = null; + static reset(index) { + ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = []; + } + static handle(gamepad) { + if (!ControllerShortcut.ACTIONS) ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); + const gamepadIndex = gamepad.index, actions = ControllerShortcut.ACTIONS[gamepad.id]; + if (!actions) return !1; + ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; + const pressed = []; + let otherButtonPressed = !1; + return gamepad.buttons.forEach((button, index) => { + if (button.pressed && index !== 16) { + if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) setTimeout(() => ControllerShortcut.runAction(actions[index]), 0); + } + }), ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; + } + static runAction(action) { + switch (action) { + case "bx-settings-show": + SettingsNavigationDialog.getInstance().show(); + break; + case "stream-screenshot-capture": + Screenshot.takeScreenshot(); + 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; } - static handle(gamepad) { - if (!ControllerShortcut.ACTIONS) ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); - const gamepadIndex = gamepad.index, actions = ControllerShortcut.ACTIONS[gamepad.id]; - if (!actions) return !1; - ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = []; - const pressed = []; - let otherButtonPressed = !1; - return gamepad.buttons.forEach((button, index) => { - if (button.pressed && index !== 16) { - if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) setTimeout(() => ControllerShortcut.runAction(actions[index]), 0); - } - }), ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed; - } - static runAction(action) { - switch (action) { - case "bx-settings-show": - SettingsNavigationDialog.getInstance().show(); - break; - case "stream-screenshot-capture": - Screenshot.takeScreenshot(); - 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; + } + static updateAction(profile, button, action) { + const actions = ControllerShortcut.ACTIONS; + if (!(profile in actions)) actions[profile] = []; + if (!action) action = null; + actions[profile][button] = action; + for (let key in ControllerShortcut.ACTIONS) { + let empty = !0; + for (let value of ControllerShortcut.ACTIONS[key]) + if (value) { + empty = !1; + break; } + if (empty) delete ControllerShortcut.ACTIONS[key]; } - static updateAction(profile, button, action) { - const actions = ControllerShortcut.ACTIONS; - if (!(profile in actions)) actions[profile] = []; - if (!action) action = null; - actions[profile][button] = action; - for (let key in ControllerShortcut.ACTIONS) { - let empty = !0; - for (let value of ControllerShortcut.ACTIONS[key]) - if (value) { - empty = !1; - break; - } - if (empty) delete ControllerShortcut.ACTIONS[key]; + window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS)), console.log(ControllerShortcut.ACTIONS); + } + static updateProfileList(e) { + const { $selectProfile: $select, $container } = ControllerShortcut, $fragment = document.createDocumentFragment(); + removeChildElements($select); + const gamepads = navigator.getGamepads(); + let hasGamepad = !1; + for (let gamepad of gamepads) { + if (!gamepad || !gamepad.connected) continue; + if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; + hasGamepad = !0; + const $option = CE("option", { value: gamepad.id }, gamepad.id); + $fragment.appendChild($option); + } + if ($container.dataset.hasGamepad = hasGamepad.toString(), hasGamepad) $select.appendChild($fragment), $select.selectedIndex = 0, $select.dispatchEvent(new Event("input")); + } + static switchProfile(profile) { + let actions = ControllerShortcut.ACTIONS[profile]; + if (!actions) actions = []; + let button; + for (button in ControllerShortcut.$selectActions) { + const $select = ControllerShortcut.$selectActions[button]; + $select.value = actions[button] || "", BxEvent.dispatch($select, "input", { + ignoreOnChange: !0, + manualTrigger: !0 + }); + } + } + static getActionsFromStorage() { + return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || "{}"); + } + static renderSettings() { + const PREF_CONTROLLER_FRIENDLY_UI = getPref("ui_controller_friendly"); + ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); + const buttons = new Map; + buttons.set(3, "⇑"), buttons.set(0, "⇓"), buttons.set(1, "⇒"), buttons.set(2, "⇐"), buttons.set(12, "≻"), buttons.set(13, "≽"), buttons.set(14, "≺"), buttons.set(15, "≼"), buttons.set(8, "⇺"), buttons.set(9, "⇻"), buttons.set(4, "↘"), buttons.set(5, "↙"), buttons.set(6, "↖"), buttons.set(7, "↗"), buttons.set(10, "↺"), buttons.set(11, "↻"); + const actions = { + [t("better-xcloud")]: { + "bx-settings-show": [t("settings"), t("show")] + }, + [t("device")]: AppInterface && { + "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-sound-toggle": [t("sound"), t("toggle")], + "stream-volume-inc": getPref("audio_enable_volume_control") && [t("volume"), t("increase")], + "stream-volume-dec": getPref("audio_enable_volume_control") && [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")] + } + }, $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); + for (let groupLabel in actions) { + const items = actions[groupLabel]; + if (!items) continue; + const $optGroup = CE("optgroup", { label: groupLabel }); + for (let action in items) { + let label = items[action]; + if (!label) continue; + if (Array.isArray(label)) label = label.join(" ❯ "); + const $option = CE("option", { value: action }, label); + $optGroup.appendChild($option); + } + $baseSelect.appendChild($optGroup); + } + let $remap; + const $selectProfile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" }), $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile; + $profile.classList.add("bx-full-width"); + const $container = CE("div", { + "data-has-gamepad": "false", + _nearby: { + focus: $profile + } + }, CE("div", {}, CE("p", { class: "bx-shortcut-note" }, t("controller-shortcuts-connect-note"))), $remap = CE("div", {}, CE("div", { + _nearby: { + focus: $profile + } + }, $profile), CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")))); + $selectProfile.addEventListener("input", (e) => { + ControllerShortcut.switchProfile($selectProfile.value); + }); + const onActionChanged = (e) => { + const $target = e.target, profile = $selectProfile.value, button = $target.dataset.button, action = $target.value; + if (!PREF_CONTROLLER_FRIENDLY_UI) { + const $fakeSelect = $target.previousElementSibling; + let fakeText = "---"; + if (action) { + const $selectedOption = $target.options[$target.selectedIndex]; + fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text; } - window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS)), console.log(ControllerShortcut.ACTIONS); - } - static updateProfileList(e) { - const { $selectProfile: $select, $container } = ControllerShortcut, $fragment = document.createDocumentFragment(); - removeChildElements($select); - const gamepads = navigator.getGamepads(); - let hasGamepad = !1; - for (let gamepad of gamepads) { - if (!gamepad || !gamepad.connected) continue; - if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue; - hasGamepad = !0; - const $option = CE("option", { value: gamepad.id }, gamepad.id); - $fragment.appendChild($option); - } - if ($container.dataset.hasGamepad = hasGamepad.toString(), hasGamepad) $select.appendChild($fragment), $select.selectedIndex = 0, $select.dispatchEvent(new Event("input")); - } - static switchProfile(profile) { - let actions = ControllerShortcut.ACTIONS[profile]; - if (!actions) actions = []; - let button; - for (button in ControllerShortcut.$selectActions) { - const $select = ControllerShortcut.$selectActions[button]; - $select.value = actions[button] || "", BxEvent.dispatch($select, "input", { - ignoreOnChange: !0, - manualTrigger: !0 - }); - } - } - static getActionsFromStorage() { - return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || "{}"); - } - static renderSettings() { - const PREF_CONTROLLER_FRIENDLY_UI = getPref("ui_controller_friendly"); - ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage(); - const buttons = new Map; - buttons.set(3, "⇑"), buttons.set(0, "⇓"), buttons.set(1, "⇒"), buttons.set(2, "⇐"), buttons.set(12, "≻"), buttons.set(13, "≽"), buttons.set(14, "≺"), buttons.set(15, "≼"), buttons.set(8, "⇺"), buttons.set(9, "⇻"), buttons.set(4, "↘"), buttons.set(5, "↙"), buttons.set(6, "↖"), buttons.set(7, "↗"), buttons.set(10, "↺"), buttons.set(11, "↻"); - const actions = { - [t("better-xcloud")]: { - "bx-settings-show": [t("settings"), t("show")] - }, - [t("device")]: AppInterface && { - "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-sound-toggle": [t("sound"), t("toggle")], - "stream-volume-inc": getPref("audio_enable_volume_control") && [t("volume"), t("increase")], - "stream-volume-dec": getPref("audio_enable_volume_control") && [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")] - } - }, $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---")); - for (let groupLabel in actions) { - const items = actions[groupLabel]; - if (!items) continue; - const $optGroup = CE("optgroup", { label: groupLabel }); - for (let action in items) { - let label = items[action]; - if (!label) continue; - if (Array.isArray(label)) label = label.join(" ❯ "); - const $option = CE("option", { value: action }, label); - $optGroup.appendChild($option); - } - $baseSelect.appendChild($optGroup); - } - let $remap; - const $selectProfile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" }), $profile = PREF_CONTROLLER_FRIENDLY_UI ? BxSelectElement.wrap($selectProfile) : $selectProfile; - $profile.classList.add("bx-full-width"); - const $container = CE("div", { - "data-has-gamepad": "false", - _nearby: { - focus: $profile - } - }, CE("div", {}, CE("p", { class: "bx-shortcut-note" }, t("controller-shortcuts-connect-note"))), $remap = CE("div", {}, CE("div", { - _nearby: { - focus: $profile - } - }, $profile), CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")))); - $selectProfile.addEventListener("input", (e) => { - ControllerShortcut.switchProfile($selectProfile.value); + $fakeSelect.firstElementChild.text = fakeText; + } + !e.ignoreOnChange && ControllerShortcut.updateAction(profile, button, action); + }; + for (let [button, prompt2] of buttons) { + const $row = CE("div", { + class: "bx-shortcut-row" + }), $label = CE("label", { class: "bx-prompt" }, `${""} + ${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }); + if (!PREF_CONTROLLER_FRIENDLY_UI) { + const $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---")); + $div.appendChild($fakeSelect); + } + const $select = $baseSelect.cloneNode(!0); + if ($select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), ControllerShortcut.$selectActions[button] = $select, PREF_CONTROLLER_FRIENDLY_UI) { + const $bxSelect = BxSelectElement.wrap($select); + $bxSelect.classList.add("bx-full-width"), $div.appendChild($bxSelect), setNearby($row, { + focus: $bxSelect }); - const onActionChanged = (e) => { - const $target = e.target, profile = $selectProfile.value, button = $target.dataset.button, action = $target.value; - if (!PREF_CONTROLLER_FRIENDLY_UI) { - const $fakeSelect = $target.previousElementSibling; - let fakeText = "---"; - if (action) { - const $selectedOption = $target.options[$target.selectedIndex]; - fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text; - } - $fakeSelect.firstElementChild.text = fakeText; - } - !e.ignoreOnChange && ControllerShortcut.updateAction(profile, button, action); - }; - for (let [button, prompt2] of buttons) { - const $row = CE("div", { - class: "bx-shortcut-row" - }), $label = CE("label", { class: "bx-prompt" }, `${""} + ${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }); - if (!PREF_CONTROLLER_FRIENDLY_UI) { - const $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---")); - $div.appendChild($fakeSelect); - } - const $select = $baseSelect.cloneNode(!0); - if ($select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), ControllerShortcut.$selectActions[button] = $select, PREF_CONTROLLER_FRIENDLY_UI) { - const $bxSelect = BxSelectElement.wrap($select); - $bxSelect.classList.add("bx-full-width"), $div.appendChild($bxSelect), setNearby($row, { - focus: $bxSelect - }); - } else $div.appendChild($select), setNearby($row, { - focus: $select - }); - $row.appendChild($label), $row.appendChild($div), $remap.appendChild($row); - } - return $container.appendChild($remap), ControllerShortcut.$selectProfile = $selectProfile, ControllerShortcut.$container = $container, window.addEventListener("gamepadconnected", ControllerShortcut.updateProfileList), window.addEventListener("gamepaddisconnected", ControllerShortcut.updateProfileList), ControllerShortcut.updateProfileList(), $container; + } else $div.appendChild($select), setNearby($row, { + focus: $select + }); + $row.appendChild($label), $row.appendChild($div), $remap.appendChild($row); } + return $container.appendChild($remap), ControllerShortcut.$selectProfile = $selectProfile, ControllerShortcut.$container = $container, window.addEventListener("gamepadconnected", ControllerShortcut.updateProfileList), window.addEventListener("gamepaddisconnected", ControllerShortcut.updateProfileList), ControllerShortcut.updateProfileList(), $container; + } } var BxExposed = { - getTitleInfo: () => STATES.currentStream.titleInfo, - modifyTitleInfo: function(titleInfo) { - titleInfo = deepClone(titleInfo); - let supportedInputTypes = titleInfo.details.supportedInputTypes; - if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); - if (getPref("native_mkb_enabled") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); - if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { - let touchControllerAvailability = getPref("stream_touch_controller"); - if (touchControllerAvailability !== "off" && getPref("stream_touch_controller_auto_off")) { - const gamepads = window.navigator.getGamepads(); - let 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, BxEvent.dispatch(window, BxEvent.TITLE_INFO_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 { - const 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: () => { - const navigationDialogManager = NavigationDialogManager.getInstance(); - if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; - const 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; + getTitleInfo: () => STATES.currentStream.titleInfo, + modifyTitleInfo: function(titleInfo) { + titleInfo = deepClone(titleInfo); + let supportedInputTypes = titleInfo.details.supportedInputTypes; + if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB"); + if (getPref("native_mkb_enabled") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB"); + if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) { + let touchControllerAvailability = getPref("stream_touch_controller"); + if (touchControllerAvailability !== "off" && getPref("stream_touch_controller_auto_off")) { + const gamepads = window.navigator.getGamepads(); + let 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, BxEvent.dispatch(window, BxEvent.TITLE_INFO_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 { + const 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: () => { + const navigationDialogManager = NavigationDialogManager.getInstance(); + if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0; + const 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; + } }; function localRedirect(path) { - const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); - if (!$pageContent) return; - const $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(); + const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent"); + if (!$pageContent) return; + const $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 = getPref("server_region"); - if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; - else return preferredRegion; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - if (!region.isDefault) continue; - if (shortName && region.shortName) return region.shortName; - else return regionName; - } - return null; + let preferredRegion = getPref("server_region"); + if (preferredRegion in STATES.serverRegions) if (shortName && STATES.serverRegions[preferredRegion].shortName) return STATES.serverRegions[preferredRegion].shortName; + else return preferredRegion; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (!region.isDefault) continue; + if (shortName && region.shortName) return region.shortName; + else return regionName; + } + return null; } class HeaderSection { - static #$remotePlayBtn = createButton({ - classes: ["bx-header-remote-play-button", "bx-gone"], - icon: BxIcon.REMOTE_PLAY, - title: t("remote-play"), - style: 4 | 32 | 512, - onClick: (e) => { - RemotePlayManager.getInstance().togglePopup(); - } - }); - static #$settingsBtn = createButton({ - classes: ["bx-header-settings-button"], - label: "???", - style: 8 | 16 | 32 | 128, - onClick: (e) => { - SettingsNavigationDialog.getInstance().show(); - } - }); - static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn); - static #observer; - static #timeout; - static #injectSettingsButton($parent) { - if (!$parent) return; - const PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn; - if (isElementVisible(HeaderSection.#$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(HeaderSection.#$buttonsWrapper); + static #$remotePlayBtn = createButton({ + classes: ["bx-header-remote-play-button", "bx-gone"], + icon: BxIcon.REMOTE_PLAY, + title: t("remote-play"), + style: 4 | 32 | 512, + onClick: (e) => { + RemotePlayManager.getInstance().togglePopup(); } - static checkHeader() { - let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); - if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); - $target && HeaderSection.#injectSettingsButton($target); - } - static showRemotePlayButton() { - HeaderSection.#$remotePlayBtn.classList.remove("bx-gone"); - } - static watchHeader() { - const $root = document.querySelector("#PageContent header") || document.querySelector("#root"); - if (!$root) return; - HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => { - HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000); - }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader(); + }); + static #$settingsBtn = createButton({ + classes: ["bx-header-settings-button"], + label: "???", + style: 8 | 16 | 32 | 128, + onClick: (e) => { + SettingsNavigationDialog.getInstance().show(); } + }); + static #$buttonsWrapper = CE("div", {}, getPref("xhome_enabled") ? HeaderSection.#$remotePlayBtn : null, HeaderSection.#$settingsBtn); + static #observer; + static #timeout; + static #injectSettingsButton($parent) { + if (!$parent) return; + const PREF_LATEST_VERSION = getPref("version_latest"), $btnSettings = HeaderSection.#$settingsBtn; + if (isElementVisible(HeaderSection.#$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(HeaderSection.#$buttonsWrapper); + } + static checkHeader() { + let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]"); + if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]"); + $target && HeaderSection.#injectSettingsButton($target); + } + static showRemotePlayButton() { + HeaderSection.#$remotePlayBtn.classList.remove("bx-gone"); + } + static watchHeader() { + const $root = document.querySelector("#PageContent header") || document.querySelector("#root"); + if (!$root) return; + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = null, HeaderSection.#observer && HeaderSection.#observer.disconnect(), HeaderSection.#observer = new MutationObserver((mutationList) => { + HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout), HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000); + }), HeaderSection.#observer.observe($root, { subtree: !0, childList: !0 }), HeaderSection.checkHeader(); + } } class RemotePlayNavigationDialog extends NavigationDialog { - static instance; - static getInstance() { - if (!RemotePlayNavigationDialog.instance) RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog; - return RemotePlayNavigationDialog.instance; - } - STATE_LABELS = { - On: t("powered-on"), - Off: t("powered-off"), - ConnectedStandby: t("standby"), - Unknown: t("unknown") - }; - $container; - constructor() { - super(); - this.setupDialog(); - } - setupDialog() { - const $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"); - let $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); - if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); - $resolutions.addEventListener("input", (e) => { - const value = e.target.value; - $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); - }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { - manualTrigger: !0 - }); - const $qualitySettings = CE("div", { - class: "bx-remote-play-settings" - }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutions)); - $fragment.appendChild($qualitySettings); - const manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); - for (let con of consoles) { - const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, 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 | 32, - 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: 4 | 32, - url: "https://better-xcloud.github.io/remote-play", - label: t("help") - }), createButton({ - style: 4 | 32, - label: t("close"), - onClick: (e) => this.hide() - }))), this.$container = $fragment; - } - getDialog() { - return this; - } - getContent() { - return this.$container; - } - focusIfNeeded() { - const $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); - $btnConnect && $btnConnect.focus(); + static instance; + static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog); + STATE_LABELS = { + On: t("powered-on"), + Off: t("powered-off"), + ConnectedStandby: t("standby"), + Unknown: t("unknown") + }; + $container; + constructor() { + super(); + this.setupDialog(); + } + setupDialog() { + const $fragment = CE("div", { class: "bx-remote-play-container" }), $settingNote = CE("p", {}), currentResolution = getPref("xhome_resolution"); + let $resolutions = CE("select", {}, CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "720p" }, "720p")); + if (getPref("ui_controller_friendly")) $resolutions = BxSelectElement.wrap($resolutions); + $resolutions.addEventListener("input", (e) => { + const value = e.target.value; + $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref("xhome_resolution", value); + }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", { + manualTrigger: !0 + }); + const $qualitySettings = CE("div", { + class: "bx-remote-play-settings" + }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutions)); + $fragment.appendChild($qualitySettings); + const manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(); + for (let con of consoles) { + const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, 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 | 32, + 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: 4 | 32, + url: "https://better-xcloud.github.io/remote-play", + label: t("help") + }), createButton({ + style: 4 | 32, + label: t("close"), + onClick: (e) => this.hide() + }))), this.$container = $fragment; + } + getDialog() { + return this; + } + getContent() { + return this.$container; + } + focusIfNeeded() { + const $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button"); + $btnConnect && $btnConnect.focus(); + } } var LOG_TAG5 = "RemotePlay"; class RemotePlayManager { - static instance; - static getInstance() { - if (!this.instance) this.instance = new RemotePlayManager; - return this.instance; + static instance; + static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager); + isInitialized = !1; + XCLOUD_TOKEN; + XHOME_TOKEN; + consoles; + regions = []; + initialize() { + if (this.isInitialized) return; + this.isInitialized = !0, this.getXhomeToken(() => { + this.getConsolesList(() => { + BxLogger.info(LOG_TAG5, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); + }); + }); + } + get xcloudToken() { + return this.XCLOUD_TOKEN; + } + set xcloudToken(token) { + this.XCLOUD_TOKEN = token; + } + get xhomeToken() { + return this.XHOME_TOKEN; + } + getConsoles() { + return this.consoles; + } + getXhomeToken(callback) { + if (this.XHOME_TOKEN) { + callback(); + return; } - isInitialized = !1; - XCLOUD_TOKEN; - XHOME_TOKEN; - consoles; - regions = []; - initialize() { - if (this.isInitialized) return; - this.isInitialized = !0, this.getXhomeToken(() => { - this.getConsolesList(() => { - BxLogger.info(LOG_TAG5, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY); - }); - }); - } - get xcloudToken() { - return this.XCLOUD_TOKEN; - } - set xcloudToken(token) { - this.XCLOUD_TOKEN = token; - } - get xhomeToken() { - return this.XHOME_TOKEN; - } - getConsoles() { - return this.consoles; - } - getXhomeToken(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++) { + const key = localStorage.key(i); + if (!key.startsWith("Auth.User.")) continue; + const 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; } - 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++) { - const key = localStorage.key(i); - if (!key.startsWith("Auth.User.")) continue; - const 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; - } - } - const 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(); - }); + break; + } } - async getConsolesList(callback) { - if (this.consoles) { - callback(); - return; - } - const options = { - method: "GET", - headers: { - Authorization: `Bearer ${this.XHOME_TOKEN}` - } - }; - for (let region of this.regions) - try { - const 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(); + const 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; } - play(serverId, resolution) { - if (resolution) setPref("xhome_resolution", resolution); - STATES.remotePlay.config = { - serverId - }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"); + const options = { + method: "GET", + headers: { + Authorization: `Bearer ${this.XHOME_TOKEN}` + } + }; + for (let region of this.regions) + try { + const 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) setPref("xhome_resolution", resolution); + 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; } - 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; - } - if (AppInterface && AppInterface.showRemotePlayDialog) { - AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); - return; - } - RemotePlayNavigationDialog.getInstance().show(); + if (this.consoles.length === 0) { + Toast.show(t("no-consoles-found"), "", { instant: !0 }); + return; } - static detect() { - if (!getPref("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; + if (AppInterface && AppInterface.showRemotePlayDialog) { + AppInterface.showRemotePlayDialog(JSON.stringify(this.consoles)), document.activeElement.blur(); + return; } + RemotePlayNavigationDialog.getInstance().show(); + } + static detect() { + if (!getPref("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 BASE_DEVICE_INFO = { - appInfo: { - env: { - clientAppId: window.location.host, - clientAppType: "browser", - clientAppVersion: "24.17.36", - clientSdkVersion: "10.1.14", - httpEnvironment: "prod", - sdkInstallId: "" - } + static #consoleAddrs = {}; + static BASE_DEVICE_INFO = { + appInfo: { + env: { + clientAppId: window.location.host, + clientAppType: "browser", + clientAppVersion: "24.17.36", + clientSdkVersion: "10.1.14", + httpEnvironment: "prod", + sdkInstallId: "" + } + }, + dev: { + displayInfo: { + dimensions: { + widthInPixels: 1920, + heightInPixels: 1080 }, - dev: { - displayInfo: { - dimensions: { - widthInPixels: 1920, - heightInPixels: 1080 - }, - pixelDensity: { - dpiX: 1, - dpiY: 1 - } - }, - hw: { - make: "Microsoft", - model: "unknown", - sdktype: "web" - }, - os: { - name: "windows", - ver: "22631.2715", - platform: "desktop" - }, - browser: { - browserName: "chrome", - browserVersion: "125.0" - } + pixelDensity: { + dpiX: 1, + dpiY: 1 } + }, + hw: { + make: "Microsoft", + model: "unknown", + sdktype: "web" + }, + os: { + name: "windows", + ver: "22631.2715", + platform: "desktop" + }, + browser: { + browserName: "chrome", + browserVersion: "125.0" + } + } + }; + static async#handleLogin(request) { + try { + const 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) { + const response = await NATIVE_FETCH(request), obj = await response.clone().json(); + console.log(obj); + const processPorts = (port) => { + const ports = new Set; + return port && ports.add(port), ports.add(9002), Array.from(ports); + }, serverDetails = obj.serverDetails; + if (serverDetails.ipAddress) XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); + if (serverDetails.ipV4Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); + if (serverDetails.ipV6Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); + return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; + } + static async#handleInputConfigs(request, opts) { + const response = await NATIVE_FETCH(request); + if (getPref("stream_touch_controller") !== "all") return response; + const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; + TouchController.setXboxTitleId(xboxTitleId); + const inputConfigs = obj[0]; + let hasTouchSupport = inputConfigs.supportedTabs.length > 0; + if (!hasTouchSupport) { + const 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) { + const clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`; + const 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) { + const body = await request.clone().json(), newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async handle(request) { + TouchController.disable(); + const clone = request.clone(), headers = {}; + for (let pair of clone.headers.entries()) + headers[pair[0]] = pair[1]; + headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`; + const deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO; + if (getPref("xhome_resolution") === "720p") deviceInfo.dev.os.name = "android"; + headers["x-ms-device-info"] = JSON.stringify(deviceInfo); + const opts = { + method: clone.method, + headers }; - static async#handleLogin(request) { - try { - const 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) { - const response = await NATIVE_FETCH(request), obj = await response.clone().json(); - console.log(obj); - const processPorts = (port) => { - const ports = new Set; - return port && ports.add(port), ports.add(9002), Array.from(ports); - }, serverDetails = obj.serverDetails; - if (serverDetails.ipAddress) XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); - if (serverDetails.ipV4Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); - if (serverDetails.ipV6Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); - return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; - } - static async#handleInputConfigs(request, opts) { - const response = await NATIVE_FETCH(request); - if (getPref("stream_touch_controller") !== "all") return response; - const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; - TouchController.setXboxTitleId(xboxTitleId); - const inputConfigs = obj[0]; - let hasTouchSupport = inputConfigs.supportedTabs.length > 0; - if (!hasTouchSupport) { - const 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) { - const clone = request.clone(), headers = {}; - for (let pair of clone.headers.entries()) - headers[pair[0]] = pair[1]; - headers.authorization = `Bearer ${RemotePlayManager.getInstance().xcloudToken}`; - const 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) { - const body = await request.clone().json(), newRequest = new Request(request, { - body: JSON.stringify(body) - }); - return NATIVE_FETCH(newRequest); - } - static async handle(request) { - TouchController.disable(); - const clone = request.clone(), headers = {}; - for (let pair of clone.headers.entries()) - headers[pair[0]] = pair[1]; - headers.authorization = `Bearer ${RemotePlayManager.getInstance().xhomeToken}`; - const deviceInfo = XhomeInterceptor.BASE_DEVICE_INFO; - if (getPref("xhome_resolution") === "720p") deviceInfo.dev.os.name = "android"; - headers["x-ms-device-info"] = JSON.stringify(deviceInfo); - const opts = { - method: clone.method, - headers - }; - if (clone.method === "POST") opts.body = await clone.text(); - let newUrl = request.url; - if (!newUrl.includes("/servers/home")) { - const index = request.url.indexOf(".xboxlive.com"); - newUrl = STATES.remotePlay.server + request.url.substring(index + 13); - } - request = new Request(newUrl, opts); - let url = typeof request === "string" ? request : request.url; - if (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); + if (clone.method === "POST") opts.body = await clone.text(); + let newUrl = request.url; + if (!newUrl.includes("/servers/home")) { + const index = request.url.indexOf(".xboxlive.com"); + newUrl = STATES.remotePlay.server + request.url.substring(index + 13); } + request = new Request(newUrl, opts); + let url = typeof request === "string" ? request : request.url; + if (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) { - const 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 $bgStyle; + static $waitTimeBox; + static waitTimeInterval = null; + static orgWebTitle; + static secondsToString(seconds) { + const 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() { + const titleInfo = STATES.currentStream.titleInfo; + if (!titleInfo) return; + if (!LoadingScreen.$bgStyle) { + const $bgStyle = CE("style"); + document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; } - static setup() { - const titleInfo = STATES.currentStream.titleInfo; - if (!titleInfo) return; - if (!LoadingScreen.$bgStyle) { - const $bgStyle = CE("style"); - document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle; - } - if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_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", $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;}`; - const bg = new Image; - bg.onload = (e) => { - $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; - }, bg.src = imageUrl; - } - static setupWaitTime(waitTime) { - if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); - let secondsLeft = waitTime, $countDown, $estimated; - LoadingScreen.orgWebTitle = document.title; - const 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", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, 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"), getPref("ui_loading_screen_game_art") && LoadingScreen.$bgStyle) { - const $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; + if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("ui_loading_screen_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", $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;}`; + const bg = new Image; + bg.onload = (e) => { + $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}'; + }, bg.src = imageUrl; + } + static setupWaitTime(waitTime) { + if (getPref("ui_loading_screen_rocket") === "hide-queue") LoadingScreen.hideRocket(); + let secondsLeft = waitTime, $countDown, $estimated; + LoadingScreen.orgWebTitle = document.title; + const 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", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, 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"), getPref("ui_loading_screen_game_art") && LoadingScreen.$bgStyle) { + const $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 TrueAchievements { - static $link = createButton({ - label: t("true-achievements"), - url: "#", - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32 | 4 | 64 | 2048, - onClick: TrueAchievements.onClick - }); - static $button = createButton({ - label: t("true-achievements"), - title: t("true-achievements"), - icon: BxIcon.TRUE_ACHIEVEMENTS, - style: 32, - onClick: TrueAchievements.onClick - }); - static onClick(e) { - e.preventDefault(); - const dataset = TrueAchievements.$link.dataset; - TrueAchievements.open(!0, dataset.xboxTitleId, dataset.id), window.BX_EXPOSED.dialogRoutes?.closeAll(); - } - static $hiddenLink = CE("a", { - target: "_blank" - }); - static updateIds(xboxTitleId, id2) { - const { $link, $button } = TrueAchievements; - if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; - if (id2) $link.dataset.id = id2, $button.dataset.id = id2; - } - static injectAchievementsProgress($elm) { - if (SCRIPT_VARIANT !== "full") return; - const $parent = $elm.parentElement, $div = CE("div", { - class: "bx-guide-home-achievements-progress" - }, $elm); - let xboxTitleId; - try { - const $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); - if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; - } catch (e) {} - if (!xboxTitleId) xboxTitleId = TrueAchievements.getStreamXboxTitleId(); - if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); - if (TrueAchievements.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(TrueAchievements.$link); - else $div.appendChild(TrueAchievements.$button); - $parent.appendChild($div); - } - static injectAchievementDetailPage($parent) { - if (SCRIPT_VARIANT !== "full") return; - const props = getReactProps($parent); - if (!props) return; - try { - const achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName; - let id2, xboxTitleId; - for (let achiev of achievementList) - if (achiev.name === achievementName) { - id2 = achiev.id, xboxTitleId = achiev.title.id; - break; - } - if (id2) TrueAchievements.updateIds(xboxTitleId, id2), $parent.appendChild(TrueAchievements.$link); - } catch (e) {} - } - static getStreamXboxTitleId() { - return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; - } - static open(override, xboxTitleId, id2) { - if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = TrueAchievements.getStreamXboxTitleId(); - if (AppInterface && AppInterface.openTrueAchievementsLink) { - AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id2?.toString()); - return; + static $link = createButton({ + label: t("true-achievements"), + url: "#", + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 32 | 4 | 64 | 2048, + onClick: TrueAchievements.onClick + }); + static $button = createButton({ + label: t("true-achievements"), + title: t("true-achievements"), + icon: BxIcon.TRUE_ACHIEVEMENTS, + style: 32, + onClick: TrueAchievements.onClick + }); + static onClick(e) { + e.preventDefault(); + const dataset = TrueAchievements.$link.dataset; + TrueAchievements.open(!0, dataset.xboxTitleId, dataset.id), window.BX_EXPOSED.dialogRoutes?.closeAll(); + } + static $hiddenLink = CE("a", { + target: "_blank" + }); + static updateIds(xboxTitleId, id2) { + const { $link, $button } = TrueAchievements; + if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId; + if (id2) $link.dataset.id = id2, $button.dataset.id = id2; + } + static injectAchievementsProgress($elm) { + if (SCRIPT_VARIANT !== "full") return; + const $parent = $elm.parentElement, $div = CE("div", { + class: "bx-guide-home-achievements-progress" + }, $elm); + let xboxTitleId; + try { + const $container = $parent.closest("div[class*=AchievementsPreview-module__container]"); + if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId; + } catch (e) {} + if (!xboxTitleId) xboxTitleId = TrueAchievements.getStreamXboxTitleId(); + if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString(); + if (TrueAchievements.updateIds(xboxTitleId), document.documentElement.dataset.xdsPlatform === "tv") $div.appendChild(TrueAchievements.$link); + else $div.appendChild(TrueAchievements.$button); + $parent.appendChild($div); + } + static injectAchievementDetailPage($parent) { + if (SCRIPT_VARIANT !== "full") return; + const props = getReactProps($parent); + if (!props) return; + try { + const achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName; + let id2, xboxTitleId; + for (let achiev of achievementList) + if (achiev.name === achievementName) { + id2 = achiev.id, xboxTitleId = achiev.title.id; + break; } - let url = "https://www.trueachievements.com"; - if (xboxTitleId) { - if (url += `/deeplink/${xboxTitleId}`, id2) url += `/${id2}`; - } - TrueAchievements.$hiddenLink.href = url, TrueAchievements.$hiddenLink.click(); + if (id2) TrueAchievements.updateIds(xboxTitleId, id2), $parent.appendChild(TrueAchievements.$link); + } catch (e) {} + } + static getStreamXboxTitleId() { + return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId; + } + static open(override, xboxTitleId, id2) { + if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = TrueAchievements.getStreamXboxTitleId(); + if (AppInterface && AppInterface.openTrueAchievementsLink) { + AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id2?.toString()); + return; } + let url = "https://www.trueachievements.com"; + if (xboxTitleId) { + if (url += `/deeplink/${xboxTitleId}`, id2) url += `/${id2}`; + } + TrueAchievements.$hiddenLink.href = url, TrueAchievements.$hiddenLink.click(); + } } class GuideMenu { - static #BUTTONS = { - scriptSettings: createButton({ - label: t("better-xcloud"), - style: 64 | 32 | 1, - onClick: (e) => { - window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { - setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); - }, { once: !0 }), GuideMenu.#closeGuideMenu(); - } - }), - closeApp: AppInterface && createButton({ - icon: BxIcon.POWER, - label: t("close-app"), - title: t("close-app"), - style: 64 | 32 | 2, - onClick: (e) => { - AppInterface.closeApp(); - }, - attributes: { - "data-state": "normal" - } - }), - reloadPage: createButton({ - icon: BxIcon.REFRESH, - label: t("reload-page"), - title: t("reload-page"), - style: 64 | 32, - onClick: (e) => { - if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); - else window.location.reload(); - GuideMenu.#closeGuideMenu(); - } - }), - backToHome: createButton({ - icon: BxIcon.HOME, - label: t("back-to-home"), - title: t("back-to-home"), - style: 64 | 32, - onClick: (e) => { - confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); - }, - attributes: { - "data-state": "playing" - } - }) - }; - static #$renderedButtons; - static #closeGuideMenu() { - if (window.BX_EXPOSED.dialogRoutes) { - window.BX_EXPOSED.dialogRoutes.closeAll(); - return; - } - const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); - $btnClose && $btnClose.click(); + static #BUTTONS = { + scriptSettings: createButton({ + label: t("better-xcloud"), + style: 64 | 32 | 1, + onClick: (e) => { + window.addEventListener(BxEvent.XCLOUD_DIALOG_DISMISSED, (e2) => { + setTimeout(() => SettingsNavigationDialog.getInstance().show(), 50); + }, { once: !0 }), GuideMenu.#closeGuideMenu(); + } + }), + closeApp: AppInterface && createButton({ + icon: BxIcon.POWER, + label: t("close-app"), + title: t("close-app"), + style: 64 | 32 | 2, + onClick: (e) => { + AppInterface.closeApp(); + }, + attributes: { + "data-state": "normal" + } + }), + reloadPage: createButton({ + icon: BxIcon.REFRESH, + label: t("reload-page"), + title: t("reload-page"), + style: 64 | 32, + onClick: (e) => { + if (STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload(); + else window.location.reload(); + GuideMenu.#closeGuideMenu(); + } + }), + backToHome: createButton({ + icon: BxIcon.HOME, + label: t("back-to-home"), + title: t("back-to-home"), + style: 64 | 32, + onClick: (e) => { + confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31)), GuideMenu.#closeGuideMenu(); + }, + attributes: { + "data-state": "playing" + } + }) + }; + static #$renderedButtons; + static #closeGuideMenu() { + if (window.BX_EXPOSED.dialogRoutes) { + window.BX_EXPOSED.dialogRoutes.closeAll(); + return; } - static #renderButtons() { - if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; - const $div = CE("div", { - class: "bx-guide-home-buttons" - }), buttons = [ - GuideMenu.#BUTTONS.scriptSettings, - [ - GuideMenu.#BUTTONS.backToHome, - GuideMenu.#BUTTONS.reloadPage, - GuideMenu.#BUTTONS.closeApp - ] - ]; - for (let $button of buttons) { - if (!$button) continue; - if ($button instanceof HTMLElement) $div.appendChild($button); - else if (Array.isArray($button)) { - const $wrapper = CE("div", {}); - for (let $child of $button) - $child && $wrapper.appendChild($child); - $div.appendChild($wrapper); - } - } - return GuideMenu.#$renderedButtons = $div, $div; + const $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]"); + $btnClose && $btnClose.click(); + } + static #renderButtons() { + if (GuideMenu.#$renderedButtons) return GuideMenu.#$renderedButtons; + const $div = CE("div", { + class: "bx-guide-home-buttons" + }), buttons = [ + GuideMenu.#BUTTONS.scriptSettings, + [ + GuideMenu.#BUTTONS.backToHome, + GuideMenu.#BUTTONS.reloadPage, + GuideMenu.#BUTTONS.closeApp + ] + ]; + for (let $button of buttons) { + if (!$button) continue; + if ($button instanceof HTMLElement) $div.appendChild($button); + else if (Array.isArray($button)) { + const $wrapper = CE("div", {}); + for (let $child of $button) + $child && $wrapper.appendChild($child); + $div.appendChild($wrapper); + } } - static #injectHome($root, isPlaying = !1) { - { - const $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); - if ($achievementsProgress) TrueAchievements.injectAchievementsProgress($achievementsProgress); - } - let $target = null; - if (isPlaying) { - $target = $root.querySelector("a[class*=QuitGameButton]"); - const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); - $btnXcloudHome && ($btnXcloudHome.style.display = "none"); - } else { - const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); - if ($dividers) $target = $dividers[$dividers.length - 1]; - } - if (!$target) return !1; - const $buttons = GuideMenu.#renderButtons(); - $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + return GuideMenu.#$renderedButtons = $div, $div; + } + static #injectHome($root, isPlaying = !1) { + { + const $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]"); + if ($achievementsProgress) TrueAchievements.injectAchievementsProgress($achievementsProgress); } - static async#onShown(e) { - if (e.where === "home") { - const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); - $root && GuideMenu.#injectHome($root, STATES.isPlaying); - } + let $target = null; + if (isPlaying) { + $target = $root.querySelector("a[class*=QuitGameButton]"); + const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]"); + $btnXcloudHome && ($btnXcloudHome.style.display = "none"); + } else { + const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]"); + if ($dividers) $target = $dividers[$dividers.length - 1]; } - static addEventListeners() { - window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); + if (!$target) return !1; + const $buttons = GuideMenu.#renderButtons(); + $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons); + } + static async#onShown(e) { + if (e.where === "home") { + const $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]"); + $root && GuideMenu.#injectHome($root, STATES.isPlaying); } - static observe($addedElm) { - const className = $addedElm.className; - if (className.includes("AchievementsButton-module__progressBarContainer")) { - TrueAchievements.injectAchievementsProgress($addedElm); - return; - } - if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; - { - const $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); - if ($achievDetailPage) { - TrueAchievements.injectAchievementDetailPage($achievDetailPage); - return; - } - } - const $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" }); - } + } + static addEventListeners() { + window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown); + } + static observe($addedElm) { + const className = $addedElm.className; + if (className.includes("AchievementsButton-module__progressBarContainer")) { + TrueAchievements.injectAchievementsProgress($addedElm); + return; } + if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return; + { + const $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]"); + if ($achievDetailPage) { + TrueAchievements.injectAchievementDetailPage($achievDetailPage); + return; + } + } + const $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() { - if (!StreamBadges.instance) StreamBadges.instance = new StreamBadges; - return StreamBadges.instance; + static instance; + static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new 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" } - 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; + setRegion(region) { + this.serverInfo.server = { + region, + ipv6: !1 }; - $container; - intervalId; - REFRESH_INTERVAL = 3000; - setRegion(region) { - this.serverInfo.server = { - region, - ipv6: !1 - }; + } + renderBadge(name, value) { + const badgeInfo = this.badges[name]; + let $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; + } + async updateBadges(forceUpdate = !1) { + if (!this.$container || !forceUpdate && !this.$container.isConnected) { + this.stop(); + return; } - renderBadge(name, value) { - const badgeInfo = this.badges[name]; - let $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; + const statsCollector = StreamStatsCollector.getInstance(); + await statsCollector.collect(); + const 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() + }; + let name; + for (name in badges) { + const value = badges[name]; + if (value === null) continue; + const $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 updateBadges(forceUpdate = !1) { - if (!this.$container || !forceUpdate && !this.$container.isConnected) { - this.stop(); - return; - } - const statsCollector = StreamStatsCollector.getInstance(); - await statsCollector.collect(); - const 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() - }; - let name; - for (name in badges) { - const value = badges[name]; - if (value === null) continue; - const $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.bind(this), this.REFRESH_INTERVAL); + } + stop() { + this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + } + async render() { + if (this.$container) return this.start(), this.$container; + await this.getServerStats(); + let batteryLevel = ""; + if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; + const BADGES = [ + ["playtime", "1m"], + ["battery", batteryLevel], + ["download", humanFileSize(0)], + ["upload", humanFileSize(0)], + this.serverInfo.server ? 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" }); + return BADGES.forEach((item2) => { + if (!item2) return; + let $badge; + if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2); + else $badge = item2; + $container.appendChild($badge); + }), this.$container = $container, await this.start(), $container; + } + async getServerStats() { + const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; + let videoCodecId, videoWidth = 0, videoHeight = 0; + const allAudioCodecs = {}; + let audioCodecId; + const allCandidates = {}; + let candidateId; + if (stats.forEach((stat) => { + if (stat.type === "codec") { + const 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 === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") candidateId = stat.remoteCandidateId; + else if (stat.type === "remote-candidate") allCandidates[stat.id] = stat.address; + }), videoCodecId) { + const videoStat = allVideoCodecs[videoCodecId], video = { + width: videoWidth, + height: videoHeight, + codec: videoStat.mimeType.substring(6) + }; + if (video.codec === "H264") { + const 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) { + const profile = video.profile; + let 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; } - async start() { - await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges.bind(this), this.REFRESH_INTERVAL); + if (audioCodecId) { + const 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; } - stop() { - this.intervalId && clearInterval(this.intervalId), this.intervalId = null; + if (candidateId) { + BxLogger.info("candidate", candidateId, allCandidates); + const server = this.serverInfo.server; + if (server) { + server.ipv6 = allCandidates[candidateId].includes(":"); + let text = ""; + if (server.region) text += server.region; + text += "@" + (server.ipv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); + } } - async render() { - if (this.$container) return this.start(), this.$container; - await this.getServerStats(); - let batteryLevel = ""; - if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%"; - const BADGES = [ - ["playtime", "1m"], - ["battery", batteryLevel], - ["download", humanFileSize(0)], - ["upload", humanFileSize(0)], - this.serverInfo.server ? 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" }); - return BADGES.forEach((item2) => { - if (!item2) return; - let $badge; - if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2); - else $badge = item2; - $container.appendChild($badge); - }), this.$container = $container, await this.start(), $container; - } - async getServerStats() { - const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}; - let videoCodecId, videoWidth = 0, videoHeight = 0; - const allAudioCodecs = {}; - let audioCodecId; - const allCandidates = {}; - let candidateId; - if (stats.forEach((stat) => { - if (stat.type === "codec") { - const 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 === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") candidateId = stat.remoteCandidateId; - else if (stat.type === "remote-candidate") allCandidates[stat.id] = stat.address; - }), videoCodecId) { - const videoStat = allVideoCodecs[videoCodecId], video = { - width: videoWidth, - height: videoHeight, - codec: videoStat.mimeType.substring(6) - }; - if (video.codec === "H264") { - const 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) { - const profile = video.profile; - let 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) { - const 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 (candidateId) { - BxLogger.info("candidate", candidateId, allCandidates); - const server = this.serverInfo.server; - if (server) { - server.ipv6 = allCandidates[candidateId].includes(":"); - let text = ""; - if (server.region) text += server.region; - text += "@" + (server.ipv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text); - } - } - } - static setupEvents() {} + } + static setupEvents() {} } class XcloudInterceptor { - static async#handleLogin(request, init) { - const bypassServer = getPref("server_bypass_restriction"); - if (bypassServer !== "off") { - const ip = BypassServerIps[bypassServer]; - ip && request.headers.set("X-Forwarded-For", ip); - } - const response = await NATIVE_FETCH(request, init); - if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; - const obj = await response.clone().json(); - RemotePlayManager.getInstance().xcloudToken = obj.gsToken; - const serverEmojis = { - AustraliaEast: "🇦🇺", - AustraliaSouthEast: "🇦🇺", - BrazilSouth: "🇧🇷", - EastUS: "🇺🇸", - EastUS2: "🇺🇸", - JapanEast: "🇯🇵", - KoreaCentral: "🇰🇷", - MexicoCentral: "🇲🇽", - NorthCentralUs: "🇺🇸", - SouthCentralUS: "🇺🇸", - UKSouth: "🇬🇧", - WestEurope: "🇪🇺", - WestUS: "🇺🇸", - WestUS2: "🇺🇸" - }, serverRegex = /\/\/(\w+)\./; - for (let region of obj.offeringSettings.regions) { - const regionName = region.name; - let shortName = region.name; - if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); - let match = serverRegex.exec(region.baseUri); - if (match) { - if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName; - } - region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); - } - BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY); - const preferredRegion = getPreferredServerRegion(); - if (preferredRegion && preferredRegion in STATES.serverRegions) { - const 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#handleLogin(request, init) { + const bypassServer = getPref("server_bypass_restriction"); + if (bypassServer !== "off") { + const ip = BypassServerIps[bypassServer]; + ip && request.headers.set("X-Forwarded-For", ip); } - static async#handlePlay(request, init) { - const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); - let badgeRegion = parsedUrl.host.split(".", 1)[0]; - for (let regionName in STATES.serverRegions) { - const region = STATES.serverRegions[regionName]; - if (parsedUrl.origin == region.baseUri) { - badgeRegion = regionName; - break; - } - } - StreamBadges.getInstance().setRegion(badgeRegion); - const body = await request.clone().json(); - if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { - const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; - body.settings.osName = osName; - } - if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; - const newRequest = new Request(request, { - body: JSON.stringify(body) - }); - return NATIVE_FETCH(newRequest); + const response = await NATIVE_FETCH(request, init); + if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response; + const obj = await response.clone().json(); + RemotePlayManager.getInstance().xcloudToken = obj.gsToken; + const serverEmojis = { + AustraliaEast: "🇦🇺", + AustraliaSouthEast: "🇦🇺", + BrazilSouth: "🇧🇷", + EastUS: "🇺🇸", + EastUS2: "🇺🇸", + JapanEast: "🇯🇵", + KoreaCentral: "🇰🇷", + MexicoCentral: "🇲🇽", + NorthCentralUs: "🇺🇸", + SouthCentralUS: "🇺🇸", + UKSouth: "🇬🇧", + WestEurope: "🇪🇺", + WestUS: "🇺🇸", + WestUS2: "🇺🇸" + }, serverRegex = /\/\/(\w+)\./; + for (let region of obj.offeringSettings.regions) { + const regionName = region.name; + let shortName = region.name; + if (region.isDefault) STATES.selectedRegion = Object.assign({}, region); + let match = serverRegex.exec(region.baseUri); + if (match) { + if (shortName = match[1], serverEmojis[regionName]) shortName = serverEmojis[regionName] + " " + shortName; + } + region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region); } - static async#handleWaitTime(request, init) { - const response = await NATIVE_FETCH(request, init); - if (getPref("ui_loading_screen_wait_time")) { - const json = await response.clone().json(); - if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds); - } - return response; + BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY); + const preferredRegion = getPreferredServerRegion(); + if (preferredRegion && preferredRegion in STATES.serverRegions) { + const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]); + tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp; } - static async#handleConfiguration(request, init) { - if (request.method !== "GET") return NATIVE_FETCH(request, init); - if (getPref("stream_touch_controller") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); - else TouchController.enable(); - const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); - if (!text.length) return response; - const obj = JSON.parse(text); - let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; - overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; - let overrideMkb = null; - if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; - if (getPref("native_mkb_enabled") === "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 (getPref("audio_mic_on_playing")) 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; + return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; + } + static async#handlePlay(request, init) { + const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); + let badgeRegion = parsedUrl.host.split(".", 1)[0]; + for (let regionName in STATES.serverRegions) { + const region = STATES.serverRegions[regionName]; + if (parsedUrl.origin == region.baseUri) { + badgeRegion = regionName; + break; + } } - 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); + StreamBadges.getInstance().setRegion(badgeRegion); + const body = await request.clone().json(); + if (PREF_STREAM_TARGET_RESOLUTION !== "auto") { + const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows"; + body.settings.osName = osName; } + if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE; + const newRequest = new Request(request, { + body: JSON.stringify(body) + }); + return NATIVE_FETCH(newRequest); + } + static async#handleWaitTime(request, init) { + const response = await NATIVE_FETCH(request, init); + if (getPref("ui_loading_screen_wait_time")) { + const 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 (getPref("stream_touch_controller") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); + else TouchController.enable(); + const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); + if (!text.length) return response; + const obj = JSON.parse(text); + let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {}; + overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0; + let overrideMkb = null; + if (getPref("native_mkb_enabled") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0; + if (getPref("native_mkb_enabled") === "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 (getPref("audio_mic_on_playing")) 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"); + window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer"); } function clearDbLogs(dbName, table) { - const request = window.indexedDB.open(dbName); - request.onsuccess = (e) => { - const db = e.target.result; - try { - const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); - objectStoreRequest.onsuccess = function() { - console.log(`[Better xCloud] Cleared ${dbName}.${table}`); - }; - } catch (ex) {} - }; + const request = window.indexedDB.open(dbName); + request.onsuccess = (e) => { + const db = e.target.result; + try { + const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear(); + objectStoreRequest.onsuccess = function() { + console.log(`[Better xCloud] Cleared ${dbName}.${table}`); + }; + } catch (ex) {} + }; } function clearAllLogs() { - clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); + clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs"); } function updateIceCandidates(candidates, options) { - const 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; - const groups = pattern.exec(item2.candidate).groups; - lst.push(groups); - } - if (options.preferIpv6Server) lst.sort((a, b) => { - const firstIp = a.ip, secondIp = b.ip; - return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; - }); - const newCandidates = []; - let foundation = 1; - const newCandidate = (candidate) => { - return { - candidate, - messageType: "iceCandidate", - sdpMLineIndex: "0", - sdpMid: "0" - }; + const 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; + const groups = pattern.exec(item2.candidate).groups; + lst.push(groups); + } + if (options.preferIpv6Server) lst.sort((a, b) => { + const firstIp = a.ip, secondIp = b.ip; + return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1; + }); + const newCandidates = []; + let foundation = 1; + const 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; + }; + 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) { - const response = await NATIVE_FETCH(request), text = await response.clone().text(); - if (!text.length) return response; - const options = { - preferIpv6Server: getPref("prefer_ipv6_server"), - consoleAddrs - }, obj = JSON.parse(text); - let 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; + const response = await NATIVE_FETCH(request), text = await response.clone().text(); + if (!text.length) return response; + const options = { + preferIpv6Server: getPref("prefer_ipv6_server"), + consoleAddrs + }, obj = JSON.parse(text); + let 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 (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ - "https://arc.msn.com", - "https://browser.events.data.microsoft.com", - "https://dc.services.visualstudio.com", - "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" - ]); - if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ - "https://peoplehub.xboxlive.com/users/me/people/social", - "https://peoplehub.xboxlive.com/users/me/people/recommendations", - "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" - ]); - const 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 blocked of BLOCKED_URLS) - if (this._url.startsWith(blocked)) { - if (blocked === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); - return !1; - } - return nativeXhrSend.apply(this, arguments); - }; - let gamepassAllGames = []; - 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)) continue; - return new Response('{"acc":1,"webResult":{}}', { - status: 200, - statusText: "200 OK" - }); + let BLOCKED_URLS = []; + if (getPref("block_tracking")) clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://arc.msn.com", + "https://browser.events.data.microsoft.com", + "https://dc.services.visualstudio.com", + "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io" + ]); + if (getPref("block_social_features")) BLOCKED_URLS = BLOCKED_URLS.concat([ + "https://peoplehub.xboxlive.com/users/me/people/social", + "https://peoplehub.xboxlive.com/users/me/people/recommendations", + "https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox" + ]); + const 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 blocked of BLOCKED_URLS) + if (this._url.startsWith(blocked)) { + if (blocked === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000); + return !1; + } + return nativeXhrSend.apply(this, arguments); + }; + let gamepassAllGames = []; + 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)) continue; + return new Response('{"acc":1,"webResult":{}}', { + status: 200, + statusText: "200 OK" + }); + } + if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING); + if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING); + if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { + const 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) { + console.log(e); + } + if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c")) 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((id2) => gamepassAllGames.includes(id2)); + const newCustomList = customList.map((item2) => ({ id: item2 })); + obj.push(...newCustomList); + } catch (e) { + console.log(e); } - if (url.endsWith("/play")) BxEvent.dispatch(window, BxEvent.STREAM_LOADING); - if (url.endsWith("/configuration")) BxEvent.dispatch(window, BxEvent.STREAM_STARTING); - if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try { - const 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) { - console.log(e); - } - if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) { - const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); - if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c")) 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((id2) => gamepassAllGames.includes(id2)); - const 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")) { - const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); - try { - const 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); - }; + return response.json = () => Promise.resolve(obj), response; + } + if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) { + const response = await NATIVE_FETCH(request, init), obj = await response.clone().json(); + try { + const 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 showGamepadToast(gamepad) { - if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; - BxLogger.info("Gamepad", gamepad); - let text = "🎮"; - if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; - const 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 }); + if (gamepad.id === VIRTUAL_GAMEPAD_ID) return; + BxLogger.info("Gamepad", gamepad); + let text = "🎮"; + if (getPref("local_co_op_enabled")) text += ` #${gamepad.index + 1}`; + const 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 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-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:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:'promptfont';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}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-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)}.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}select[multiple]{overflow:auto}#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}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);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-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-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))}.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-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));backdrop-filter:blur(4px) brightness(1.5)}.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);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-left:10px}.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-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-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:20px;border-radius:8px;z-index:var(--bx-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-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-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-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-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-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-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-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.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-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]: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-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.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;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);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 > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-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}@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 + *{margin:0 0 0 auto}.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}.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-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.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-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-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.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}.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;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;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}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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 span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{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 button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;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[data-disabled=true] button{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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;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:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.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:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.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}}`; - const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; - if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); - 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 (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); - if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; - if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; - if (getPref("hide_dots_icon")) 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}", getPref("stream_simplify_menu")) 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 (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; - const $style = CE("style", {}, css); - document.documentElement.appendChild($style); + 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-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:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:99999;--bx-toast-z-index:60000;--bx-dialog-z-index:50000;--bx-dialog-overlay-z-index:40200;--bx-stats-bar-z-index:40100;--bx-mkb-pointer-lock-msg-z-index:40000;--bx-navigation-dialog-z-index:30100;--bx-navigation-dialog-overlay-z-index:30000;--bx-game-bar-z-index:10000;--bx-screenshot-animation-z-index:9000;--bx-wait-time-box-z-index:1000}@font-face{font-family:'promptfont';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf")}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-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)}.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}select[multiple]{overflow:auto}#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}div[class*=SupportedInputsBadge]:not(:has(:nth-child(2))),div[class*=SupportedInputsBadge] svg:first-of-type{display:none}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.549);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-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-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))}.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-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));backdrop-filter:blur(4px) brightness(1.5)}.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);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-left:10px}.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-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-dialog-overlay-z-index);background:#000;opacity:50%}.bx-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:20px;border-radius:8px;z-index:var(--bx-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-dialog *:focus{outline:none !important}.bx-dialog h2{display:flex;margin-bottom:12px}.bx-dialog h2 b{flex:1;color:#fff;display:block;font-family:var(--bx-title-font);font-size:26px;font-weight:400;line-height:var(--bx-button-height)}.bx-dialog.bx-binding-dialog h2 b{font-family:var(--bx-promptfont-font) !important}.bx-dialog > div{overflow:auto;padding:2px 0}.bx-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-dialog > button:hover{background-color:#515863}}.bx-dialog > button:focus{background-color:#515863}@media screen and (max-width:450px){.bx-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-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-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-focusable::after{border-radius:4px}.bx-settings-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.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-dialog input{accent-color:var(--bx-primary-button-color)}.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-settings-dialog select option:disabled{display:none}.bx-settings-dialog input[type=checkbox]: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-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-settings-dialog a:hover,.bx-settings-dialog a:focus{color:#5dc21e}.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;padding:10px;margin-left:48px;width:450px;max-width:calc(100vw - tabsWidth);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 > div[data-tab-group=mkb]{display:flex;flex-direction:column;height:100%;overflow:hidden}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:first-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=true] > div:last-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:first-of-type{display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] > div[data-has-gamepad=false] > div:last-of-type{display:none}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-profile{width:100%;height:36px;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-note{margin-top:10px;font-size:14px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row{display:flex;margin-bottom:10px}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row label.bx-prompt{flex:1;font-size:26px;margin-bottom:0}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions{flex:2;position:relative}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select{position:absolute;width:100%;height:100%;display:block}.bx-settings-tab-contents > div[data-tab-group=shortcuts] .bx-shortcut-row .bx-shortcut-actions select:last-of-type{opacity:0;z-index:calc(var(--bx-settings-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}@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 + *{margin:0 0 0 auto}.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}.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-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:10px;border-top-right-radius:10px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:10px;border-bottom-right-radius:10px}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:10px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861}.bx-suggest-toggler label{flex:1;margin-bottom:0;padding:10px;background:#004f87}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:40px;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;background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.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-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-container{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;width:420px;max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:20px}.bx-remote-play-container > .bx-button{display:table;margin:0 0 0 auto}.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}.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;padding:4px 0}.bx-remote-play-device-name{font-size:20px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:12px;background:#004c87;color:#fff;display:inline-block;border-radius:14px;padding:2px 10px;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}.bx-select{display:flex;align-items:center;flex:0 1 auto}.bx-select select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-select > div,.bx-select button.bx-select-value{min-width:120px;text-align:left;margin:0 8px;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;flex:1}.bx-select > div{display:inline-block}.bx-select > div input{display:inline-block;margin-right:8px}.bx-select > div label{margin-bottom:0;font-size:14px;width:100%}.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:initial}.bx-select button.bx-select-value{border:none;display:inline-flex;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}.bx-select button.bx-select-value:hover input,.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}.bx-select button.bx-select-value:hover::after,.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}.bx-select button.bx-button{border:none;height:24px;width:24px;padding:0;line-height:24px;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}.bx-select button.bx-button span{line-height:unset}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}html[data-xds-platform=tv] .bx-guide-home-achievements-progress{flex-direction:column}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress{flex-direction:row}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}html:not([data-xds-platform=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}html:not([data-xds-platform=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}html[data-xds-platform=tv] .bx-guide-home-buttons > div{flex-direction:column}html[data-xds-platform=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}html:not([data-xds-platform=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]{display:flex}div[data-testid=media-container].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)}#game-stream video{margin:auto;align-self:center;background:#000}#game-stream canvas{position:absolute;align-self:center;margin:auto;left:0;right:0}#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 span{display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);font-size:13px;margin:0 4px}.bx-number-stepper button{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 button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type="range"]{display:block;margin:12px auto 2px;width:180px;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[data-disabled=true] button{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-transparent=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{min-width:60px;display:inline-block;text-align:right;vertical-align:middle}.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-stats-bar span:first-of-type{min-width:22px}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-settings select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;text-align:right;border:none;color:#fff}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:50%;transform:translateX(-50%) translateY(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;text-align:center;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:220px;opacity:.9}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > div:first-of-type{display:flex;flex-direction:column;text-align:left}.bx-mkb-pointer-lock-msg p{margin:0}.bx-mkb-pointer-lock-msg p:first-child{font-size:22px;margin-bottom:4px;font-weight:bold}.bx-mkb-pointer-lock-msg p:last-child{font-size:12px;font-style:italic}.bx-mkb-pointer-lock-msg > div:last-of-type{margin-top:10px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type{margin-bottom:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div{display:flex;flex-flow:row;margin-top:8px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button{flex:1}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type{margin-right:5px}.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type{margin-left:5px}.bx-mkb-preset-tools{display:flex;margin-bottom:12px}.bx-mkb-preset-tools select{flex:1}.bx-mkb-preset-tools button{margin-left:6px}.bx-mkb-settings-rows{flex:1;overflow:scroll}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:26px;text-align:center;width:26px;height:32px;line-height:32px}.bx-mkb-key-row button{flex:1;height:32px;line-height:32px;margin:0 0 0 10px;background:transparent;border:none;color:#fff;border-radius:0;border-left:1px solid #373737}.bx-mkb-key-row button:hover{background:transparent;cursor:default}.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:16px 0 10px;font-size:12px}.bx-mkb-note:first-of-type{margin-top:0}.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}}`; + const PREF_HIDE_SECTIONS = getPref("ui_hide_sections"), selectorToHide = []; + if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]"); + 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 (getPref("block_social_features")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]"); + if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }"; + if (getPref("reduce_animations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}"; + if (getPref("hide_dots_icon")) 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}", getPref("stream_simplify_menu")) 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 (getPref("ui_scrollbar_hide")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}"; + const $style = CE("style", {}, css); + document.documentElement.appendChild($style); } function preloadFonts() { - const $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); + const $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 #timeout; - static #cursorVisible = !0; - static show() { - document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; - } - static hide() { - document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; - } - static onMouseMove(e) { - !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); - } - static start() { - MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); - } - static stop() { - MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); - } + static #timeout; + static #cursorVisible = !0; + static show() { + document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0; + } + static hide() { + document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1; + } + static onMouseMove(e) { + !MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000); + } + static start() { + MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove); + } + static stop() { + MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show(); + } } function patchHistoryMethod(type) { - const orig = window.history[type]; - return function(...args) { - return BxEvent.dispatch(window, BxEvent.POPSTATE, { - arguments: args - }), orig.apply(this, arguments); - }; + const 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); - const $settings = document.querySelector(".bx-settings-container"); - if ($settings) $settings.classList.add("bx-gone"); - NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return; + window.setTimeout(RemotePlayManager.detect, 10); + const $settings = document.querySelector(".bx-settings-container"); + if ($settings) $settings.classList.add("bx-gone"); + NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); } function overridePreloadState() { - let _state; - Object.defineProperty(window, "__PRELOADED_STATE__", { - configurable: !0, - get: () => { - return _state; - }, - set: (state) => { - try { - state.appContext.requestInfo.userAgent = window.navigator.userAgent; - } catch (e) { - BxLogger.error(LOG_TAG6, e); - } - if (STATES.userAgent.capabilities.touch) try { - const sigls = state.xcloud.sigls; - if ("9c86f07a-f3e8-45ad-82a0-a1f759597059" in sigls) { - let customList = TouchController.getCustomList(); - const allGames = sigls["29a81209-df6f-41fd-a528-2ae6b91f719c"].data.products; - customList = customList.filter((id2) => allGames.includes(id2)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); - } - if (BX_FLAGS.ForceNativeMkbTitles && "8fa264dd-124f-4af3-97e8-596fcdf4b486" in sigls) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles); - } catch (e) { - BxLogger.error(LOG_TAG6, e); - } - if (getPref("ui_home_context_menu_disabled")) try { - state.experiments.experimentationInfo.data.treatments.EnableHomeContextMenu = !1; - } catch (e) { - BxLogger.error(LOG_TAG6, e); - } - _state = state, STATES.appContext = deepClone(state.appContext); + let _state; + Object.defineProperty(window, "__PRELOADED_STATE__", { + configurable: !0, + get: () => { + return _state; + }, + set: (state) => { + try { + state.appContext.requestInfo.userAgent = window.navigator.userAgent; + } catch (e) { + BxLogger.error(LOG_TAG6, e); + } + if (STATES.userAgent.capabilities.touch) try { + const sigls = state.xcloud.sigls; + if ("9c86f07a-f3e8-45ad-82a0-a1f759597059" in sigls) { + let customList = TouchController.getCustomList(); + const allGames = sigls["29a81209-df6f-41fd-a528-2ae6b91f719c"].data.products; + customList = customList.filter((id2) => allGames.includes(id2)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList); + } + if (BX_FLAGS.ForceNativeMkbTitles && "8fa264dd-124f-4af3-97e8-596fcdf4b486" in sigls) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles); + } catch (e) { + BxLogger.error(LOG_TAG6, e); } - }); + if (getPref("ui_home_context_menu_disabled")) try { + state.experiments.experimentationInfo.data.treatments.EnableHomeContextMenu = !1; + } catch (e) { + BxLogger.error(LOG_TAG6, e); + } + _state = state, STATES.appContext = deepClone(state.appContext); + } + }); } var LOG_TAG6 = "PreloadState"; function setCodecPreferences(sdp, preferredCodec) { - const 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) { - const id2 = match[1]; - if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id2); - } - if (!preferredCodecIds.length) return sdp; - const lines = sdp.split("\r\n"); - for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { - const line = lines[lineIndex]; - if (!line.startsWith("m=video")) continue; - const tmp = line.trim().split(" "); - let 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\n"); + const 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) { + const id2 = match[1]; + if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id2); + } + if (!preferredCodecIds.length) return sdp; + const lines = sdp.split("\r\n"); + for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) { + const line = lines[lineIndex]; + if (!line.startsWith("m=video")) continue; + const tmp = line.trim().split(" "); + let 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\n"); } function patchSdpBitrate(sdp, video, audio) { - const lines = sdp.split("\r\n"), mediaSet = new Set; - !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); - const 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; - const 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; - } - } + const lines = sdp.split("\r\n"), mediaSet = new Set; + !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio"); + const 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; + const 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\n"); + } + return lines.join("\r\n"); } var clarity_boost_default = "#version 300 es\n\nin vec4 position;\n\nvoid main() {\ngl_Position = position;\n}\n"; var clarity_boost_default2 = "#version 300 es\n\nprecision mediump float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nconst int FILTER_UNSHARP_MASKING = 1;\n\nconst float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;\n\nconst vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nout vec4 fragColor;\n\nvec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) {\nvec2 texelSize = 1.0 / iResolution.xy;\n\nvec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb;\nvec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb;\nvec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb;\n\nvec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb;\nvec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb;\n\nvec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb;\nvec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb;\nvec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb;\n\nif (filterId == FILTER_UNSHARP_MASKING) {\nvec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;\ngaussianBlur /= 16.0;\n\nreturn e + (e - gaussianBlur) * sharpenFactor / 3.0;\n}\n\nvec3 minRgb = min(min(min(d, e), min(f, b)), h);\nminRgb += min(min(a, c), min(g, i));\n\nvec3 maxRgb = max(max(max(d, e), max(f, b)), h);\nmaxRgb += max(max(a, c), max(g, i));\n\nvec3 reciprocalMaxRgb = 1.0 / maxRgb;\nvec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\namplifyRgb = inversesqrt(amplifyRgb);\n\nvec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));\nvec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\nvec3 window = b + d + f + h;\nvec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\nreturn mix(e, outColor, sharpenFactor / 2.0);\n}\n\nvoid main() {\nvec2 uv = gl_FragCoord.xy / iResolution.xy;\nvec3 color = texture(data, uv).rgb;\n\ncolor = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color;\n\ncolor = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color;\n\ncolor = contrast * (color - 0.5) + 0.5;\n\ncolor = brightness * color;\n\nfragColor = vec4(color, 1.0);\n}\n"; 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 = Math.ceil(1000 / this.targetFps); - lastFrameTime = 0; - animFrameId = null; - constructor($video) { - BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; - const $canvas = document.createElement("canvas"); - $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas); + 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 = Math.ceil(1000 / this.targetFps); + lastFrameTime = 0; + animFrameId = null; + constructor($video) { + BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video; + const $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() { + const 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); + } + drawFrame() { + if (this.targetFps === 0) return; + if (this.targetFps < 60) { + const currentTime = performance.now(); + if (currentTime - this.lastFrameTime < this.frameInterval) return; + this.lastFrameTime = currentTime; } - setFilter(filterId, update = !0) { - this.options.filterId = filterId, update && this.updateCanvas(); + const 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 animate; + if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { + const $video = this.$video; + animate = () => { + if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate); + }, this.animFrameId = $video.requestVideoFrameCallback(animate); + } else animate = () => { + if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate); + }, this.animFrameId = requestAnimationFrame(animate); + } + setupShaders() { + BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); + const gl = this.$canvas.getContext("webgl2", { + isBx: !0, + antialias: !0, + alpha: !1, + powerPreference: getPref("video_power_preference") + }); + this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); + const vShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); + const fShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); + const 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(); + const 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); + const 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; } - 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.frameInterval = Math.ceil(1000 / target); - } - getCanvas() { - return this.$canvas; - } - updateCanvas() { - const 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); - } - drawFrame() { - if (this.targetFps < 60) { - const currentTime = performance.now(); - if (currentTime - this.lastFrameTime < this.frameInterval) return; - this.lastFrameTime = currentTime; - } - const 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 animate; - if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) { - const $video = this.$video; - animate = () => { - if (!this.stopped) this.drawFrame(), this.animFrameId = $video.requestVideoFrameCallback(animate); - }, this.animFrameId = $video.requestVideoFrameCallback(animate); - } else animate = () => { - if (!this.stopped) this.drawFrame(), this.animFrameId = requestAnimationFrame(animate); - }, this.animFrameId = requestAnimationFrame(animate); - } - setupShaders() { - BxLogger.info(this.LOG_TAG, "Setting up", getPref("video_power_preference")); - const gl = this.$canvas.getContext("webgl2", { - isBx: !0, - antialias: !0, - alpha: !1, - powerPreference: getPref("video_power_preference") - }); - this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); - const vShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader); - const fShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader); - const 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(); - const 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); - const 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(); - const gl = this.gl; - if (gl) { - gl.getExtension("WEBGL_lose_context")?.loseContext(); - for (let resource of this.resources) - if (resource instanceof WebGLProgram) gl.useProgram(null), 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; + } + destroy() { + BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); + const 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); + $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) { + this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix"); + return; } - setupVideoElements() { - if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) { - this.$usmMatrix = this.$videoCss.querySelector("#bx-filter-usm-matrix"); - return; - } - const $fragment = document.createDocumentFragment(); - this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); - const $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); + const $fragment = document.createDocumentFragment(); + this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss); + const $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() { + const filters = [], sharpness = this.options.sharpness || 0; + if (this.options.processing === "usm" && sharpness != 0) { + const 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)"); } - getVideoPlayerFilterStyle() { - const filters = [], sharpness = this.options.sharpness || 0; - if (this.options.processing === "usm" && sharpness != 0) { - const 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)"); - } - const saturation = this.options.saturation || 100; - if (saturation != 100) filters.push(`saturate(${saturation}%)`); - const contrast = this.options.contrast || 100; - if (contrast != 100) filters.push(`contrast(${contrast}%)`); - const brightness = this.options.brightness || 100; - if (brightness != 100) filters.push(`brightness(${brightness}%)`); - return filters.join(" "); - } - resizePlayer() { - const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; - let $webGL2Canvas; - if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); - let targetWidth, targetHeight, targetObjectFit; - if (PREF_RATIO.includes(":")) { - const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); - let width = 0, height = 0; - const 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(), 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; - if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); - } - setPlayerType(type, refreshPlayer = !1) { - if (this.playerType !== type) if (type === "webgl2") { - if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); - else this.webGL2Player.resume(); - this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel"); - } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel"); - 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") { - const options = this.options, webGL2Player = this.webGL2Player; - if (options.processing === "usm") webGL2Player.setFilter(1); - else webGL2Player.setFilter(2); - Screenshot.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 (getPref("screenshot_apply_filters")) Screenshot.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(); + const saturation = this.options.saturation || 100; + if (saturation != 100) filters.push(`saturate(${saturation}%)`); + const contrast = this.options.contrast || 100; + if (contrast != 100) filters.push(`contrast(${contrast}%)`); + const brightness = this.options.brightness || 100; + if (brightness != 100) filters.push(`brightness(${brightness}%)`); + return filters.join(" "); + } + resizePlayer() { + const PREF_RATIO = getPref("video_ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; + let $webGL2Canvas; + if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas(); + let targetWidth, targetHeight, targetObjectFit; + if (PREF_RATIO.includes(":")) { + const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]); + let width = 0, height = 0; + const 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(), 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; + if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions(); + } + setPlayerType(type, refreshPlayer = !1) { + if (this.playerType !== type) if (type === "webgl2") { + if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video); + else this.webGL2Player.resume(); + this.$videoCss.textContent = "", this.$video.classList.add("bx-pixel"); + } else this.webGL2Player?.stop(), this.$video.classList.remove("bx-pixel"); + 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") { + const options = this.options, webGL2Player = this.webGL2Player; + if (options.processing === "usm") webGL2Player.setFilter(1); + else webGL2Player.setFilter(2); + Screenshot.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 (getPref("screenshot_apply_filters")) Screenshot.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() { - const PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { - if (this.style.visibility = "visible", !this.videoWidth) return; - const playerOptions = { - processing: getPref("video_processing"), - sharpness: getPref("video_sharpness"), - saturation: getPref("video_saturation"), - contrast: getPref("video_contrast"), - brightness: getPref("video_brightness") - }; - STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_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); - } - const $parent = this.parentElement; - if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); - return nativePlay.apply(this); + const PREF_SKIP_SPLASH_VIDEO = getPref("skip_splash_video"), showFunc = function() { + if (this.style.visibility = "visible", !this.videoWidth) return; + const playerOptions = { + processing: getPref("video_processing"), + sharpness: getPref("video_sharpness"), + saturation: getPref("video_saturation"), + contrast: getPref("video_contrast"), + brightness: getPref("video_brightness") }; + STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video_player_type"), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_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); + } + const $parent = this.parentElement; + if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 }); + return nativePlay.apply(this); + }; } function patchRtcCodecs() { - if (getPref("stream_codec_profile") === "default") return; - if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; + if (getPref("stream_codec_profile") === "default") return; + if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1; } function patchRtcPeerConnection() { - const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; - RTCPeerConnection.prototype.createDataChannel = function() { - const dataChannel = nativeCreateDataChannel.apply(this, arguments); - return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { - dataChannel - }), dataChannel; - }; - const maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); - if (codec !== "default" || maxVideoBitrate > 0) { - const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; - RTCPeerConnection.prototype.setLocalDescription = function(description) { - if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); - try { - if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); - } catch (e) { - BxLogger.error("setLocalDescription", e); - } - return nativeSetLocalDescription.apply(this, arguments); - }; - } - const OrgRTCPeerConnection = window.RTCPeerConnection; - window.RTCPeerConnection = function() { - const conn = new OrgRTCPeerConnection; - return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { - BxLogger.info("connectionstatechange", conn.connectionState); - }), conn; + const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; + RTCPeerConnection.prototype.createDataChannel = function() { + const dataChannel = nativeCreateDataChannel.apply(this, arguments); + return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, { + dataChannel + }), dataChannel; + }; + const maxVideoBitrate = getPref("bitrate_video_max"), codec = getPref("stream_codec_profile"); + if (codec !== "default" || maxVideoBitrate > 0) { + const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription; + RTCPeerConnection.prototype.setLocalDescription = function(description) { + if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec); + try { + if (maxVideoBitrate > 0 && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000)); + } catch (e) { + BxLogger.error("setLocalDescription", e); + } + return nativeSetLocalDescription.apply(this, arguments); }; + } + const OrgRTCPeerConnection = window.RTCPeerConnection; + window.RTCPeerConnection = function() { + const conn = new OrgRTCPeerConnection; + return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => { + BxLogger.info("connectionstatechange", conn.connectionState); + }), conn; + }; } function patchAudioContext() { - const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; - window.AudioContext = function(options) { - if (options && options.latencyHint) options.latencyHint = 0; - const ctx = new OrgAudioContext(options); - return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { - const gainNode = nativeCreateGain.apply(this); - return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; - }, STATES.currentStream.audioContext = ctx, ctx; - }; + const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain; + window.AudioContext = function(options) { + if (options && options.latencyHint) options.latencyHint = 0; + const ctx = new OrgAudioContext(options); + return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() { + const gainNode = nativeCreateGain.apply(this); + return gainNode.gain.value = getPref("audio_volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode; + }, STATES.currentStream.audioContext = ctx, ctx; + }; } function patchMeControl() { - const 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); + const 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() { - window.adobe = Object.freeze({}); + window.adobe = Object.freeze({}); } function patchCanvasContext() { - const 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]); - }; + const 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)); - }; + 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) { - BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); - } + constructor() {} + reset() {} + onClick(e) { + BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); + } + render() { + return this.$content; + } } class ScreenshotAction extends BaseGameBarAction { - $content; - constructor() { - super(); - this.$content = createButton({ - style: 4, - icon: BxIcon.SCREENSHOT, - title: t("take-screenshot"), - onClick: this.onClick.bind(this) - }); - } - onClick(e) { - super.onClick(e), Screenshot.takeScreenshot(); - } - render() { - return this.$content; - } + $content; + constructor() { + super(); + this.$content = createButton({ + style: 4, + icon: BxIcon.SCREENSHOT, + title: t("take-screenshot"), + onClick: this.onClick.bind(this) + }); + } + onClick(e) { + super.onClick(e), Screenshot.takeScreenshot(); + } } class TouchControlAction extends BaseGameBarAction { - $content; - constructor() { - super(); - const $btnEnable = createButton({ - style: 4, - icon: BxIcon.TOUCH_CONTROL_ENABLE, - title: t("show-touch-controller"), - onClick: this.onClick.bind(this) - }), $btnDisable = createButton({ - style: 4, - icon: BxIcon.TOUCH_CONTROL_DISABLE, - title: t("hide-touch-controller"), - onClick: this.onClick.bind(this), - classes: ["bx-activated"] - }); - this.$content = CE("div", {}, $btnEnable, $btnDisable), this.reset(); - } - onClick(e) { - super.onClick(e); - const isVisible = TouchController.toggleVisibility(); - this.$content.dataset.activated = (!isVisible).toString(); - } - render() { - return this.$content; - } - reset() { - this.$content.dataset.activated = "false"; - } + $content; + constructor() { + super(); + const $btnEnable = createButton({ + style: 4, + icon: BxIcon.TOUCH_CONTROL_ENABLE, + title: t("show-touch-controller"), + onClick: this.onClick.bind(this) + }), $btnDisable = createButton({ + style: 4, + icon: BxIcon.TOUCH_CONTROL_DISABLE, + title: t("hide-touch-controller"), + onClick: this.onClick.bind(this), + classes: ["bx-activated"] + }); + this.$content = CE("div", {}, $btnEnable, $btnDisable); + } + onClick(e) { + super.onClick(e); + const isVisible = TouchController.toggleVisibility(); + this.$content.dataset.activated = (!isVisible).toString(); + } + reset() { + this.$content.dataset.activated = "false"; + } } class MicrophoneAction extends BaseGameBarAction { - $content; - visible = !1; - constructor() { - super(); - const $btnDefault = createButton({ - style: 4, - icon: BxIcon.MICROPHONE, - onClick: this.onClick.bind(this), - classes: ["bx-activated"] - }), $btnMuted = createButton({ - style: 4, - icon: BxIcon.MICROPHONE_MUTED, - onClick: this.onClick.bind(this) - }); - this.$content = CE("div", {}, $btnMuted, $btnDefault), this.reset(), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { - const enabled = e.microphoneState === "Enabled"; - this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); - }); - } - onClick(e) { - super.onClick(e); - const enabled = MicrophoneShortcut.toggle(!1); - this.$content.dataset.activated = enabled.toString(); - } - render() { - return this.$content; - } - reset() { - this.visible = !1, this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false"; - } + $content; + constructor() { + super(); + const $btnDefault = createButton({ + style: 4, + icon: BxIcon.MICROPHONE, + onClick: this.onClick.bind(this), + classes: ["bx-activated"] + }), $btnMuted = createButton({ + style: 4, + icon: BxIcon.MICROPHONE_MUTED, + onClick: this.onClick.bind(this) + }); + this.$content = CE("div", {}, $btnMuted, $btnDefault), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { + const enabled = e.microphoneState === "Enabled"; + this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); + }); + } + onClick(e) { + super.onClick(e); + const 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: 4, - icon: BxIcon.TRUE_ACHIEVEMENTS, - title: t("true-achievements"), - onClick: this.onClick.bind(this) - }); - } - onClick(e) { - super.onClick(e), TrueAchievements.open(!1); - } - render() { - return this.$content; - } + $content; + constructor() { + super(); + this.$content = createButton({ + style: 4, + icon: BxIcon.TRUE_ACHIEVEMENTS, + onClick: this.onClick.bind(this) + }); + } + onClick(e) { + super.onClick(e), TrueAchievements.open(!1); + } } class SpeakerAction extends BaseGameBarAction { - $content; - constructor() { - super(); - const $btnEnable = createButton({ - style: 4, - icon: BxIcon.AUDIO, - onClick: this.onClick.bind(this) - }), $btnMuted = createButton({ - style: 4, - icon: BxIcon.SPEAKER_MUTED, - onClick: this.onClick.bind(this), - classes: ["bx-activated"] - }); - this.$content = CE("div", {}, $btnEnable, $btnMuted), this.reset(), window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, (e) => { - const enabled = e.speakerState === 0; - this.$content.dataset.activated = (!enabled).toString(); - }); - } - onClick(e) { - super.onClick(e), SoundShortcut.muteUnmute(); - } - render() { - return this.$content; - } - reset() { - this.$content.dataset.activated = "false"; - } + $content; + constructor() { + super(); + const $btnEnable = createButton({ + style: 4, + icon: BxIcon.AUDIO, + onClick: this.onClick.bind(this) + }), $btnMuted = createButton({ + style: 4, + icon: BxIcon.SPEAKER_MUTED, + onClick: this.onClick.bind(this), + classes: ["bx-activated"] + }); + this.$content = CE("div", {}, $btnEnable, $btnMuted), window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, (e) => { + const enabled = e.speakerState === 0; + this.$content.dataset.activated = (!enabled).toString(); + }); + } + onClick(e) { + super.onClick(e), SoundShortcut.muteUnmute(); + } + reset() { + this.$content.dataset.activated = "false"; + } } class RendererShortcut { - static toggleVisibility() { - const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); - if (!$mediaContainer) return !0; - return $mediaContainer.classList.toggle("bx-gone"), !$mediaContainer.classList.contains("bx-gone"); - } + static toggleVisibility() { + const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); + if (!$mediaContainer) return !0; + $mediaContainer.classList.toggle("bx-gone"); + const isShowing = !$mediaContainer.classList.contains("bx-gone"); + return limitVideoPlayerFps(isShowing ? getPref("video_max_fps") : 0), isShowing; + } } class RendererAction extends BaseGameBarAction { - $content; - constructor() { - super(); - const $btnDefault = createButton({ - style: 4, - icon: BxIcon.EYE, - onClick: this.onClick.bind(this) - }), $btnActivated = createButton({ - style: 4, - icon: BxIcon.EYE_SLASH, - onClick: this.onClick.bind(this), - classes: ["bx-activated"] - }); - this.$content = CE("div", {}, $btnDefault, $btnActivated), this.reset(); - } - onClick(e) { - super.onClick(e); - const isVisible = RendererShortcut.toggleVisibility(); - this.$content.dataset.activated = (!isVisible).toString(); - } - render() { - return this.$content; - } - reset() { - this.$content.dataset.activated = "false"; - } + $content; + constructor() { + super(); + const $btnDefault = createButton({ + style: 4, + icon: BxIcon.EYE, + onClick: this.onClick.bind(this) + }), $btnActivated = createButton({ + style: 4, + icon: BxIcon.EYE_SLASH, + onClick: this.onClick.bind(this), + classes: ["bx-activated"] + }); + this.$content = CE("div", {}, $btnDefault, $btnActivated); + } + onClick(e) { + super.onClick(e); + const isVisible = RendererShortcut.toggleVisibility(); + this.$content.dataset.activated = (!isVisible).toString(); + } + reset() { + this.$content.dataset.activated = "false"; + } } class GameBar { - static instance; - static getInstance() { - if (!GameBar.instance) GameBar.instance = new GameBar; - return GameBar.instance; - } - static VISIBLE_DURATION = 2000; - $gameBar; - $container; - timeoutId = null; - actions = []; - constructor() { - let $container; - const position = getPref("game_bar_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 && getPref("stream_touch_controller") !== "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(); - }), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => { - const classList = $container.classList; - if (classList.contains("bx-hide")) classList.remove("bx-hide"), classList.add("bx-offscreen"); - }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref("game_bar_position") !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { - if (!STATES.isPlaying) { - this.disable(); - return; - } - e.mode !== "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 && this.$gameBar.classList.remove("bx-gone"); - } - disable() { - this.hideBar(), this.$gameBar && this.$gameBar.classList.add("bx-gone"); - } - showBar() { - if (!this.$container) return; - this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout(); - } - hideBar() { - if (this.clearHideTimeout(), clearFocus(), !this.$container) return; - this.$container.classList.remove("bx-show"), this.$container.classList.add("bx-hide"); - } - reset() { - for (let action of this.actions) - action.reset(); - } + static instance; + static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar); + static VISIBLE_DURATION = 2000; + $gameBar; + $container; + timeoutId = null; + actions = []; + constructor() { + let $container; + const position = getPref("game_bar_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 && getPref("stream_touch_controller") !== "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(); + }), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => { + $container.classList.replace("bx-hide", "bx-offscreen"); + }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref("game_bar_position") !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { + if (!STATES.isPlaying) { + this.disable(); + return; + } + e.mode !== "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(), clearFocus(), this.$container.classList.replace("bx-show", "bx-hide"); + } + reset() { + this.actions.forEach((action) => action.reset()); + } } class XcloudApi { - static instance; - static getInstance() { - if (!XcloudApi.instance) XcloudApi.instance = new XcloudApi; - return XcloudApi.instance; + static instance; + static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi); + CACHE_TITLES = {}; + CACHE_WAIT_TIME = {}; + async getTitleInfo(id2) { + if (id2 in this.CACHE_TITLES) return this.CACHE_TITLES[id2]; + const baseUri = STATES.selectedRegion.baseUri; + if (!baseUri || !STATES.gsToken) return null; + 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: [id2], + alternateIdType: "productId" + }) + })).json()).results[0]; + } catch (e) { + json = {}; } - CACHE_TITLES = {}; - CACHE_WAIT_TIME = {}; - async getTitleInfo(id2) { - if (id2 in this.CACHE_TITLES) return this.CACHE_TITLES[id2]; - const baseUri = STATES.selectedRegion.baseUri; - if (!baseUri || !STATES.gsToken) return null; - 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: [id2], - alternateIdType: "productId" - }) - })).json()).results[0]; - } catch (e) { - json = {}; + return this.CACHE_TITLES[id2] = json, json; + } + async getWaitTime(id2) { + if (id2 in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id2]; + const baseUri = STATES.selectedRegion.baseUri; + if (!baseUri || !STATES.gsToken) return null; + let json; + try { + json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id2}`, { + method: "GET", + headers: { + Authorization: `Bearer ${STATES.gsToken}` } - return this.CACHE_TITLES[id2] = json, json; - } - async getWaitTime(id2) { - if (id2 in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id2]; - const baseUri = STATES.selectedRegion.baseUri; - if (!baseUri || !STATES.gsToken) return null; - let json; - try { - json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id2}`, { - method: "GET", - headers: { - Authorization: `Bearer ${STATES.gsToken}` - } - })).json(); - } catch (e) { - json = {}; - } - return this.CACHE_WAIT_TIME[id2] = json, json; + })).json(); + } catch (e) { + json = {}; } + return this.CACHE_WAIT_TIME[id2] = json, json; + } } class GameTile { - static #timeout; - static async#showWaitTime($elm, productId) { - if ($elm.hasWaitTime) return; - $elm.hasWaitTime = !0; - let totalWaitTime; - const api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId); - if (info) { - const waitTime = await api.getWaitTime(info.titleId); - if (waitTime) totalWaitTime = waitTime.estimatedAllocationTimeInSeconds; - } - if (typeof totalWaitTime === "number" && isElementVisible($elm)) { - const $div = CE("div", { class: "bx-game-tile-wait-time" }, createSvgIcon(BxIcon.PLAYTIME), CE("span", {}, secondsToHms(totalWaitTime))); - $elm.insertAdjacentElement("afterbegin", $div); - } + static #timeout; + static async#showWaitTime($elm, productId) { + if ($elm.hasWaitTime) return; + $elm.hasWaitTime = !0; + let totalWaitTime; + const api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId); + if (info) { + const waitTime = await api.getWaitTime(info.titleId); + if (waitTime) totalWaitTime = waitTime.estimatedAllocationTimeInSeconds; } - static #requestWaitTime($elm, productId) { - GameTile.#timeout && clearTimeout(GameTile.#timeout), GameTile.#timeout = 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) => { - const $elm = e.element; - if (($elm.className || "").includes("MruGameCard")) { - const $ol = $elm.closest("ol"); - if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => { - const productId = GameTile.#findProductId($elm2); - productId && GameTile.#showWaitTime($elm2, productId); - }); - } else { - const productId = GameTile.#findProductId($elm); - productId && GameTile.#requestWaitTime($elm, productId); - } - }); + if (typeof totalWaitTime === "number" && isElementVisible($elm)) { + const $div = CE("div", { class: "bx-game-tile-wait-time" }, createSvgIcon(BxIcon.PLAYTIME), CE("span", {}, secondsToHms(totalWaitTime))); + $elm.insertAdjacentElement("afterbegin", $div); } + } + static #requestWaitTime($elm, productId) { + GameTile.#timeout && clearTimeout(GameTile.#timeout), GameTile.#timeout = 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) => { + const $elm = e.element; + if (($elm.className || "").includes("MruGameCard")) { + const $ol = $elm.closest("ol"); + if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => { + const productId = GameTile.#findProductId($elm2); + productId && GameTile.#showWaitTime($elm2, productId); + }); + } else { + const productId = GameTile.#findProductId($elm); + productId && GameTile.#requestWaitTime($elm, productId); + } + }); + } } class ProductDetailsPage { - static $btnShortcut = AppInterface && createButton({ - icon: BxIcon.CREATE_SHORTCUT, - label: t("create-shortcut"), - style: 32, - tabIndex: 0, - onClick: (e) => { - AppInterface.createShortcut(window.location.pathname.substring(6)); - } - }); - static $btnWallpaper = AppInterface && createButton({ - icon: BxIcon.DOWNLOAD, - label: t("wallpaper"), - style: 32, - tabIndex: 0, - onClick: async (e) => { - try { - const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); - if (!matches?.groups) return; - const titleSlug = matches.groups.titleSlug.replaceAll("%" + "7C", "-"), productId = matches.groups.productId; - AppInterface.downloadWallpapers(titleSlug, productId); - } catch (e2) {} - } - }); - static injectTimeoutId = null; - static injectButtons() { - if (!AppInterface) return; - ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { - const $container = document.querySelector("div[class*=ActionButtons-module__container]"); - if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { - class: "bx-product-details-buttons" - }, BX_FLAGS.DeviceInfo.deviceType === "android" && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper)); - }, 500); + static $btnShortcut = AppInterface && createButton({ + icon: BxIcon.CREATE_SHORTCUT, + label: t("create-shortcut"), + style: 32, + tabIndex: 0, + onClick: (e) => { + AppInterface.createShortcut(window.location.pathname.substring(6)); } + }); + static $btnWallpaper = AppInterface && createButton({ + icon: BxIcon.DOWNLOAD, + label: t("wallpaper"), + style: 32, + tabIndex: 0, + onClick: async (e) => { + try { + const matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname); + if (!matches?.groups) return; + const titleSlug = matches.groups.titleSlug.replaceAll("%" + "7C", "-"), productId = matches.groups.productId; + AppInterface.downloadWallpapers(titleSlug, productId); + } catch (e2) {} + } + }); + static injectTimeoutId = null; + static injectButtons() { + if (!AppInterface) return; + ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => { + const $container = document.querySelector("div[class*=ActionButtons-module__container]"); + if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", { + class: "bx-product-details-buttons" + }, BX_FLAGS.DeviceInfo.deviceType === "android" && 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; - const $container = $btnOrg.cloneNode(!0); - let timeout; - if (STATES.browser.capabilities.touch) { - const onTransitionStart = (e) => { - if (e.propertyName !== "opacity") return; - timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none"; - }, onTransitionEnd = (e) => { - if (e.propertyName !== "opacity") return; - const $streamHud = e.target.closest("#StreamHud"); - if (!$streamHud) return; - if ($streamHud.style.left === "0px") { - const $target = e.target; - timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { - $target.style.pointerEvents = "auto"; - }, 100); - } - }; - $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); + static $btnStreamSettings; + static $btnStreamStats; + static $btnRefresh; + static $btnHome; + static observer; + static cloneStreamHudButton($btnOrg, label, svgIcon) { + if (!$btnOrg) return null; + const $container = $btnOrg.cloneNode(!0); + let timeout; + if (STATES.browser.capabilities.touch) { + const onTransitionStart = (e) => { + if (e.propertyName !== "opacity") return; + timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none"; + }, onTransitionEnd = (e) => { + if (e.propertyName !== "opacity") return; + const $streamHud = e.target.closest("#StreamHud"); + if (!$streamHud) return; + if ($streamHud.style.left === "0px") { + const $target = e.target; + timeout && clearTimeout(timeout), timeout = window.setTimeout(() => { + $target.style.pointerEvents = "auto"; + }, 100); } - const $button = $container.querySelector("button"); - if (!$button) return null; - $button.setAttribute("title", label); - const $orgSvg = $button.querySelector("svg"); - if (!$orgSvg) return null; - const $svg = createSvgIcon(svgIcon); - return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container; + }; + $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd); } - static cloneCloseButton($btnOrg, icon, className, onChange) { - if (!$btnOrg) return null; - const $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; + const $button = $container.querySelector("button"); + if (!$button) return null; + $button.setAttribute("title", label); + const $orgSvg = $button.querySelector("svg"); + if (!$orgSvg) return null; + const $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; + const $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() { + const $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) { + const $orgButton = $streamHud.querySelector("div[class^=HUDButton]"); + if (!$orgButton) return; + const hideGripHandle = () => { + const $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(); + }; + let $btnStreamSettings = StreamUiHandler.$btnStreamSettings; + if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { + hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); + }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; + const streamStats = StreamStats.getInstance(); + let $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(); + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn); + }), StreamUiHandler.$btnStreamStats = $btnStreamStats; + const $btnParent = $orgButton.parentElement; + if ($btnStreamSettings && $btnStreamStats) { + const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); + $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); } - static async handleStreamMenu() { - const $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) { - const $orgButton = $streamHud.querySelector("div[class^=HUDButton]"); - if (!$orgButton) return; - const hideGripHandle = () => { - const $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(); - }; - let $btnStreamSettings = StreamUiHandler.$btnStreamSettings; - if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => { - hideGripHandle(), e.preventDefault(), SettingsNavigationDialog.getInstance().show(); - }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings; - const streamStats = StreamStats.getInstance(); - let $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(); - const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); - $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn); - }), StreamUiHandler.$btnStreamStats = $btnStreamStats; - const $btnParent = $orgButton.parentElement; - if ($btnStreamSettings && $btnStreamStats) { - const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing(); - $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats); - } - const $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(); - const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); - if (!$screen) return; - const observer = new MutationObserver((mutationList) => { - mutationList.forEach((item2) => { - if (item2.type !== "childList") return; - item2.addedNodes.forEach(async ($node) => { - if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; - let $elm = $node; - if (!($elm instanceof HTMLElement)) return; - const className = $elm.className || ""; - if (className.includes("PureErrorPage")) { - BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); - 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); - }); - }); + const $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(); + const $screen = document.querySelector("#PageContent section[class*=PureScreens]"); + if (!$screen) return; + const observer = new MutationObserver((mutationList) => { + mutationList.forEach((item2) => { + if (item2.type !== "childList") return; + item2.addedNodes.forEach(async ($node) => { + if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return; + let $elm = $node; + if (!($elm instanceof HTMLElement)) return; + const className = $elm.className || ""; + if (className.includes("PureErrorPage")) { + BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE); + 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; - } + }); + }); + observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer; + } } class XboxApi { - static CACHED_TITLES = {}; - static async getProductTitle(xboxTitleId) { - if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; - try { - const 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 null; - } + static CACHED_TITLES = {}; + static async getProductTitle(xboxTitleId) { + if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId]; + try { + const 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 null; + } } function unload() { - if (!STATES.isPlaying) return; - EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(), MouseCursorHider.stop(), TouchController.reset(), GameBar.getInstance().disable(); + if (!STATES.isPlaying) return; + EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().onStoppedPlaying(), MouseCursorHider.stop(), TouchController.reset(), GameBar.getInstance().disable(); } function observeRootDialog($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) { - const $addedElm = mutation.addedNodes[0]; - if ($addedElm instanceof HTMLElement && $addedElm.className) { - if ($root.querySelector("div[class*=GuideDialog]")) GuideMenu.observe($addedElm); - } - } - const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); - if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); + 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) { + const $addedElm = mutation.addedNodes[0]; + if ($addedElm instanceof HTMLElement && $addedElm.className) { + if ($root.querySelector("div[class*=GuideDialog]")) GuideMenu.observe($addedElm); } - }).observe($root, { subtree: !0, childList: !0 }); + } + const shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0); + if (shown !== beingShown) beingShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED); + } + }).observe($root, { subtree: !0, childList: !0 }); } function waitForRootDialog() { - const observer = new MutationObserver((mutationList) => { - for (let mutation of mutationList) { - if (mutation.type !== "childList") continue; - const $target = mutation.target; - if ($target.id && $target.id === "gamepass-dialog-root") { - observer.disconnect(), observeRootDialog($target); - break; - } - } - }); - observer.observe(document.documentElement, { subtree: !0, childList: !0 }); + const observer = new MutationObserver((mutationList) => { + for (let mutation of mutationList) { + if (mutation.type !== "childList") continue; + const $target = mutation.target; + if ($target.id && $target.id === "gamepass-dialog-root") { + observer.disconnect(), observeRootDialog($target); + break; + } + } + }); + observer.observe(document.documentElement, { subtree: !0, childList: !0 }); } function main() { - if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); - if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); - if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("game_bar_position") !== "off" && GameBar.getInstance(), Screenshot.setup(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect(); - if (getPref("stream_touch_controller") === "all") TouchController.setup(); - if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); - if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); + if (getPref("game_msfs2020_force_native_mkb")) BX_FLAGS.ForceNativeMkbTitles.push("9PMQDM08SNK9"); + if (patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref("audio_enable_volume_control") && patchAudioContext(), getPref("block_tracking")) patchMeControl(), disableAdobeAudienceManager(); + if (waitForRootDialog(), addCss(), Toast.setup(), GuideMenu.addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("game_bar_position") !== "off" && GameBar.getInstance(), Screenshot.setup(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getPref("xhome_enabled")) RemotePlayManager.detect(); + if (getPref("stream_touch_controller") === "all") TouchController.setup(); + if (getPref("mkb_enabled") && AppInterface) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString()); + if (getPref("ui_game_card_show_wait_time") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getPref("controller_show_connection_status")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)); } if (window.location.pathname.includes("/auth/msa")) { - const nativePushState = window.history.pushState; - throw window.history.pushState = function(...args) { - const 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"); + const nativePushState = window.history.pushState; + throw window.history.pushState = function(...args) { + const 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}'; - const isSafari = UserAgent.isSafari(); - let $secondaryAction; - if (isSafari) $secondaryAction = CE("p", {}, t("settings-reloading")); - else $secondaryAction = CE("a", { - href: "https://better-xcloud.github.io/troubleshooting", - target: "_blank" - }, "🤓 " + t("how-to-fix")); - const $fragment = document.createDocumentFragment(); - throw $fragment.appendChild(CE("style", {}, css)), $fragment.appendChild(CE("div", { - class: "bx-reload-overlay" - }, CE("div", {}, CE("p", {}, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari"); + 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}'; + const isSafari = UserAgent.isSafari(); + let $secondaryAction; + if (isSafari) $secondaryAction = CE("p", {}, t("settings-reloading")); + else $secondaryAction = CE("a", { + href: "https://better-xcloud.github.io/troubleshooting", + target: "_blank" + }, "🤓 " + t("how-to-fix")); + const $fragment = document.createDocumentFragment(); + throw $fragment.appendChild(CE("style", {}, css)), $fragment.appendChild(CE("div", { + class: "bx-reload-overlay" + }, CE("div", {}, CE("p", {}, 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); + 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) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); - else window.setTimeout(HeaderSection.watchHeader, 2000); - if (getPref("ui_hide_sections").includes("friends")) { - const $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]"); - $parent && ($parent.style.display = "none"); - } - preloadFonts(); + if (document.readyState !== "interactive") return; + if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) getPref("xhome_enabled") && RemotePlayManager.getInstance().initialize(); + else window.setTimeout(HeaderSection.watchHeader, 2000); + if (getPref("ui_hide_sections").includes("friends")) { + const $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); @@ -7919,57 +7863,57 @@ window.addEventListener("popstate", onHistoryChanged); window.history.pushState = patchHistoryMethod("pushState"); window.history.replaceState = patchHistoryMethod("replaceState"); window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, (e) => { - if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); + if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsNavigationDialog.getInstance().show(); }, { once: !0 }); window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => { - STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); + STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000); }); window.addEventListener(BxEvent.STREAM_LOADING, (e) => { - if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); - else STATES.currentStream.titleSlug = "remote-play"; + if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title); + else STATES.currentStream.titleSlug = "remote-play"; }); getPref("ui_loading_screen_game_art") && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup); window.addEventListener(BxEvent.STREAM_STARTING, (e) => { - if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); + if (LoadingScreen.hide(), !getPref("mkb_enabled") && getPref("mkb_hide_idle_cursor")) MouseCursorHider.start(), MouseCursorHider.hide(); }); window.addEventListener(BxEvent.STREAM_PLAYING, (e) => { - if (STATES.isPlaying = !0, StreamUiHandler.observe(), getPref("game_bar_position") !== "off") { - const gameBar = GameBar.getInstance(); - gameBar.reset(), gameBar.enable(), gameBar.showBar(); - } - { - const $video = e.$video; - Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight); - } - updateVideoPlayer(); + if (STATES.isPlaying = !0, StreamUiHandler.observe(), getPref("game_bar_position") !== "off") { + const gameBar = GameBar.getInstance(); + gameBar.reset(), gameBar.enable(), gameBar.showBar(); + } + { + const $video = e.$video; + Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight); + } + updateVideoPlayer(); }); window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => { - BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => { - if (e.component === "product-details") ProductDetailsPage.injectButtons(); + if (e.component === "product-details") ProductDetailsPage.injectButtons(); }); window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => { - const dataChannel = e.dataChannel; - if (!dataChannel || dataChannel.label !== "message") return; - dataChannel.addEventListener("message", async (msg) => { - if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; - if (msg.data.includes("/titleinfo")) { - const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); - if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.remotePlay.isPlaying) { - if (STATES.currentStream.titleSlug = "remote-play", json.focused) { - const productTitle = await XboxApi.getProductTitle(xboxTitleId); - if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle); - } - } + const dataChannel = e.dataChannel; + if (!dataChannel || dataChannel.label !== "message") return; + dataChannel.addEventListener("message", async (msg) => { + if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return; + if (msg.data.includes("/titleinfo")) { + const json = JSON.parse(JSON.parse(msg.data).content), xboxTitleId = parseInt(json.titleid, 16); + if (STATES.currentStream.xboxTitleId = xboxTitleId, STATES.remotePlay.isPlaying) { + if (STATES.currentStream.titleSlug = "remote-play", json.focused) { + const productTitle = await XboxApi.getProductTitle(xboxTitleId); + if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle); } - }); + } + } + }); }); window.addEventListener(BxEvent.STREAM_STOPPED, unload); window.addEventListener("pagehide", (e) => { - BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); + BxEvent.dispatch(window, BxEvent.STREAM_STOPPED); }); window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => { - Screenshot.takeScreenshot(); + Screenshot.takeScreenshot(); }); main();