From d638700e03fd772ae544a9b3e5f66e9933f387b0 Mon Sep 17 00:00:00 2001
From: redphx <96280+redphx@users.noreply.github.com>
Date: Tue, 28 Jan 2025 11:29:42 +0700
Subject: [PATCH] Update dists
---
dist/better-xcloud.lite.user.js | 12797 ++++++++++----------
dist/better-xcloud.user.js | 19433 +++++++++++++++---------------
2 files changed, 16297 insertions(+), 15933 deletions(-)
diff --git a/dist/better-xcloud.lite.user.js b/dist/better-xcloud.lite.user.js
index aafbd9e..b0e1d25 100755
--- 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 6.2.2
+// @version 6.3.0-beta-1
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@@ -12,5845 +12,6212 @@
// ==/UserScript==
"use strict";
class BxLogger {
- static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args);
- static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args);
- static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);
- static log(color, tag, ...args) {
- console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
- }
+static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args);
+static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args);
+static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);
+static log(color, tag, ...args) {
+console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
+}
}
window.BxLogger = BxLogger;
/* ADDITIONAL CODE */
var DEFAULT_FLAGS = {
- Debug: !1,
- CheckForUpdate: !0,
- EnableXcloudLogging: !1,
- SafariWorkaround: !0,
- ForceNativeMkbTitles: [],
- FeatureGates: null,
- DeviceInfo: {
- deviceType: "unknown"
- }
+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 ALL_PREFS = {
+global: [
+"audio.mic.onPlaying",
+"audio.volume.booster.enabled",
+"block.features",
+"block.tracking",
+"gameBar.position",
+"game.fortnite.forceConsole",
+"loadingScreen.gameArt.show",
+"loadingScreen.rocket",
+"loadingScreen.waitTime.show",
+"mkb.enabled",
+"mkb.cursor.hideIdle",
+"nativeMkb.forcedGames",
+"nativeMkb.mode",
+"xhome.enabled",
+"xhome.video.resolution",
+"screenshot.applyFilters",
+"server.bypassRestriction",
+"server.ipv6.prefer",
+"server.region",
+"stream.video.codecProfile",
+"stream.video.combineAudio",
+"stream.video.maxBitrate",
+"stream.locale",
+"stream.video.resolution",
+"touchController.autoOff",
+"touchController.opacity.default",
+"touchController.mode",
+"touchController.style.custom",
+"touchController.style.standard",
+"ui.controllerFriendly",
+"ui.controllerStatus.show",
+"ui.feedbackDialog.disabled",
+"ui.gameCard.waitTime.show",
+"ui.hideSections",
+"ui.systemMenu.hideHandle",
+"ui.imageQuality",
+"ui.layout",
+"ui.reduceAnimations",
+"ui.hideScrollbar",
+"ui.streamMenu.simplify",
+"ui.splashVideo.skip",
+"version.current",
+"version.lastCheck",
+"version.latest",
+"bx.locale",
+"userAgent.profile"
+],
+stream: [
+"audio.volume",
+"controller.pollingRate",
+"controller.settings",
+"deviceVibration.intensity",
+"deviceVibration.mode",
+"keyboardShortcuts.preset.inGameId",
+"localCoOp.enabled",
+"mkb.p1.preset.mappingId",
+"mkb.p1.slot",
+"mkb.p2.preset.mappingId",
+"mkb.p2.slot",
+"nativeMkb.scroll.sensitivityX",
+"nativeMkb.scroll.sensitivityY",
+"stats.colors",
+"stats.items",
+"stats.opacity.all",
+"stats.opacity.background",
+"stats.position",
+"stats.quickGlance.enabled",
+"stats.showWhenPlaying",
+"stats.textSize",
+"video.brightness",
+"video.contrast",
+"video.maxFps",
+"video.player.type",
+"video.position",
+"video.player.powerPreference",
+"video.processing",
+"video.ratio",
+"video.saturation",
+"video.processing.sharpness"
+]
+};
var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0";
if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {
- let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
- if (match) CHROMIUM_VERSION = match[1];
+let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
+if (match) CHROMIUM_VERSION = match[1];
}
class UserAgent {
- static STORAGE_KEY = "BetterXcloud.UserAgent";
- static #config;
- static #isMobile = null;
- static #isSafari = null;
- static #isSafariMobile = null;
- static #USER_AGENTS = {
- "windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
- "macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
- "smarttv-generic": `${window.navigator.userAgent} Smart-TV`,
- "smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
- "vr-oculus": window.navigator.userAgent + " OculusBrowser VR"
- };
- static init() {
- if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";
- if (!UserAgent.#config.custom) UserAgent.#config.custom = "";
- UserAgent.spoof();
- }
- static updateStorage(profile, custom) {
- let config = UserAgent.#config;
- if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;
- window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));
- }
- static getDefault() {
- return window.navigator.orgUserAgent || window.navigator.userAgent;
- }
- static get(profile) {
- let defaultUserAgent = window.navigator.userAgent;
- switch (profile) {
- case "default":
- return defaultUserAgent;
- case "custom":
- return UserAgent.#config.custom || defaultUserAgent;
- default:
- return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
- }
- }
- static isSafari() {
- if (this.#isSafari !== null) return this.#isSafari;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");
- return this.#isSafari = result, result;
- }
- static isSafariMobile() {
- if (this.#isSafariMobile !== null) return this.#isSafariMobile;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");
- return this.#isSafariMobile = result, result;
- }
- static isMobile() {
- if (this.#isMobile !== null) return this.#isMobile;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);
- return this.#isMobile = result, result;
- }
- static spoof() {
- let profile = UserAgent.#config.profile;
- if (profile === "default") return;
- let newUserAgent = UserAgent.get(profile);
- if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});
- window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {
- value: newUserAgent
- });
- }
+static STORAGE_KEY = "BetterXcloud.UserAgent";
+static #config;
+static #isMobile = null;
+static #isSafari = null;
+static #isSafariMobile = null;
+static #USER_AGENTS = {
+"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
+"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
+"smarttv-generic": `${window.navigator.userAgent} Smart-TV`,
+"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
+"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"
+};
+static init() {
+if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";
+if (!UserAgent.#config.custom) UserAgent.#config.custom = "";
+UserAgent.spoof();
}
-var SCRIPT_VERSION = "6.2.2", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
+static updateStorage(profile, custom) {
+let config = UserAgent.#config;
+if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;
+window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));
+}
+static getDefault() {
+return window.navigator.orgUserAgent || window.navigator.userAgent;
+}
+static get(profile) {
+let defaultUserAgent = window.navigator.userAgent;
+switch (profile) {
+case "default":
+return defaultUserAgent;
+case "custom":
+return UserAgent.#config.custom || defaultUserAgent;
+default:
+return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
+}
+}
+static isSafari() {
+if (this.#isSafari !== null) return this.#isSafari;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");
+return this.#isSafari = result, result;
+}
+static isSafariMobile() {
+if (this.#isSafariMobile !== null) return this.#isSafariMobile;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");
+return this.#isSafariMobile = result, result;
+}
+static isMobile() {
+if (this.#isMobile !== null) return this.#isMobile;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);
+return this.#isMobile = result, result;
+}
+static spoof() {
+let profile = UserAgent.#config.profile;
+if (profile === "default") return;
+let newUserAgent = UserAgent.get(profile);
+if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});
+window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {
+value: newUserAgent
+});
+}
+}
+var SCRIPT_VERSION = "6.3.0-beta-1", SCRIPT_VARIANT = "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, STATES = {
- supportedRegion: !0,
- serverRegions: {},
- selectedRegion: {},
- gsToken: "",
- isSignedIn: !1,
- isPlaying: !1,
- browser: {
- capabilities: {
- touch: browserHasTouchSupport,
- batteryApi: "getBattery" in window.navigator,
- deviceVibration: !!window.navigator.vibrate,
- mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),
- emulatedNativeMkb: !!AppInterface
- }
- },
- userAgent: {
- isTv,
- capabilities: {
- touch: userAgentHasTouchSupport,
- mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)
- }
- },
- currentStream: {},
- remotePlay: {},
- pointerServerPort: 9269
-}, STORAGE = {};
+supportedRegion: !0,
+serverRegions: {},
+selectedRegion: {},
+gsToken: "",
+isSignedIn: !1,
+isPlaying: !1,
+browser: {
+capabilities: {
+touch: browserHasTouchSupport,
+batteryApi: "getBattery" in window.navigator,
+deviceVibration: !!window.navigator.vibrate,
+mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),
+emulatedNativeMkb: !!AppInterface
+}
+},
+userAgent: {
+isTv,
+capabilities: {
+touch: userAgentHasTouchSupport,
+mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)
+}
+},
+currentStream: {},
+remotePlay: {},
+pointerServerPort: 9269
+};
function deepClone(obj) {
- if (!obj) return {};
- if ("structuredClone" in window) return structuredClone(obj);
- return JSON.parse(JSON.stringify(obj));
+if (!obj) return {};
+if ("structuredClone" in window) return structuredClone(obj);
+return JSON.parse(JSON.stringify(obj));
}
var BxEvent;
((BxEvent) => {
- BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready";
- function dispatch(target, eventName, data) {
- if (!target) return;
- if (!eventName) {
- alert("BxEvent.dispatch(): eventName is null");
- return;
- }
- let event = new Event(eventName);
- if (data) for (let key in data)
- event[key] = data[key];
- target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data);
- }
- BxEvent.dispatch = dispatch;
+BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready";
+function dispatch(target, eventName, data) {
+if (!target) return;
+if (!eventName) {
+alert("BxEvent.dispatch(): eventName is null");
+return;
+}
+let event = new Event(eventName);
+if (data) for (let key in data)
+event[key] = data[key];
+target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data);
+}
+BxEvent.dispatch = dispatch;
})(BxEvent ||= {});
window.BxEvent = BxEvent;
class BxEventBus {
- listeners = new Map;
- group;
- appJsInterfaces;
- static Script = new BxEventBus("script", {
- "dialog.shown": "onDialogShown",
- "dialog.dismissed": "onDialogDismissed"
- });
- static Stream = new BxEventBus("stream", {
- "state.loading": "onStreamPlaying",
- "state.playing": "onStreamPlaying",
- "state.stopped": "onStreamStopped"
- });
- constructor(group, appJsInterfaces) {
- this.group = group, this.appJsInterfaces = appJsInterfaces;
- }
- on(event, callback) {
- if (!this.listeners.has(event)) this.listeners.set(event, new Set);
- this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);
- }
- once(event, callback) {
- let wrapper = (...args) => {
- callback(...args), this.off(event, wrapper);
- };
- this.on(event, wrapper);
- }
- off(event, callback) {
- if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {
- this.listeners.delete(event);
- return;
- }
- let callbacks = this.listeners.get(event);
- if (!callbacks) return;
- if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);
- }
- offAll() {
- this.listeners.clear();
- }
- emit(event, payload) {
- let callbacks = this.listeners.get(event) || [];
- for (let callback of callbacks)
- callback(payload);
- if (AppInterface) try {
- if (event in this.appJsInterfaces) {
- let method = this.appJsInterfaces[event];
- AppInterface[method] && AppInterface[method]();
- } else AppInterface.onEventBus(this.group + "." + event);
- } catch (e) {
- console.log(e);
- }
- BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", event, payload);
- }
+listeners = new Map;
+group;
+appJsInterfaces;
+static Script = new BxEventBus("script", {
+"dialog.shown": "onDialogShown",
+"dialog.dismissed": "onDialogDismissed"
+});
+static Stream = new BxEventBus("stream", {
+"state.loading": "onStreamPlaying",
+"state.playing": "onStreamPlaying",
+"state.stopped": "onStreamStopped"
+});
+constructor(group, appJsInterfaces) {
+this.group = group, this.appJsInterfaces = appJsInterfaces;
+}
+on(event, callback) {
+if (!this.listeners.has(event)) this.listeners.set(event, new Set);
+this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);
+}
+once(event, callback) {
+let wrapper = (...args) => {
+callback(...args), this.off(event, wrapper);
+};
+this.on(event, wrapper);
+}
+off(event, callback) {
+if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {
+this.listeners.delete(event);
+return;
+}
+let callbacks = this.listeners.get(event);
+if (!callbacks) return;
+if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);
+}
+offAll() {
+this.listeners.clear();
+}
+emit(event, payload) {
+let callbacks = this.listeners.get(event) || [];
+for (let callback of callbacks)
+callback(payload);
+if (AppInterface) try {
+if (event in this.appJsInterfaces) {
+let method = this.appJsInterfaces[event];
+AppInterface[method] && AppInterface[method]();
+} else AppInterface.onEventBus(this.group + "." + event);
+} catch (e) {
+console.log(e);
+}
+BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);
+}
}
window.BxEventBus = BxEventBus;
class GhPagesUtils {
- static fetchLatestCommit() {
- NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {
- method: "GET",
- headers: {
- Accept: "application/vnd.github.v3+json"
- }
- }).then((response) => response.json()).then((data) => {
- let latestCommitHash = data.commit.sha;
- window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);
- }).catch((error) => {
- BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);
- });
- }
- static getUrl(path) {
- if (path[0] === "/") alert('`path` must not starts with "/"');
- let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");
- if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;
- else return `${prefix}/refs/heads/gh-pages/${path}`;
- }
- static getNativeMkbCustomList(update = !1) {
- let key = "BetterXcloud.GhPages.ForceNativeMkb";
- update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {
- if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {
- data: json
- });
- else window.localStorage.removeItem(key);
- });
- let info = JSON.parse(window.localStorage.getItem(key) || "{}");
- if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};
- return info.data;
- }
- static getTouchControlCustomList() {
- let key = "BetterXcloud.GhPages.CustomTouchLayouts";
- return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {
- if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));
- }), JSON.parse(window.localStorage.getItem(key) || "[]");
- }
- static getLocalCoOpList() {
- let key = "BetterXcloud.GhPages.LocalCoOp";
- NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {
- if (json.$schemaVersion === 1) {
- window.localStorage.setItem(key, JSON.stringify(json));
- let ids = new Set(Object.keys(json.data));
- BxEventBus.Script.emit("list.localCoOp.updated", { ids });
- } else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });
- });
- let info = JSON.parse(window.localStorage.getItem(key) || "{}");
- if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;
- return new Set(Object.keys(info.data || {}));
- }
+static fetchLatestCommit() {
+NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {
+method: "GET",
+headers: {
+Accept: "application/vnd.github.v3+json"
+}
+}).then((response) => response.json()).then((data) => {
+let latestCommitHash = data.commit.sha;
+window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);
+}).catch((error) => {
+BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);
+});
+}
+static getUrl(path) {
+if (path[0] === "/") alert('`path` must not starts with "/"');
+let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");
+if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;
+else return `${prefix}/refs/heads/gh-pages/${path}`;
+}
+static getNativeMkbCustomList(update = !1) {
+let key = "BetterXcloud.GhPages.ForceNativeMkb";
+update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {
+if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {
+data: json
+});
+else window.localStorage.removeItem(key);
+});
+let info = JSON.parse(window.localStorage.getItem(key) || "{}");
+if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};
+return info.data;
+}
+static getTouchControlCustomList() {
+let key = "BetterXcloud.GhPages.CustomTouchLayouts";
+return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {
+if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));
+}), JSON.parse(window.localStorage.getItem(key) || "[]");
+}
+static getLocalCoOpList() {
+let key = "BetterXcloud.GhPages.LocalCoOp";
+NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {
+if (json.$schemaVersion === 1) {
+window.localStorage.setItem(key, JSON.stringify(json));
+let ids = new Set(Object.keys(json.data));
+BxEventBus.Script.emit("list.localCoOp.updated", { ids });
+} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });
+});
+let info = JSON.parse(window.localStorage.getItem(key) || "{}");
+if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;
+return new Set(Object.keys(info.data || {}));
+}
}
var SUPPORTED_LANGUAGES = {
- "en-US": "English (US)",
- "ca-CA": "Català",
- "da-DK": "dansk",
- "de-DE": "Deutsch",
- "en-ID": "Bahasa Indonesia",
- "es-ES": "español (España)",
- "fr-FR": "français",
- "it-IT": "italiano",
- "ja-JP": "日本語",
- "ko-KR": "한국어",
- "pl-PL": "polski",
- "pt-BR": "português (Brasil)",
- "ru-RU": "русский",
- "th-TH": "ภาษาไทย",
- "tr-TR": "Türkçe",
- "uk-UA": "українська",
- "vi-VN": "Tiếng Việt",
- "zh-CN": "中文(简体)",
- "zh-TW": "中文(繁體)"
+"en-US": "English (US)",
+"ca-CA": "Català",
+"da-DK": "dansk",
+"de-DE": "Deutsch",
+"en-ID": "Bahasa Indonesia",
+"es-ES": "español (España)",
+"fr-FR": "français",
+"it-IT": "italiano",
+"ja-JP": "日本語",
+"ko-KR": "한국어",
+"pl-PL": "polski",
+"pt-BR": "português (Brasil)",
+"ru-RU": "русский",
+"th-TH": "ภาษาไทย",
+"tr-TR": "Türkçe",
+"uk-UA": "українська",
+"vi-VN": "Tiếng Việt",
+"zh-CN": "中文(简体)",
+"zh-TW": "中文(繁體)"
}, Texts = {
- achievements: "Achievements",
- activate: "Activate",
- activated: "Activated",
- active: "Active",
- advanced: "Advanced",
- "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)?",
- "background-opacity": "Background opacity",
- battery: "Battery",
- "battery-saving": "Battery saving",
- "better-xcloud": "Better xCloud",
- "bitrate-audio-maximum": "Maximum audio bitrate",
- "bitrate-video-maximum": "Maximum video bitrate",
- bottom: "Bottom",
- "bottom-half": "Bottom half",
- "bottom-left": "Bottom-left",
- "bottom-right": "Bottom-right",
- brazil: "Brazil",
- brightness: "Brightness",
- "browser-unsupported-feature": "Your browser doesn't support this feature",
- "button-xbox": "Xbox button",
- "bypass-region-restriction": "Bypass region restriction",
- "can-stream-xbox-360-games": "Can stream Xbox 360 games",
- cancel: "Cancel",
- "cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
- center: "Center",
- chat: "Chat",
- "clarity-boost": "Clarity boost",
- "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
- clear: "Clear",
- "clear-data": "Clear data",
- "clear-data-confirm": "Do you want to clear all Better xCloud settings and data?",
- "clear-data-success": "Data cleared! Refresh the page to apply the changes.",
- clock: "Clock",
- close: "Close",
- "close-app": "Close app",
- "combine-audio-video-streams": "Combine audio & video streams",
- "combine-audio-video-streams-summary": "May fix the laggy audio problem",
- "conditional-formatting": "Conditional formatting text color",
- "confirm-delete-preset": "Do you want to delete this preset?",
- "confirm-reload-stream": "Do you want to refresh the stream?",
- connected: "Connected",
- "console-connect": "Connect",
- "continent-asia": "Asia",
- "continent-australia": "Australia",
- "continent-europe": "Europe",
- "continent-north-america": "North America",
- "continent-south-america": "South America",
- contrast: "Contrast",
- controller: "Controller",
- "controller-customization": "Controller customization",
- "controller-customization-input-latency-note": "May slightly increase input latency",
- "controller-friendly-ui": "Controller-friendly UI",
- "controller-shortcuts": "Controller shortcuts",
- "controller-shortcuts-connect-note": "Connect a controller to use this feature",
- "controller-shortcuts-xbox-note": "Button to open the Guide menu",
- "controller-vibration": "Controller vibration",
- copy: "Copy",
- "create-shortcut": "Shortcut",
- custom: "Custom",
- "deadzone-counterweight": "Deadzone counterweight",
- decrease: "Decrease",
- default: "Default",
- "default-preset-note": "You can't modify default presets. Create a new one to customize it.",
- delete: "Delete",
- "detect-controller-button": "Detect controller button",
- device: "Device",
- "device-unsupported-touch": "Your device doesn't have touch support",
- "device-vibration": "Device vibration",
- "device-vibration-not-using-gamepad": "On when not using gamepad",
- disable: "Disable",
- "disable-features": "Disable features",
- "disable-home-context-menu": "Disable context menu in Home page",
- "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
- "disable-social-features": "Disable social features",
- "disable-xcloud-analytics": "Disable xCloud analytics",
- disabled: "Disabled",
- disconnected: "Disconnected",
- download: "Download",
- downloaded: "Downloaded",
- edit: "Edit",
- "enable-controller-shortcuts": "Enable controller shortcuts",
- "enable-local-co-op-support": "Enable local co-op support",
- "enable-local-co-op-support-note": "Only works with some games",
- "enable-mic-on-startup": "Enable microphone on game launch",
- "enable-mkb": "Emulate controller with Mouse & Keyboard",
- "enable-quick-glance-mode": 'Enable "Quick Glance" mode',
- "enable-remote-play-feature": 'Enable the "Remote Play" feature',
- "enable-volume-control": "Enable volume control feature",
- enabled: "Enabled",
- experimental: "Experimental",
- export: "Export",
- fast: "Fast",
- "force-native-mkb-games": "Force native Mouse & Keyboard for these games",
- "fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',
- "fortnite-force-console-version": "Fortnite: force console version",
- "friends-followers": "Friends and followers",
- "game-bar": "Game Bar",
- "getting-consoles-list": "Getting the list of consoles...",
- guide: "Guide",
- help: "Help",
- hide: "Hide",
- "hide-idle-cursor": "Hide mouse cursor on idle",
- "hide-scrollbar": "Hide web page's scrollbar",
- "hide-sections": "Hide sections",
- "hide-system-menu-icon": "Hide System menu's icon",
- "hide-touch-controller": "Hide touch controller",
- "high-performance": "High performance",
- "highest-quality": "Highest quality",
- "highest-quality-note": "Your device may not be powerful enough to use these settings",
- "horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
- "horizontal-sensitivity": "Horizontal sensitivity",
- "how-to-fix": "How to fix",
- "how-to-improve-app-performance": "How to improve app's performance",
- ignore: "Ignore",
- "image-quality": "Website's image quality",
- import: "Import",
- "in-game-controller-customization": "In-game controller customization",
- "in-game-controller-shortcuts": "In-game controller shortcuts",
- "in-game-keyboard-shortcuts": "In-game keyboard shortcuts",
- "in-game-shortcuts": "In-game shortcuts",
- increase: "Increase",
- "install-android": "Better xCloud app for Android",
- invites: "Invites",
- japan: "Japan",
- jitter: "Jitter",
- "keyboard-key": "Keyboard key",
- "keyboard-shortcuts": "Keyboard shortcuts",
- korea: "Korea",
- language: "Language",
- large: "Large",
- layout: "Layout",
- "left-stick": "Left stick",
- "left-stick-deadzone": "Left stick deadzone",
- "left-trigger-range": "Left trigger range",
- "limit-fps": "Limit FPS",
- "load-failed-message": "Failed to run Better xCloud",
- "loading-screen": "Loading screen",
- "local-co-op": "Local co-op",
- "lowest-quality": "Lowest quality",
- manage: "Manage",
- "map-mouse-to": "Map mouse to",
- "may-not-work-properly": "May not work properly!",
- menu: "Menu",
- microphone: "Microphone",
- "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
- "mkb-click-to-activate": "Click to activate",
- "mkb-disclaimer": "This could be viewed as cheating when playing online",
- "modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.",
- "mouse-and-keyboard": "Mouse & Keyboard",
- "mouse-click": "Mouse click",
- "mouse-wheel": "Mouse wheel",
- muted: "Muted",
- name: "Name",
- "native-mkb": "Native Mouse & Keyboard",
- new: "New",
- "new-version-available": [
- e => `Version ${e.version} available`,
- e => `Versió ${e.version} disponible`,
- ,
- e => `Version ${e.version} verfügbar`,
- e => `Versi ${e.version} tersedia`,
- e => `Versión ${e.version} disponible`,
- e => `Version ${e.version} disponible`,
- e => `Disponibile la versione ${e.version}`,
- e => `Ver ${e.version} が利用可能です`,
- e => `${e.version} 버전 사용가능`,
- e => `Dostępna jest nowa wersja ${e.version}`,
- e => `Versão ${e.version} disponível`,
- e => `Версия ${e.version} доступна`,
- e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
- e => `${e.version} sayılı yeni sürüm mevcut`,
- e => `Доступна версія ${e.version}`,
- e => `Đã có phiên bản ${e.version}`,
- e => `版本 ${e.version} 可供更新`,
- e => `已可更新為 ${e.version} 版`
- ],
- "no-consoles-found": "No consoles found",
- "no-controllers-connected": "No controllers connected",
- normal: "Normal",
- notifications: "Notifications",
- off: "Off",
- official: "Official",
- on: "On",
- "only-supports-some-games": "Only supports some games",
- opacity: "Opacity",
- other: "Other",
- playing: "Playing",
- playtime: "Playtime",
- poland: "Poland",
- "polling-rate": "Polling rate",
- position: "Position",
- "powered-off": "Powered off",
- "powered-on": "Powered on",
- "prefer-ipv6-server": "Prefer IPv6 server",
- "preferred-game-language": "Preferred game's language",
- preset: "Preset",
- press: "Press",
- "press-any-button": "Press any button...",
- "press-esc-to-cancel": "Press Esc to cancel",
- "press-key-to-toggle-mkb": [
- e => `Press ${e.key} to toggle this feature`,
- e => `Premeu ${e.key} per alternar aquesta funció`,
- e => `Tryk på ${e.key} for at slå denne funktion til`,
- e => `${e.key}: Funktion an-/ausschalten`,
- e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,
- e => `Pulsa ${e.key} para alternar esta función`,
- e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
- e => `Premi ${e.key} per attivare questa funzionalità`,
- e => `${e.key} でこの機能を切替`,
- e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
- e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
- e => `Pressione ${e.key} para alternar este recurso`,
- e => `Нажмите ${e.key} для переключения этой функции`,
- e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,
- e => `Etkinleştirmek için ${e.key} tuşuna basın`,
- e => `Натисніть ${e.key} щоб перемкнути цю функцію`,
- e => `Nhấn ${e.key} để bật/tắt tính năng này`,
- e => `按下 ${e.key} 来切换此功能`,
- e => `按下 ${e.key} 來啟用此功能`
- ],
- "press-to-bind": "Press a key or do a mouse click to bind...",
- "prompt-preset-name": "Preset's name:",
- recommended: "Recommended",
- "recommended-settings-for-device": [
- e => `Recommended settings for ${e.device}`,
- e => `Configuració recomanada per a ${e.device}`,
- ,
- e => `Empfohlene Einstellungen für ${e.device}`,
- e => `Rekomendasi pengaturan untuk ${e.device}`,
- e => `Ajustes recomendados para ${e.device}`,
- e => `Paramètres recommandés pour ${e.device}`,
- e => `Configurazioni consigliate per ${e.device}`,
- e => `${e.device} の推奨設定`,
- e => `다음 기기에서 권장되는 설정: ${e.device}`,
- e => `Zalecane ustawienia dla ${e.device}`,
- e => `Configurações recomendadas para ${e.device}`,
- e => `Рекомендуемые настройки для ${e.device}`,
- e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,
- e => `${e.device} için önerilen ayarlar`,
- e => `Рекомендовані налаштування для ${e.device}`,
- e => `Cấu hình được đề xuất cho ${e.device}`,
- e => `${e.device} 的推荐设置`,
- e => `${e.device} 推薦的設定`
- ],
- "reduce-animations": "Reduce UI animations",
- region: "Region",
- "reload-page": "Reload page",
- "remote-play": "Remote Play",
- rename: "Rename",
- renderer: "Renderer",
- "renderer-configuration": "Renderer configuration",
- "right-click-to-unbind": "Right-click on a key to unbind it",
- "right-stick": "Right stick",
- "right-stick-deadzone": "Right stick deadzone",
- "right-trigger-range": "Right trigger range",
- "rocket-always-hide": "Always hide",
- "rocket-always-show": "Always show",
- "rocket-animation": "Rocket animation",
- "rocket-hide-queue": "Hide when queuing",
- saturation: "Saturation",
- save: "Save",
- screen: "Screen",
- "screenshot-apply-filters": "Apply video filters to screenshots",
- "section-all-games": "All games",
- "section-most-popular": "Most popular",
- "section-native-mkb": "Play with mouse & keyboard",
- "section-news": "News",
- "section-play-with-friends": "Play with friends",
- "section-touch": "Play with touch",
- "separate-touch-controller": "Separate Touch controller & Controller #1",
- "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
- server: "Server",
- "server-locations": "Server locations",
- settings: "Settings",
- "settings-reload": "Reload page to reflect changes",
- "settings-reload-note": "Settings in this tab only go into effect on the next page load",
- "settings-reloading": "Reloading...",
- sharpness: "Sharpness",
- "shortcut-keys": "Shortcut keys",
- show: "Show",
- "show-controller-connection-status": "Show controller connection status",
- "show-game-art": "Show game art",
- "show-hide": "Show/hide",
- "show-stats-on-startup": "Show stats when starting the game",
- "show-touch-controller": "Show touch controller",
- "show-wait-time": "Show the estimated wait time",
- "show-wait-time-in-game-card": "Show wait time in game card",
- "simplify-stream-menu": "Simplify Stream's menu",
- "skip-splash-video": "Skip Xbox splash video",
- slow: "Slow",
- small: "Small",
- "smart-tv": "Smart TV",
- sound: "Sound",
- standard: "Standard",
- standby: "Standby",
- "stat-bitrate": "Bitrate",
- "stat-decode-time": "Decode time",
- "stat-fps": "FPS",
- "stat-frames-lost": "Frames lost",
- "stat-packets-lost": "Packets lost",
- "stat-ping": "Ping",
- stats: "Stats",
- "stick-decay-minimum": "Stick decay minimum",
- "stick-decay-strength": "Stick decay strength",
- stream: "Stream",
- "stream-settings": "Stream settings",
- "stream-stats": "Stream stats",
- "stream-your-own-game": "Stream your own game",
- stretch: "Stretch",
- "suggest-settings": "Suggest settings",
- "suggest-settings-link": "Suggest recommended settings for this device",
- "support-better-xcloud": "Support Better xCloud",
- "swap-buttons": "Swap buttons",
- "take-screenshot": "Take screenshot",
- "target-resolution": "Target resolution",
- "tc-all-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: "Top",
- "top-center": "Top-center",
- "top-half": "Top half",
- "top-left": "Top-left",
- "top-right": "Top-right",
- "touch-control-layout": "Touch control layout",
- "touch-control-layout-by": [
- e => `Touch control layout by ${e.name}`,
- e => `Format del control tàctil per ${e.name}`,
- e => `Touch-kontrol layout af ${e.name}`,
- e => `Touch-Steuerungslayout von ${e.name}`,
- e => `Tata letak Sentuhan layar oleh ${e.name}`,
- e => `Disposición del control táctil por ${e.nombre}`,
- e => `Disposition du contrôleur tactile par ${e.name}`,
- e => `Configurazione dei comandi su schermo creata da ${e.name}`,
- e => `タッチ操作レイアウト作成者: ${e.name}`,
- e => `${e.name} 제작, 터치 컨트롤 레이아웃`,
- e => `Układ sterowania dotykowego stworzony przez ${e.name}`,
- e => `Disposição de controle por toque feito por ${e.name}`,
- e => `Сенсорная раскладка по ${e.name}`,
- e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,
- e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
- e => `Розташування сенсорного керування від ${e.name}`,
- e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
- e => `由 ${e.name} 提供的虚拟按键样式`,
- e => `觸控遊玩佈局由 ${e.name} 提供`
- ],
- "touch-controller": "Touch controller",
- "true-achievements": "TrueAchievements",
- ui: "UI",
- "unexpected-behavior": "May cause unexpected behavior",
- "united-states": "United States",
- unknown: "Unknown",
- unlimited: "Unlimited",
- unmuted: "Unmuted",
- unofficial: "Unofficial",
- "unofficial-game-list": "Unofficial game list",
- "unsharp-masking": "Unsharp masking",
- upload: "Upload",
- uploaded: "Uploaded",
- "use-mouse-absolute-position": "Use mouse's absolute position",
- "use-this-at-your-own-risk": "Use this at your own risk",
- "user-agent-profile": "User-Agent profile",
- "vertical-scroll-sensitivity": "Vertical scroll sensitivity",
- "vertical-sensitivity": "Vertical sensitivity",
- "vibration-intensity": "Vibration intensity",
- "vibration-status": "Vibration",
- video: "Video",
- "virtual-controller": "Virtual controller",
- "virtual-controller-slot": "Virtual controller slot",
- "visual-quality": "Visual quality",
- "visual-quality-high": "High",
- "visual-quality-low": "Low",
- "visual-quality-normal": "Normal",
- volume: "Volume",
- "wait-time-countdown": "Countdown",
- "wait-time-estimated": "Estimated finish time",
- "waiting-for-input": "Waiting for input...",
- wallpaper: "Wallpaper",
- webgl2: "WebGL2"
+achievements: "Achievements",
+activate: "Activate",
+activated: "Activated",
+active: "Active",
+advanced: "Advanced",
+"all-games": "All games",
+"always-off": "Always off",
+"always-on": "Always on",
+"amd-fidelity-cas": "AMD FidelityFX CAS",
+"app-settings": "App settings",
+apply: "Apply",
+"aspect-ratio": "Aspect ratio",
+"aspect-ratio-note": "Don't use with native touch games",
+audio: "Audio",
+auto: "Auto",
+availability: "Availability",
+"back-to-home": "Back to home",
+"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?",
+"background-opacity": "Background opacity",
+battery: "Battery",
+"battery-saving": "Battery saving",
+"better-xcloud": "Better xCloud",
+"bitrate-audio-maximum": "Maximum audio bitrate",
+"bitrate-video-maximum": "Maximum video bitrate",
+bottom: "Bottom",
+"bottom-half": "Bottom half",
+"bottom-left": "Bottom-left",
+"bottom-right": "Bottom-right",
+brazil: "Brazil",
+brightness: "Brightness",
+"browser-unsupported-feature": "Your browser doesn't support this feature",
+"button-xbox": "Xbox button",
+"bypass-region-restriction": "Bypass region restriction",
+"can-stream-xbox-360-games": "Can stream Xbox 360 games",
+cancel: "Cancel",
+"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
+center: "Center",
+chat: "Chat",
+"clarity-boost": "Clarity boost",
+"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
+clear: "Clear",
+"clear-data": "Clear data",
+"clear-data-confirm": "Do you want to clear all Better xCloud settings and data?",
+"clear-data-success": "Data cleared! Refresh the page to apply the changes.",
+clock: "Clock",
+close: "Close",
+"close-app": "Close app",
+"combine-audio-video-streams": "Combine audio & video streams",
+"combine-audio-video-streams-summary": "May fix the laggy audio problem",
+"conditional-formatting": "Conditional formatting text color",
+"confirm-delete-preset": "Do you want to delete this preset?",
+"confirm-reload-stream": "Do you want to refresh the stream?",
+connected: "Connected",
+"console-connect": "Connect",
+"continent-asia": "Asia",
+"continent-australia": "Australia",
+"continent-europe": "Europe",
+"continent-north-america": "North America",
+"continent-south-america": "South America",
+contrast: "Contrast",
+controller: "Controller",
+"controller-customization": "Controller customization",
+"controller-customization-input-latency-note": "May slightly increase input latency",
+"controller-friendly-ui": "Controller-friendly UI",
+"controller-shortcuts": "Controller shortcuts",
+"controller-shortcuts-connect-note": "Connect a controller to use this feature",
+"controller-shortcuts-xbox-note": "Button to open the Guide menu",
+"controller-vibration": "Controller vibration",
+copy: "Copy",
+"create-shortcut": "Shortcut",
+custom: "Custom",
+"deadzone-counterweight": "Deadzone counterweight",
+decrease: "Decrease",
+default: "Default",
+"default-opacity": "Default opacity",
+"default-preset-note": "You can't modify default presets. Create a new one to customize it.",
+delete: "Delete",
+"detect-controller-button": "Detect controller button",
+device: "Device",
+"device-unsupported-touch": "Your device doesn't have touch support",
+"device-vibration": "Device vibration",
+"device-vibration-not-using-gamepad": "On when not using gamepad",
+disable: "Disable",
+"disable-features": "Disable features",
+"disable-home-context-menu": "Disable context menu in Home page",
+"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
+"disable-social-features": "Disable social features",
+"disable-xcloud-analytics": "Disable xCloud analytics",
+disabled: "Disabled",
+disconnected: "Disconnected",
+download: "Download",
+downloaded: "Downloaded",
+edit: "Edit",
+"enable-controller-shortcuts": "Enable controller shortcuts",
+"enable-local-co-op-support": "Enable local co-op support",
+"enable-local-co-op-support-note": "Only works with some games",
+"enable-mic-on-startup": "Enable microphone on game launch",
+"enable-mkb": "Emulate controller with Mouse & Keyboard",
+"enable-quick-glance-mode": 'Enable "Quick Glance" mode',
+"enable-remote-play-feature": 'Enable the "Remote Play" feature',
+"enable-volume-control": "Enable volume control feature",
+enabled: "Enabled",
+experimental: "Experimental",
+export: "Export",
+fast: "Fast",
+"force-native-mkb-games": "Force native Mouse & Keyboard for these games",
+"fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',
+"fortnite-force-console-version": "Fortnite: force console version",
+"friends-followers": "Friends and followers",
+"game-bar": "Game Bar",
+"getting-consoles-list": "Getting the list of consoles...",
+guide: "Guide",
+help: "Help",
+hide: "Hide",
+"hide-idle-cursor": "Hide mouse cursor on idle",
+"hide-scrollbar": "Hide web page's scrollbar",
+"hide-sections": "Hide sections",
+"hide-system-menu-icon": "Hide System menu's icon",
+"hide-touch-controller": "Hide touch controller",
+"high-performance": "High performance",
+"highest-quality": "Highest quality",
+"highest-quality-note": "Your device may not be powerful enough to use these settings",
+"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
+"horizontal-sensitivity": "Horizontal sensitivity",
+"how-to-fix": "How to fix",
+"how-to-improve-app-performance": "How to improve app's performance",
+ignore: "Ignore",
+"image-quality": "Website's image quality",
+import: "Import",
+"in-game-controller-customization": "In-game controller customization",
+"in-game-controller-shortcuts": "In-game controller shortcuts",
+"in-game-keyboard-shortcuts": "In-game keyboard shortcuts",
+"in-game-shortcuts": "In-game shortcuts",
+increase: "Increase",
+"install-android": "Better xCloud app for Android",
+invites: "Invites",
+japan: "Japan",
+jitter: "Jitter",
+"keyboard-key": "Keyboard key",
+"keyboard-shortcuts": "Keyboard shortcuts",
+korea: "Korea",
+language: "Language",
+large: "Large",
+layout: "Layout",
+"left-stick": "Left stick",
+"left-stick-deadzone": "Left stick deadzone",
+"left-trigger-range": "Left trigger range",
+"limit-fps": "Limit FPS",
+"load-failed-message": "Failed to run Better xCloud",
+"loading-screen": "Loading screen",
+"local-co-op": "Local co-op",
+"lowest-quality": "Lowest quality",
+manage: "Manage",
+"map-mouse-to": "Map mouse to",
+"may-not-work-properly": "May not work properly!",
+menu: "Menu",
+microphone: "Microphone",
+"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
+"mkb-click-to-activate": "Click to activate",
+"mkb-disclaimer": "This could be viewed as cheating when playing online",
+"modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.",
+"mouse-and-keyboard": "Mouse & Keyboard",
+"mouse-click": "Mouse click",
+"mouse-wheel": "Mouse wheel",
+muted: "Muted",
+name: "Name",
+"native-mkb": "Native Mouse & Keyboard",
+new: "New",
+"new-version-available": [
+e => `Version ${e.version} available`,
+e => `Versió ${e.version} disponible`,
+,
+e => `Version ${e.version} verfügbar`,
+e => `Versi ${e.version} tersedia`,
+e => `Versión ${e.version} disponible`,
+e => `Version ${e.version} disponible`,
+e => `Disponibile la versione ${e.version}`,
+e => `Ver ${e.version} が利用可能です`,
+e => `${e.version} 버전 사용가능`,
+e => `Dostępna jest nowa wersja ${e.version}`,
+e => `Versão ${e.version} disponível`,
+e => `Версия ${e.version} доступна`,
+e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
+e => `${e.version} sayılı yeni sürüm mevcut`,
+e => `Доступна версія ${e.version}`,
+e => `Đã có phiên bản ${e.version}`,
+e => `版本 ${e.version} 可供更新`,
+e => `已可更新為 ${e.version} 版`
+],
+"no-consoles-found": "No consoles found",
+"no-controllers-connected": "No controllers connected",
+normal: "Normal",
+notifications: "Notifications",
+off: "Off",
+official: "Official",
+on: "On",
+"only-supports-some-games": "Only supports some games",
+opacity: "Opacity",
+other: "Other",
+playing: "Playing",
+playtime: "Playtime",
+poland: "Poland",
+"polling-rate": "Polling rate",
+position: "Position",
+"powered-off": "Powered off",
+"powered-on": "Powered on",
+"prefer-ipv6-server": "Prefer IPv6 server",
+"preferred-game-language": "Preferred game's language",
+preset: "Preset",
+press: "Press",
+"press-any-button": "Press any button...",
+"press-esc-to-cancel": "Press Esc to cancel",
+"press-key-to-toggle-mkb": [
+e => `Press ${e.key} to toggle this feature`,
+e => `Premeu ${e.key} per alternar aquesta funció`,
+e => `Tryk på ${e.key} for at slå denne funktion til`,
+e => `${e.key}: Funktion an-/ausschalten`,
+e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,
+e => `Pulsa ${e.key} para alternar esta función`,
+e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
+e => `Premi ${e.key} per attivare questa funzionalità`,
+e => `${e.key} でこの機能を切替`,
+e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
+e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
+e => `Pressione ${e.key} para alternar este recurso`,
+e => `Нажмите ${e.key} для переключения этой функции`,
+e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,
+e => `Etkinleştirmek için ${e.key} tuşuna basın`,
+e => `Натисніть ${e.key} щоб перемкнути цю функцію`,
+e => `Nhấn ${e.key} để bật/tắt tính năng này`,
+e => `按下 ${e.key} 来切换此功能`,
+e => `按下 ${e.key} 來啟用此功能`
+],
+"press-to-bind": "Press a key or do a mouse click to bind...",
+"prompt-preset-name": "Preset's name:",
+recommended: "Recommended",
+"recommended-settings-for-device": [
+e => `Recommended settings for ${e.device}`,
+e => `Configuració recomanada per a ${e.device}`,
+,
+e => `Empfohlene Einstellungen für ${e.device}`,
+e => `Rekomendasi pengaturan untuk ${e.device}`,
+e => `Ajustes recomendados para ${e.device}`,
+e => `Paramètres recommandés pour ${e.device}`,
+e => `Configurazioni consigliate per ${e.device}`,
+e => `${e.device} の推奨設定`,
+e => `다음 기기에서 권장되는 설정: ${e.device}`,
+e => `Zalecane ustawienia dla ${e.device}`,
+e => `Configurações recomendadas para ${e.device}`,
+e => `Рекомендуемые настройки для ${e.device}`,
+e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,
+e => `${e.device} için önerilen ayarlar`,
+e => `Рекомендовані налаштування для ${e.device}`,
+e => `Cấu hình được đề xuất cho ${e.device}`,
+e => `${e.device} 的推荐设置`,
+e => `${e.device} 推薦的設定`
+],
+"reduce-animations": "Reduce UI animations",
+region: "Region",
+"reload-page": "Reload page",
+"remote-play": "Remote Play",
+rename: "Rename",
+renderer: "Renderer",
+"renderer-configuration": "Renderer configuration",
+"reset-highlighted-setting": "Reset highlighted setting",
+"right-click-to-unbind": "Right-click on a key to unbind it",
+"right-stick": "Right stick",
+"right-stick-deadzone": "Right stick deadzone",
+"right-trigger-range": "Right trigger range",
+"rocket-always-hide": "Always hide",
+"rocket-always-show": "Always show",
+"rocket-animation": "Rocket animation",
+"rocket-hide-queue": "Hide when queuing",
+saturation: "Saturation",
+save: "Save",
+screen: "Screen",
+"screenshot-apply-filters": "Apply video filters to screenshots",
+"section-all-games": "All games",
+"section-most-popular": "Most popular",
+"section-native-mkb": "Play with mouse & keyboard",
+"section-news": "News",
+"section-play-with-friends": "Play with friends",
+"section-touch": "Play with touch",
+"separate-touch-controller": "Separate Touch controller & Controller #1",
+"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
+server: "Server",
+"server-locations": "Server locations",
+settings: "Settings",
+"settings-for": "Settings for",
+"settings-reload": "Reload page to reflect changes",
+"settings-reload-note": "Settings in this tab only go into effect on the next page load",
+"settings-reloading": "Reloading...",
+sharpness: "Sharpness",
+"shortcut-keys": "Shortcut keys",
+show: "Show",
+"show-controller-connection-status": "Show controller connection status",
+"show-game-art": "Show game art",
+"show-hide": "Show/hide",
+"show-stats-on-startup": "Show stats when starting the game",
+"show-touch-controller": "Show touch controller",
+"show-wait-time": "Show the estimated wait time",
+"show-wait-time-in-game-card": "Show wait time in game card",
+"simplify-stream-menu": "Simplify Stream's menu",
+"skip-splash-video": "Skip Xbox splash video",
+slow: "Slow",
+small: "Small",
+"smart-tv": "Smart TV",
+sound: "Sound",
+standard: "Standard",
+standby: "Standby",
+"stat-bitrate": "Bitrate",
+"stat-decode-time": "Decode time",
+"stat-fps": "FPS",
+"stat-frames-lost": "Frames lost",
+"stat-packets-lost": "Packets lost",
+"stat-ping": "Ping",
+stats: "Stats",
+"stick-decay-minimum": "Stick decay minimum",
+"stick-decay-strength": "Stick decay strength",
+stream: "Stream",
+"stream-settings": "Stream settings",
+"stream-stats": "Stream stats",
+"stream-your-own-game": "Stream your own game",
+stretch: "Stretch",
+"suggest-settings": "Suggest settings",
+"suggest-settings-link": "Suggest recommended settings for this device",
+"support-better-xcloud": "Support Better xCloud",
+"swap-buttons": "Swap buttons",
+"take-screenshot": "Take screenshot",
+"target-resolution": "Target resolution",
+"tc-all-white": "All white",
+"tc-auto-off": "Off when controller found",
+"tc-custom-layout-style": "Custom layout's button style",
+"tc-muted-colors": "Muted colors",
+"tc-standard-layout-style": "Standard layout's button style",
+"text-size": "Text size",
+toggle: "Toggle",
+top: "Top",
+"top-center": "Top-center",
+"top-half": "Top half",
+"top-left": "Top-left",
+"top-right": "Top-right",
+"touch-control-layout": "Touch control layout",
+"touch-control-layout-by": [
+e => `Touch control layout by ${e.name}`,
+e => `Format del control tàctil per ${e.name}`,
+e => `Touch-kontrol layout af ${e.name}`,
+e => `Touch-Steuerungslayout von ${e.name}`,
+e => `Tata letak Sentuhan layar oleh ${e.name}`,
+e => `Disposición del control táctil por ${e.nombre}`,
+e => `Disposition du contrôleur tactile par ${e.name}`,
+e => `Configurazione dei comandi su schermo creata da ${e.name}`,
+e => `タッチ操作レイアウト作成者: ${e.name}`,
+e => `${e.name} 제작, 터치 컨트롤 레이아웃`,
+e => `Układ sterowania dotykowego stworzony przez ${e.name}`,
+e => `Disposição de controle por toque feito por ${e.name}`,
+e => `Сенсорная раскладка по ${e.name}`,
+e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,
+e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
+e => `Розташування сенсорного керування від ${e.name}`,
+e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
+e => `由 ${e.name} 提供的虚拟按键样式`,
+e => `觸控遊玩佈局由 ${e.name} 提供`
+],
+"touch-controller": "Touch controller",
+"true-achievements": "TrueAchievements",
+ui: "UI",
+"unexpected-behavior": "May cause unexpected behavior",
+"united-states": "United States",
+unknown: "Unknown",
+unlimited: "Unlimited",
+unmuted: "Unmuted",
+unofficial: "Unofficial",
+"unofficial-game-list": "Unofficial game list",
+"unsharp-masking": "Unsharp masking",
+upload: "Upload",
+uploaded: "Uploaded",
+"use-mouse-absolute-position": "Use mouse's absolute position",
+"use-this-at-your-own-risk": "Use this at your own risk",
+"user-agent-profile": "User-Agent profile",
+"vertical-scroll-sensitivity": "Vertical scroll sensitivity",
+"vertical-sensitivity": "Vertical sensitivity",
+"vibration-intensity": "Vibration intensity",
+"vibration-status": "Vibration",
+video: "Video",
+"virtual-controller": "Virtual controller",
+"virtual-controller-slot": "Virtual controller slot",
+"visual-quality": "Visual quality",
+"visual-quality-high": "High",
+"visual-quality-low": "Low",
+"visual-quality-normal": "Normal",
+volume: "Volume",
+"wait-time-countdown": "Countdown",
+"wait-time-estimated": "Estimated finish time",
+"waiting-for-input": "Waiting for input...",
+wallpaper: "Wallpaper",
+webgl2: "WebGL2"
};
class Translations {
- static EN_US = "en-US";
- static KEY_LOCALE = "BetterXcloud.Locale";
- static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";
- static selectedLocaleIndex = -1;
- static selectedLocale = "en-US";
- static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
- static foreignTranslations = {};
- static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);
- static async init() {
- Translations.refreshLocale(), await Translations.loadTranslations();
- }
- static refreshLocale(newLocale) {
- let locale;
- if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;
- else locale = localStorage.getItem(Translations.KEY_LOCALE);
- let supportedLocales = Translations.supportedLocales;
- if (!locale) {
- if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;
- localStorage.setItem(Translations.KEY_LOCALE, locale);
- }
- Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);
- }
- static get(key, values) {
- let text = null;
- if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];
- if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);
- let translation;
- if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);
- return translation = text, translation;
- }
- static async loadTranslations() {
- if (Translations.selectedLocale === Translations.EN_US) return;
- try {
- Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));
- } catch (e) {}
- if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);
- }
- static async updateTranslations(async = !1) {
- if (Translations.selectedLocale === Translations.EN_US) {
- localStorage.removeItem(Translations.KEY_TRANSLATIONS);
- return;
- }
- if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);
- else await Translations.downloadTranslations(Translations.selectedLocale);
- }
- static async downloadTranslations(locale) {
- try {
- let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();
- if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
- return !0;
- } catch (e) {
- debugger;
- }
- return !1;
- }
- static downloadTranslationsAsync(locale) {
- NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {
- window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
- });
- }
- static switchLocale(locale) {
- localStorage.setItem(Translations.KEY_LOCALE, locale);
- }
+static EN_US = "en-US";
+static KEY_LOCALE = "BetterXcloud.Locale";
+static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";
+static selectedLocaleIndex = -1;
+static selectedLocale = "en-US";
+static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
+static foreignTranslations = {};
+static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);
+static async init() {
+Translations.refreshLocale(), await Translations.loadTranslations();
+}
+static refreshLocale(newLocale) {
+let locale;
+if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;
+else locale = localStorage.getItem(Translations.KEY_LOCALE);
+let supportedLocales = Translations.supportedLocales;
+if (!locale) {
+if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;
+localStorage.setItem(Translations.KEY_LOCALE, locale);
+}
+Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);
+}
+static get(key, values) {
+let text = null;
+if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];
+if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);
+let translation;
+if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);
+return translation = text, translation;
+}
+static async loadTranslations() {
+if (Translations.selectedLocale === Translations.EN_US) return;
+try {
+Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));
+} catch (e) {}
+if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);
+}
+static async updateTranslations(async = !1) {
+if (Translations.selectedLocale === Translations.EN_US) {
+localStorage.removeItem(Translations.KEY_TRANSLATIONS);
+return;
+}
+if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);
+else await Translations.downloadTranslations(Translations.selectedLocale);
+}
+static async downloadTranslations(locale) {
+try {
+let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();
+if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
+return !0;
+} catch (e) {
+debugger;
+}
+return !1;
+}
+static downloadTranslationsAsync(locale) {
+NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {
+window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
+});
+}
+static switchLocale(locale) {
+localStorage.setItem(Translations.KEY_LOCALE, locale);
+}
}
var t = Translations.get;
Translations.init();
class NavigationUtils {
- static setNearby($elm, nearby) {
- $elm.nearby = $elm.nearby || {};
- let key;
- for (key in nearby)
- $elm.nearby[key] = nearby[key];
- }
+static setNearby($elm, nearby) {
+$elm.nearby = $elm.nearby || {};
+let key;
+for (key in nearby)
+$elm.nearby[key] = nearby[key];
+}
}
var setNearby = NavigationUtils.setNearby;
var ButtonStyleClass = {
- 1: "bx-primary",
- 2: "bx-warning",
- 4: "bx-danger",
- 8: "bx-ghost",
- 16: "bx-frosted",
- 32: "bx-drop-shadow",
- 64: "bx-focusable",
- 128: "bx-full-width",
- 256: "bx-full-height",
- 512: "bx-auto-height",
- 1024: "bx-tall",
- 2048: "bx-circular",
- 4096: "bx-normal-case",
- 8192: "bx-normal-link"
+1: "bx-primary",
+2: "bx-warning",
+4: "bx-danger",
+8: "bx-ghost",
+16: "bx-frosted",
+32: "bx-drop-shadow",
+64: "bx-focusable",
+128: "bx-full-width",
+256: "bx-full-height",
+512: "bx-auto-height",
+1024: "bx-tall",
+2048: "bx-circular",
+4096: "bx-normal-case",
+8192: "bx-normal-link"
};
function createElement(elmName, props, ..._) {
- let $elm, hasNs = props && "xmlns" in props;
- if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;
- else $elm = document.createElement(elmName);
- if (props) {
- if (props._nearby) setNearby($elm, props._nearby), delete props._nearby;
- if (props._on) {
- for (let name in props._on)
- $elm.addEventListener(name, props._on[name]);
- delete props._on;
- }
- if (props._dataset) {
- for (let name in props._dataset)
- $elm.dataset[name] = props._dataset[name];
- delete props._dataset;
- }
- for (let key in props) {
- if ($elm.hasOwnProperty(key)) continue;
- let value = props[key];
- if (hasNs) $elm.setAttributeNS(null, key, value);
- else $elm.setAttribute(key, value);
- }
- }
- for (let i = 2, size = arguments.length;i < size; i++) {
- let arg = arguments[i];
- if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg);
- }
- return $elm;
+let $elm, hasNs = props && "xmlns" in props;
+if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;
+else $elm = document.createElement(elmName);
+if (props) {
+if (props._nearby) setNearby($elm, props._nearby), delete props._nearby;
+if (props._on) {
+for (let name in props._on)
+$elm.addEventListener(name, props._on[name]);
+delete props._on;
+}
+if (props._dataset) {
+for (let name in props._dataset)
+$elm.dataset[name] = props._dataset[name];
+delete props._dataset;
+}
+for (let key in props) {
+if ($elm.hasOwnProperty(key)) continue;
+let value = props[key];
+if (hasNs) $elm.setAttributeNS(null, key, value);
+else $elm.setAttribute(key, value);
+}
+}
+for (let i = 2, size = arguments.length;i < size; i++) {
+let arg = arguments[i];
+if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg);
+}
+return $elm;
}
var domParser = new DOMParser;
function createSvgIcon(icon) {
- return domParser.parseFromString(icon, "image/svg+xml").documentElement;
+return domParser.parseFromString(icon, "image/svg+xml").documentElement;
}
var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i));
function createButton(options) {
- let $btn;
- if (options.url) $btn = CE("a", {
- class: "bx-button",
- href: options.url,
- target: "_blank"
- });
- else $btn = CE("button", {
- class: "bx-button",
- type: "button"
- }), options.disabled && ($btn.disabled = !0);
- let style = options.style || 0;
- if (style) {
- let index;
- for (index of ButtonStyleIndices)
- style & index && $btn.classList.add(ButtonStyleClass[index]);
- }
- if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText));
- for (let key in options.attributes)
- if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]);
- return $btn;
+let $btn;
+if (options.url) $btn = CE("a", {
+class: "bx-button",
+href: options.url,
+target: "_blank"
+});
+else $btn = CE("button", {
+class: "bx-button",
+type: "button"
+}), options.disabled && ($btn.disabled = !0);
+let style = options.style || 0;
+if (style) {
+let index;
+for (index of ButtonStyleIndices)
+style & index && $btn.classList.add(ButtonStyleClass[index]);
+}
+if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText));
+for (let key in options.attributes)
+if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]);
+return $btn;
}
function createSettingRow(label, $control, options = {}) {
- let $label, $row = CE("label", { class: "bx-settings-row" }, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control), $link = $label.querySelector("a");
- if ($link) $link.classList.add("bx-focusable"), setNearby($label, {
- focus: $link
- });
- if (setNearby($row, {
- orientation: options.multiLines ? "vertical" : "horizontal"
- }), options.multiLines)
- $row.dataset.multiLines = "true";
- if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id;
- return $row;
+let $label, $row = CE("label", {
+class: "bx-settings-row"
+}, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control);
+if (options.pref) $row.prefKey = options.pref;
+if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu);
+let $link = $label.querySelector("a");
+if ($link) $link.classList.add("bx-focusable"), setNearby($label, {
+focus: $link
+});
+if (setNearby($row, {
+orientation: options.multiLines ? "vertical" : "horizontal"
+}), options.multiLines)
+$row.dataset.multiLines = "true";
+if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id;
+return $row;
}
function isElementVisible($elm) {
- let rect = $elm.getBoundingClientRect();
- return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;
+let rect = $elm.getBoundingClientRect();
+return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;
}
function removeChildElements($parent) {
- if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select");
- while ($parent.firstElementChild)
- $parent.firstElementChild.remove();
+if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select");
+while ($parent.firstElementChild)
+$parent.firstElementChild.remove();
}
function clearDataSet($elm) {
- Object.keys($elm.dataset).forEach((key) => {
- delete $elm.dataset[key];
- });
+Object.keys($elm.dataset).forEach((key) => {
+delete $elm.dataset[key];
+});
}
function calculateSelectBoxes($root) {
- let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select"));
- for (let $select of selects) {
- let $parent = $select.parentElement;
- if ($parent.classList.contains("bx-full-width")) {
- $parent.dataset.calculated = "true";
- continue;
- }
- let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width);
- if (!width) continue;
- if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) {
- if ($select.multiple) width += 20;
- if ($select.querySelector("optgroup")) width -= 15;
- } else width += 10;
- $select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
- }
+let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select"));
+for (let $select of selects) {
+let $parent = $select.parentElement;
+if ($parent.classList.contains("bx-full-width")) {
+$parent.dataset.calculated = "true";
+continue;
+}
+let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width);
+if (!width) continue;
+if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) {
+if ($select.multiple) width += 20;
+if ($select.querySelector("optgroup")) width -= 15;
+} else width += 10;
+$select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
+}
}
var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"];
function humanFileSize(size) {
- let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
- return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i];
+let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
+return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i];
}
function secondsToHm(seconds) {
- let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1;
- if (m === 60) h += 1, m = 0;
- let output = [];
- return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" ");
+let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1;
+if (m === 60) h += 1, m = 0;
+let output = [];
+return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" ");
}
function escapeCssSelector(name) {
- return name.replaceAll(".", "-");
+return name.replaceAll(".", "-");
}
var CE = createElement;
window.BX_CE = createElement;
class Toast {
- static instance;
- static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);
- LOG_TAG = "Toast";
- $wrapper;
- $msg;
- $status;
- stack = [];
- isShowing = !1;
- timeoutId;
- DURATION = 3000;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {
- let classList = this.$wrapper.classList;
- if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();
- }), document.documentElement.appendChild(this.$wrapper);
- }
- show(msg, status, options = {}) {
- options = options || {};
- let args = Array.from(arguments);
- if (options.instant) this.stack = [args], this.showNext();
- else this.stack.push(args), !this.isShowing && this.showNext();
- }
- showNext() {
- if (!this.stack.length) {
- this.isShowing = !1;
- return;
- }
- this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION);
- let [msg, status, options] = this.stack.shift();
- if (options && options.html) this.$msg.innerHTML = msg;
- else this.$msg.textContent = msg;
- if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;
- else this.$status.classList.add("bx-gone");
- let classList = this.$wrapper.classList;
- classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
- }
- hide = () => {
- this.timeoutId = null;
- let classList = this.$wrapper.classList;
- classList.remove("bx-show"), classList.add("bx-hide");
- };
- static show(msg, status, options = {}) {
- Toast.getInstance().show(msg, status, options);
- }
- static showNext() {
- Toast.getInstance().showNext();
- }
+static instance;
+static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);
+LOG_TAG = "Toast";
+$wrapper;
+$msg;
+$status;
+stack = [];
+isShowing = !1;
+timeoutId;
+DURATION = 3000;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {
+let classList = this.$wrapper.classList;
+if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();
+}), document.documentElement.appendChild(this.$wrapper);
}
-var BypassServers = {
- br: t("brazil"),
- jp: t("japan"),
- kr: t("korea"),
- pl: t("poland"),
- us: t("united-states")
-}, BypassServerIps = {
- br: "169.150.198.66",
- kr: "121.125.60.151",
- jp: "138.199.21.239",
- pl: "45.134.212.66",
- us: "143.244.47.65"
+show(msg, status, options = {}) {
+options = options || {};
+let args = Array.from(arguments);
+if (options.instant) this.stack = [args], this.showNext();
+else this.stack.push(args), !this.isShowing && this.showNext();
+}
+showNext() {
+if (!this.stack.length) {
+this.isShowing = !1;
+return;
+}
+this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION);
+let [msg, status, options] = this.stack.shift();
+if (options && options.html) this.$msg.innerHTML = msg;
+else this.$msg.textContent = msg;
+if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;
+else this.$status.classList.add("bx-gone");
+let classList = this.$wrapper.classList;
+classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
+}
+hide = () => {
+this.timeoutId = null;
+let classList = this.$wrapper.classList;
+classList.remove("bx-show"), classList.add("bx-hide");
};
-class BaseSettingsStore {
- storage;
- storageKey;
- _settings;
- definitions;
- constructor(storageKey, definitions) {
- this.storage = window.localStorage, this.storageKey = storageKey;
- let settingId;
- for (settingId in definitions) {
- let 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;
- }
- get settings() {
- if (this._settings) return this._settings;
- let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");
- for (let key in settings)
- settings[key] = this.validateValue("get", key, settings[key]);
- return this._settings = settings, settings;
- }
- getDefinition(key) {
- if (!this.definitions[key]) {
- let error = "Request invalid definition: " + key;
- throw alert(error), Error(error);
- }
- return this.definitions[key];
- }
- getSetting(key, checkUnsupported = !0) {
- let definition = this.definitions[key];
- if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return definition.default;
- if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;
- else return definition.default;
- if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);
- return this.settings[key];
- }
- setSetting(key, value, emitEvent = !1) {
- return value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), emitEvent && BxEventBus.Script.emit("setting.changed", {
- storageKey: this.storageKey,
- settingKey: key,
- settingValue: value
- }), value;
- }
- saveSettings() {
- this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
- }
- validateValue(action, key, value) {
- let def = this.definitions[key];
- if (!def) return value;
- if (typeof value === "undefined" || value === null) value = def.default;
- if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);
- if ("min" in def) value = Math.max(def.min, value);
- if ("max" in def) value = Math.min(def.max, value);
- if ("options" in def) {
- if (!(value in def.options)) value = def.default;
- } else if ("multipleOptions" in def) {
- if (value.length) {
- let validOptions = Object.keys(def.multipleOptions);
- value.forEach((item, idx) => {
- validOptions.indexOf(item) === -1 && value.splice(idx, 1);
- });
- }
- if (!value.length) value = def.default;
- }
- if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);
- return value;
- }
- getLabel(key) {
- return this.definitions[key].label || key;
- }
- getValueText(key, value) {
- let definition = this.definitions[key];
- if ("min" in definition) {
- let params = definition.params;
- if (params.customTextValue) {
- if (definition.transformValue) value = definition.transformValue.get.call(definition, value);
- let text = params.customTextValue(value, definition.min, definition.max);
- if (text) return text;
- }
- return value.toString();
- } else if ("options" in definition) {
- let options = definition.options;
- if (value in options) return options[value];
- } else if (typeof value === "boolean") return value ? t("on") : t("off");
- return value.toString();
- }
+static show(msg, status, options = {}) {
+Toast.getInstance().show(msg, status, options);
+}
+static showNext() {
+Toast.getInstance().showNext();
+}
}
class LocalDb {
- static instance;
- static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb);
- static DB_NAME = "BetterXcloud";
- static DB_VERSION = 4;
- static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers";
- static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts";
- static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations";
- static TABLE_CONTROLLER_SETTINGS = "controller_settings";
- static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts";
- db;
- open() {
- return new Promise((resolve, reject) => {
- if (this.db) {
- resolve(this.db);
- return;
- }
- let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
- request.onupgradeneeded = (e) => {
- let db = e.target.result;
- if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
- if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {
- keyPath: "id"
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {
- keyPath: "id",
- autoIncrement: !0
- });
- }, request.onerror = (e) => {
- console.log(e), alert(e.target.error.message), reject && reject();
- }, request.onsuccess = (e) => {
- this.db = e.target.result, resolve(this.db);
- };
- });
- }
+static instance;
+static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb);
+static DB_NAME = "BetterXcloud";
+static DB_VERSION = 4;
+static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers";
+static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts";
+static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations";
+static TABLE_CONTROLLER_SETTINGS = "controller_settings";
+static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts";
+db;
+open() {
+return new Promise((resolve, reject) => {
+if (this.db) {
+resolve(this.db);
+return;
}
-class BaseLocalTable {
- tableName;
- constructor(tableName) {
- this.tableName = tableName;
- }
- async prepareTable(type = "readonly") {
- return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);
- }
- call(method) {
- return new Promise((resolve) => {
- let request = method.call(null, ...Array.from(arguments).slice(1));
- request.onsuccess = (e) => {
- resolve(e.target.result);
- };
- });
- }
- async count() {
- let table = await this.prepareTable();
- return this.call(table.count.bind(table));
- }
- async add(data) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.add.bind(table), ...arguments);
- }
- async put(data) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.put.bind(table), ...arguments);
- }
- async delete(id) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.delete.bind(table), ...arguments);
- }
- async get(id) {
- let table = await this.prepareTable();
- return this.call(table.get.bind(table), ...arguments);
- }
- async getAll() {
- let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};
- return all.forEach((item) => {
- results[item.id] = item;
- }), results;
- }
+let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
+request.onupgradeneeded = (e) => {
+let db = e.target.result;
+if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
+if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {
+keyPath: "id"
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {
+keyPath: "id",
+autoIncrement: !0
+});
+}, request.onerror = (e) => {
+console.log(e), alert(e.target.error.message), reject && reject();
+}, request.onsuccess = (e) => {
+this.db = e.target.result, resolve(this.db);
+};
+});
}
-class BasePresetsTable extends BaseLocalTable {
- async newPreset(name, data) {
- let newRecord = { name, data };
- return await this.add(newRecord);
- }
- async updatePreset(preset) {
- return await this.put(preset);
- }
- async deletePreset(id) {
- return this.delete(id);
- }
- async getPreset(id) {
- if (id === 0) return null;
- if (id < 0) return this.DEFAULT_PRESETS[id];
- let preset = await this.get(id);
- if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];
- return preset;
- }
- async getPresets() {
- let all = deepClone(this.DEFAULT_PRESETS), presets = {
- default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)),
- custom: [],
- data: {}
- };
- if (await this.count() > 0) {
- let items = await this.getAll(), id;
- for (id in items) {
- let item = items[id];
- presets.custom.push(item.id), all[item.id] = item;
- }
- }
- return presets.data = all, presets;
- }
- async getPresetsData() {
- let presetsData = {};
- for (let id in this.DEFAULT_PRESETS) {
- let preset = this.DEFAULT_PRESETS[id];
- presetsData[id] = deepClone(preset.data);
- }
- if (await this.count() > 0) {
- let items = await this.getAll(), id;
- for (id in items) {
- let item = items[id];
- presetsData[item.id] = item.data;
- }
- }
- return presetsData;
- }
}
-class MkbMappingPresetsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable);
- LOG_TAG = "MkbMappingPresetsTable";
- TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: t("standard"),
- data: {
- mapping: {
- 16: ["Backquote"],
- 12: ["ArrowUp", "Digit1"],
- 13: ["ArrowDown", "Digit2"],
- 14: ["ArrowLeft", "Digit3"],
- 15: ["ArrowRight", "Digit4"],
- 100: ["KeyW"],
- 101: ["KeyS"],
- 102: ["KeyA"],
- 103: ["KeyD"],
- 200: ["KeyU"],
- 201: ["KeyJ"],
- 202: ["KeyH"],
- 203: ["KeyK"],
- 0: ["Space", "KeyE"],
- 2: ["KeyR"],
- 1: ["KeyC", "Backspace"],
- 3: ["KeyV"],
- 9: ["Enter"],
- 8: ["Tab"],
- 4: ["KeyQ"],
- 5: ["KeyF"],
- 7: ["Mouse0"],
- 6: ["Mouse2"],
- 10: ["KeyX"],
- 11: ["KeyZ"]
- },
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- }
- },
- [-2]: {
- id: -2,
- name: "Shooter",
- data: {
- mapping: {
- 16: ["Backquote"],
- 12: ["ArrowUp"],
- 13: ["ArrowDown"],
- 14: ["ArrowLeft"],
- 15: ["ArrowRight"],
- 100: ["KeyW"],
- 101: ["KeyS"],
- 102: ["KeyA"],
- 103: ["KeyD"],
- 200: ["KeyI"],
- 201: ["KeyK"],
- 202: ["KeyJ"],
- 203: ["KeyL"],
- 0: ["Space", "KeyE"],
- 2: ["KeyR"],
- 1: ["ControlLeft", "Backspace"],
- 3: ["KeyV"],
- 9: ["Enter"],
- 8: ["Tab"],
- 4: ["KeyC", "KeyG"],
- 5: ["KeyQ"],
- 7: ["Mouse0"],
- 6: ["Mouse2"],
- 10: ["ShiftLeft"],
- 11: ["KeyF"]
- },
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {},
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
+var BypassServers = {
+br: t("brazil"),
+jp: t("japan"),
+kr: t("korea"),
+pl: t("poland"),
+us: t("united-states")
+}, BypassServerIps = {
+br: "169.150.198.66",
+kr: "121.125.60.151",
+jp: "138.199.21.239",
+pl: "45.134.212.66",
+us: "143.244.47.65"
+};
+class BaseSettingsStorage {
+storage;
+storageKey;
+_settings;
+definitions;
+constructor(storageKey, definitions) {
+this.storage = window.localStorage, this.storageKey = storageKey;
+for (let [_, setting] of Object.entries(definitions)) {
+if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants];
+setting.ready && setting.ready.call(this, setting);
+}
+this.definitions = definitions, this._settings = null;
+}
+get settings() {
+if (this._settings) return this._settings;
+let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");
+for (let key in settings)
+settings[key] = this.validateValue("get", key, settings[key]);
+return this._settings = settings, settings;
+}
+getDefinition(key) {
+if (!this.definitions[key]) return alert("Request invalid definition: " + key), {};
+return this.definitions[key];
+}
+hasSetting(key) {
+return key in this.settings;
+}
+getSetting(key, checkUnsupported = !0) {
+let definition = this.definitions[key];
+if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
+if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;
+else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
+if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);
+return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key];
+}
+setSetting(key, value, origin) {
+if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", {
+storageKey: this.storageKey,
+settingKey: key
+});
+else BxEventBus.Script.emit("setting.changed", {
+storageKey: this.storageKey,
+settingKey: key
+});
+return value;
+}
+saveSettings() {
+this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
+}
+validateValue(action, key, value) {
+let def = this.definitions[key];
+if (!def) return value;
+if (typeof value === "undefined" || value === null) value = def.default;
+if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);
+if ("min" in def) value = Math.max(def.min, value);
+if ("max" in def) value = Math.min(def.max, value);
+if ("options" in def) {
+if (!(value in def.options)) value = def.default;
+} else if ("multipleOptions" in def) {
+if (value.length) {
+let validOptions = Object.keys(def.multipleOptions);
+value.forEach((item, idx) => {
+validOptions.indexOf(item) === -1 && value.splice(idx, 1);
+});
+}
+if (!value.length) value = def.default;
+}
+if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);
+return value;
+}
+getLabel(key) {
+return this.definitions[key].label || key;
+}
+getValueText(key, value) {
+let definition = this.definitions[key];
+if ("min" in definition) {
+let params = definition.params;
+if (params.customTextValue) {
+if (definition.transformValue) value = definition.transformValue.get.call(definition, value);
+let text = params.customTextValue(value, definition.min, definition.max);
+if (text) return text;
+}
+return value.toString();
+} else if ("options" in definition) {
+let options = definition.options;
+if (value in options) return options[value];
+} else if (typeof value === "boolean") return value ? t("on") : t("off");
+return value.toString();
}
-class KeyboardShortcutsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);
- LOG_TAG = "KeyboardShortcutsTable";
- TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: t("standard"),
- data: {
- mapping: {
- "mkb.toggle": {
- code: "F8"
- },
- "stream.screenshot.capture": {
- code: "Slash"
- }
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {}
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
}
var BxIcon = {
- BETTER_XCLOUD: "",
- TRUE_ACHIEVEMENTS: "",
- STREAM_SETTINGS: "",
- STREAM_STATS: "",
- CLOSE: "",
- CONTROLLER: "",
- CREATE_SHORTCUT: "",
- DISPLAY: "",
- EYE: "",
- EYE_SLASH: "",
- HOME: "",
- LOCAL_CO_OP: "",
- NATIVE_MKB: "",
- NEW: "",
- MANAGE: "",
- COPY: "",
- TRASH: "",
- CURSOR_TEXT: "",
- POWER: "",
- QUESTION: "",
- REFRESH: "",
- REMOTE_PLAY: "",
- CARET_LEFT: "",
- CARET_RIGHT: "",
- SCREENSHOT: "",
- SPEAKER_MUTED: "",
- TOUCH_CONTROL_ENABLE: "",
- TOUCH_CONTROL_DISABLE: "",
- MICROPHONE: "",
- MICROPHONE_MUTED: "",
- BATTERY: "",
- PLAYTIME: "",
- SERVER: "",
- DOWNLOAD: "",
- UPLOAD: "",
- AUDIO: ""
+BETTER_XCLOUD: "",
+TRUE_ACHIEVEMENTS: "",
+STREAM_SETTINGS: "",
+STREAM_STATS: "",
+CLOSE: "",
+CONTROLLER: "",
+CREATE_SHORTCUT: "",
+DISPLAY: "",
+EYE: "",
+EYE_SLASH: "",
+HOME: "",
+LOCAL_CO_OP: "",
+NATIVE_MKB: "",
+NEW: "",
+MANAGE: "",
+COPY: "",
+TRASH: "",
+CURSOR_TEXT: "",
+POWER: "",
+QUESTION: "",
+REFRESH: "",
+REMOTE_PLAY: "",
+CARET_LEFT: "",
+CARET_RIGHT: "",
+SCREENSHOT: "",
+SPEAKER_MUTED: "",
+TOUCH_CONTROL_ENABLE: "",
+TOUCH_CONTROL_DISABLE: "",
+MICROPHONE: "",
+MICROPHONE_MUTED: "",
+BATTERY: "",
+PLAYTIME: "",
+SERVER: "",
+DOWNLOAD: "",
+UPLOAD: "",
+AUDIO: ""
};
function getSupportedCodecProfiles() {
- let options = {
- default: t("default")
- };
- if (!("getCapabilities" in RTCRtpReceiver)) return options;
- let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;
- for (let codec of codecs) {
- if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;
- let fmtp = codec.sdpFmtpLine.toLowerCase();
- if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;
- else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;
- else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;
- }
- if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`;
- else options["low"] = t("visual-quality-low");
- if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`;
- else options["normal"] = t("visual-quality-normal");
- if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`;
- else options["high"] = t("visual-quality-high");
- return options;
+let options = {
+default: t("default")
+};
+if (!("getCapabilities" in RTCRtpReceiver)) return options;
+let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;
+for (let codec of codecs) {
+if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;
+let fmtp = codec.sdpFmtpLine.toLowerCase();
+if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;
+else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;
+else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;
}
-class GlobalSettingsStorage extends BaseSettingsStore {
- static DEFINITIONS = {
- "version.lastCheck": {
- default: 0
- },
- "version.latest": {
- default: ""
- },
- "version.current": {
- default: ""
- },
- "bx.locale": {
- label: t("language"),
- default: localStorage.getItem("BetterXcloud.Locale") || "en-US",
- options: SUPPORTED_LANGUAGES
- },
- "server.region": {
- label: t("region"),
- note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
- default: "default"
- },
- "server.bypassRestriction": {
- label: t("bypass-region-restriction"),
- note: "⚠️ " + t("use-this-at-your-own-risk"),
- default: "off",
- optionsGroup: t("region"),
- options: Object.assign({
- off: t("off")
- }, BypassServers)
- },
- "stream.locale": {
- label: t("preferred-game-language"),
- default: "default",
- options: {
- default: t("default"),
- "ar-SA": "العربية",
- "bg-BG": "Български",
- "cs-CZ": "čeština",
- "da-DK": "dansk",
- "de-DE": "Deutsch",
- "el-GR": "Ελληνικά",
- "en-GB": "English (UK)",
- "en-US": "English (US)",
- "es-ES": "español (España)",
- "es-MX": "español (Latinoamérica)",
- "fi-FI": "suomi",
- "fr-FR": "français",
- "he-IL": "עברית",
- "hu-HU": "magyar",
- "it-IT": "italiano",
- "ja-JP": "日本語",
- "ko-KR": "한국어",
- "nb-NO": "norsk bokmål",
- "nl-NL": "Nederlands",
- "pl-PL": "polski",
- "pt-BR": "português (Brasil)",
- "pt-PT": "português (Portugal)",
- "ro-RO": "Română",
- "ru-RU": "русский",
- "sk-SK": "slovenčina",
- "sv-SE": "svenska",
- "th-TH": "ไทย",
- "tr-TR": "Türkçe",
- "zh-CN": "中文(简体)",
- "zh-TW": "中文 (繁體)"
- }
- },
- "stream.video.resolution": {
- label: t("target-resolution"),
- default: "auto",
- options: {
- auto: t("default"),
- "720p": "720p",
- "1080p": "1080p",
- "1080p-hq": "1080p (HQ)"
- },
- suggest: {
- lowest: "720p",
- highest: "1080p-hq"
- }
- },
- "stream.video.codecProfile": {
- label: t("visual-quality"),
- default: "default",
- options: getSupportedCodecProfiles(),
- ready: (setting) => {
- let options = setting.options, keys = Object.keys(options);
- if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");
- setting.suggest = {
- lowest: keys.length === 1 ? keys[0] : keys[1],
- highest: keys[keys.length - 1]
- };
- }
- },
- "server.ipv6.prefer": {
- label: t("prefer-ipv6-server"),
- default: !1
- },
- "screenshot.applyFilters": {
- requiredVariants: "full",
- label: t("screenshot-apply-filters"),
- default: !1
- },
- "ui.splashVideo.skip": {
- label: t("skip-splash-video"),
- default: !1
- },
- "ui.systemMenu.hideHandle": {
- label: t("hide-system-menu-icon"),
- default: !1
- },
- "ui.imageQuality": {
- requiredVariants: "full",
- label: t("image-quality"),
- default: 90,
- min: 10,
- max: 90,
- params: {
- steps: 10,
- exactTicks: 20,
- hideSlider: !0,
- customTextValue(value, min, max) {
- if (value === 90) return t("default");
- return value + "%";
- }
- }
- },
- "stream.video.combineAudio": {
- requiredVariants: "full",
- label: t("combine-audio-video-streams"),
- default: !1,
- experimental: !0,
- note: t("combine-audio-video-streams-summary")
- },
- "touchController.mode": {
- requiredVariants: "full",
- label: t("tc-availability"),
- default: "all",
- options: {
- default: t("default"),
- off: t("off"),
- all: t("tc-all-games")
- },
- unsupported: !STATES.userAgent.capabilities.touch,
- unsupportedValue: "default"
- },
- "touchController.autoOff": {
- requiredVariants: "full",
- label: t("tc-auto-off"),
- default: !1,
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.opacity.default": {
- requiredVariants: "full",
- label: t("tc-default-opacity"),
- default: 100,
- min: 10,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10,
- hideSlider: !0
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.style.standard": {
- requiredVariants: "full",
- label: t("tc-standard-layout-style"),
- default: "default",
- options: {
- default: t("default"),
- white: t("tc-all-white"),
- muted: t("tc-muted-colors")
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.style.custom": {
- requiredVariants: "full",
- label: t("tc-custom-layout-style"),
- default: "default",
- options: {
- default: t("default"),
- muted: t("tc-muted-colors")
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "ui.streamMenu.simplify": {
- label: t("simplify-stream-menu"),
- default: !1
- },
- "mkb.cursor.hideIdle": {
- requiredVariants: "full",
- label: t("hide-idle-cursor"),
- default: !1
- },
- "ui.feedbackDialog.disabled": {
- requiredVariants: "full",
- label: t("disable-post-stream-feedback-dialog"),
- default: !1
- },
- "stream.video.maxBitrate": {
- requiredVariants: "full",
- label: t("bitrate-video-maximum"),
- note: "⚠️ " + t("unexpected-behavior"),
- default: 0,
- min: 102400,
- max: 15360000,
- transformValue: {
- get(value) {
- return value === 0 ? this.max : value;
- },
- set(value) {
- return value === this.max ? 0 : value;
- }
- },
- params: {
- steps: 102400,
- exactTicks: 5120000,
- customTextValue: (value, min, max) => {
- if (value = parseInt(value), value === max) return t("unlimited");
- else return (value / 1024000).toFixed(1) + " Mb/s";
- }
- },
- suggest: {
- highest: 0
- }
- },
- "gameBar.position": {
- requiredVariants: "full",
- label: t("position"),
- default: "bottom-left",
- options: {
- off: t("off"),
- "bottom-left": t("bottom-left"),
- "bottom-right": t("bottom-right")
- }
- },
- "localCoOp.enabled": {
- requiredVariants: "full",
- label: t("enable-local-co-op-support"),
- labelIcon: BxIcon.LOCAL_CO_OP,
- default: !1,
- note: () => CE("div", !1, CE("a", {
- href: "https://github.com/redphx/better-xcloud/discussions/275",
- target: "_blank"
- }, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))
- },
- "ui.controllerStatus.show": {
- label: t("show-controller-connection-status"),
- default: !0
- },
- "deviceVibration.mode": {
- requiredVariants: "full",
- label: t("device-vibration"),
- default: "off",
- options: {
- off: t("off"),
- on: t("on"),
- auto: t("device-vibration-not-using-gamepad")
- }
- },
- "deviceVibration.intensity": {
- requiredVariants: "full",
- label: t("vibration-intensity"),
- default: 50,
- min: 10,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- exactTicks: 20
- }
- },
- "controller.pollingRate": {
- requiredVariants: "full",
- label: t("polling-rate"),
- default: 4,
- min: 4,
- max: 60,
- params: {
- steps: 4,
- exactTicks: 20,
- reverse: !0,
- customTextValue(value) {
- value = parseInt(value);
- let text = +(1000 / value).toFixed(2) + " Hz";
- if (value === 4) text = `${text} (${t("default")})`;
- return text;
- }
- }
- },
- "mkb.enabled": {
- requiredVariants: "full",
- label: t("enable-mkb"),
- default: !1,
- unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,
- ready: (setting) => {
- let note, url;
- if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
- else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
- setting.unsupportedNote = () => CE("a", {
- href: url,
- target: "_blank"
- }, "⚠️ " + note);
- }
- },
- "nativeMkb.mode": {
- requiredVariants: "full",
- label: t("native-mkb"),
- default: "default",
- options: {
- default: t("default"),
- off: t("off"),
- on: t("on")
- },
- ready: (setting) => {
- if (STATES.browser.capabilities.emulatedNativeMkb) ;
- else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];
- else delete setting.options["on"];
- }
- },
- "nativeMkb.forcedGames": {
- label: t("force-native-mkb-games"),
- default: [],
- unsupported: !AppInterface && UserAgent.isMobile(),
- ready: (setting) => {
- if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {
- setting.multipleOptions = payload.data.data;
- });
- },
- params: {
- size: 6
- }
- },
- "nativeMkb.scroll.sensitivityX": {
- requiredVariants: "full",
- label: t("horizontal-scroll-sensitivity"),
- default: 0,
- min: 0,
- max: 1e4,
- params: {
- steps: 10,
- exactTicks: 2000,
- customTextValue: (value) => {
- if (!value) return t("default");
- return (value / 100).toFixed(1) + "x";
- }
- }
- },
- "nativeMkb.scroll.sensitivityY": {
- requiredVariants: "full",
- label: t("vertical-scroll-sensitivity"),
- default: 0,
- min: 0,
- max: 1e4,
- params: {
- steps: 10,
- exactTicks: 2000,
- customTextValue: (value) => {
- if (!value) return t("default");
- return (value / 100).toFixed(1) + "x";
- }
- }
- },
- "mkb.p1.preset.mappingId": {
- requiredVariants: "full",
- default: -1
- },
- "mkb.p1.slot": {
- requiredVariants: "full",
- default: 1,
- min: 1,
- max: 4,
- params: {
- hideSlider: !0
- }
- },
- "mkb.p2.preset.mappingId": {
- requiredVariants: "full",
- default: 0
- },
- "mkb.p2.slot": {
- requiredVariants: "full",
- default: 0,
- min: 0,
- max: 4,
- params: {
- hideSlider: !0,
- customTextValue(value) {
- return value = parseInt(value), value === 0 ? t("off") : value.toString();
- }
- }
- },
- "keyboardShortcuts.preset.inGameId": {
- requiredVariants: "full",
- default: -1
- },
- "ui.reduceAnimations": {
- label: t("reduce-animations"),
- default: !1
- },
- "loadingScreen.gameArt.show": {
- requiredVariants: "full",
- label: t("show-game-art"),
- default: !0
- },
- "loadingScreen.waitTime.show": {
- label: t("show-wait-time"),
- default: !0
- },
- "loadingScreen.rocket": {
- label: t("rocket-animation"),
- default: "show",
- options: {
- show: t("rocket-always-show"),
- "hide-queue": t("rocket-hide-queue"),
- hide: t("rocket-always-hide")
- }
- },
- "ui.controllerFriendly": {
- label: t("controller-friendly-ui"),
- default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"
- },
- "ui.layout": {
- requiredVariants: "full",
- label: t("layout"),
- default: "default",
- options: {
- default: t("default"),
- normal: t("normal"),
- tv: t("smart-tv")
- }
- },
- "ui.hideScrollbar": {
- label: t("hide-scrollbar"),
- default: !1
- },
- "ui.hideSections": {
- requiredVariants: "full",
- label: t("hide-sections"),
- default: [],
- multipleOptions: {
- news: t("section-news"),
- friends: t("section-play-with-friends"),
- "native-mkb": t("section-native-mkb"),
- touch: t("section-touch"),
- "most-popular": t("section-most-popular"),
- "all-games": t("section-all-games")
- },
- params: {
- size: 0
- }
- },
- "ui.gameCard.waitTime.show": {
- requiredVariants: "full",
- label: t("show-wait-time-in-game-card"),
- default: !0
- },
- "block.tracking": {
- label: t("disable-xcloud-analytics"),
- default: !1
- },
- "block.features": {
- requiredVariants: "full",
- label: t("disable-features"),
- default: [],
- multipleOptions: {
- chat: t("chat"),
- friends: t("friends-followers"),
- byog: t("stream-your-own-game"),
- "notifications-invites": t("notifications") + ": " + t("invites"),
- "notifications-achievements": t("notifications") + ": " + t("achievements")
- }
- },
- "userAgent.profile": {
- label: t("user-agent-profile"),
- note: "⚠️ " + t("unexpected-behavior"),
- default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",
- options: {
- default: t("default"),
- "windows-edge": "Edge + Windows",
- "macos-safari": "Safari + macOS",
- "vr-oculus": "Android TV",
- "smarttv-generic": "Smart TV",
- "smarttv-tizen": "Samsung Smart TV",
- custom: t("custom")
- }
- },
- "video.player.type": {
- label: t("renderer"),
- default: "default",
- options: {
- default: t("default"),
- webgl2: t("webgl2")
- },
- suggest: {
- lowest: "default",
- highest: "webgl2"
- }
- },
- "video.processing": {
- label: t("clarity-boost"),
- default: "usm",
- options: {
- usm: t("unsharp-masking"),
- cas: t("amd-fidelity-cas")
- },
- suggest: {
- lowest: "usm",
- highest: "cas"
- }
- },
- "video.player.powerPreference": {
- label: t("renderer-configuration"),
- default: "default",
- options: {
- default: t("default"),
- "low-power": t("battery-saving"),
- "high-performance": t("high-performance")
- },
- suggest: {
- highest: "low-power"
- }
- },
- "video.maxFps": {
- label: t("limit-fps"),
- default: 60,
- min: 10,
- max: 60,
- params: {
- steps: 10,
- exactTicks: 10,
- customTextValue: (value) => {
- return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";
- }
- }
- },
- "video.processing.sharpness": {
- label: t("sharpness"),
- default: 0,
- min: 0,
- max: 10,
- params: {
- exactTicks: 2,
- customTextValue: (value) => {
- return value = parseInt(value), value === 0 ? t("off") : value.toString();
- }
- },
- suggest: {
- lowest: 0,
- highest: 2
- }
- },
- "video.ratio": {
- label: t("aspect-ratio"),
- note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
- default: "16:9",
- options: {
- "16:9": `16:9 (${t("default")})`,
- "18:9": "18:9",
- "21:9": "21:9",
- "16:10": "16:10",
- "4:3": "4:3",
- fill: t("stretch")
- }
- },
- "video.position": {
- label: t("position"),
- note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
- default: "center",
- options: {
- top: t("top"),
- "top-half": t("top-half"),
- center: `${t("center")} (${t("default")})`,
- "bottom-half": t("bottom-half"),
- bottom: t("bottom")
- }
- },
- "video.saturation": {
- label: t("saturation"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "video.contrast": {
- label: t("contrast"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "video.brightness": {
- label: t("brightness"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "audio.mic.onPlaying": {
- label: t("enable-mic-on-startup"),
- default: !1
- },
- "audio.volume.booster.enabled": {
- requiredVariants: "full",
- label: t("enable-volume-control"),
- default: !1
- },
- "audio.volume": {
- label: t("volume"),
- default: 100,
- min: 0,
- max: 600,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 100
- }
- },
- "stats.items": {
- label: t("stats"),
- default: ["ping", "fps", "btr", "dt", "pl", "fl"],
- multipleOptions: {
- time: t("clock"),
- play: t("playtime"),
- batt: t("battery"),
- ping: t("stat-ping"),
- jit: t("jitter"),
- fps: t("stat-fps"),
- btr: t("stat-bitrate"),
- dt: t("stat-decode-time"),
- pl: t("stat-packets-lost"),
- fl: t("stat-frames-lost"),
- dl: t("downloaded"),
- ul: t("uploaded")
- },
- params: {
- size: 0
- },
- ready: (setting) => {
- let multipleOptions = setting.multipleOptions;
- if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];
- for (let key in multipleOptions)
- multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];
- }
- },
- "stats.showWhenPlaying": {
- label: t("show-stats-on-startup"),
- default: !1
- },
- "stats.quickGlance.enabled": {
- label: "👀 " + t("enable-quick-glance-mode"),
- default: !0
- },
- "stats.position": {
- label: t("position"),
- default: "top-right",
- options: {
- "top-left": t("top-left"),
- "top-center": t("top-center"),
- "top-right": t("top-right")
- }
- },
- "stats.textSize": {
- label: t("text-size"),
- default: "0.9rem",
- options: {
- "0.9rem": t("small"),
- "1.0rem": t("normal"),
- "1.1rem": t("large")
- }
- },
- "stats.opacity.all": {
- label: t("opacity"),
- default: 80,
- min: 50,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10
- }
- },
- "stats.opacity.background": {
- label: t("background-opacity"),
- default: 100,
- min: 0,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10
- }
- },
- "stats.colors": {
- label: t("conditional-formatting"),
- default: !1
- },
- "xhome.enabled": {
- requiredVariants: "full",
- label: t("enable-remote-play-feature"),
- default: !1
- },
- "xhome.video.resolution": {
- requiredVariants: "full",
- default: "1080p",
- options: {
- "720p": "720p",
- "1080p": "1080p",
- "1080p-hq": "1080p (HQ)"
- }
- },
- "game.fortnite.forceConsole": {
- requiredVariants: "full",
- label: "🎮 " + t("fortnite-force-console-version"),
- default: !1,
- note: t("fortnite-allow-stw-mode")
- }
- };
- constructor() {
- super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);
- }
+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;
}
-var globalSettings = new GlobalSettingsStorage, getPrefDefinition = globalSettings.getDefinition.bind(globalSettings), getPref = globalSettings.getSetting.bind(globalSettings), setPref = globalSettings.setSetting.bind(globalSettings);
-STORAGE.Global = globalSettings;
-function ceilToNearest(value, interval) {
- return Math.ceil(value / interval) * interval;
+class GlobalSettingsStorage extends BaseSettingsStorage {
+static DEFINITIONS = {
+"version.lastCheck": {
+default: 0
+},
+"version.latest": {
+default: ""
+},
+"version.current": {
+default: ""
+},
+"bx.locale": {
+label: t("language"),
+default: localStorage.getItem("BetterXcloud.Locale") || "en-US",
+options: SUPPORTED_LANGUAGES
+},
+"server.region": {
+label: t("region"),
+note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
+default: "default"
+},
+"server.bypassRestriction": {
+label: t("bypass-region-restriction"),
+note: "⚠️ " + t("use-this-at-your-own-risk"),
+default: "off",
+optionsGroup: t("region"),
+options: Object.assign({
+off: t("off")
+}, BypassServers)
+},
+"stream.locale": {
+label: t("preferred-game-language"),
+default: "default",
+options: {
+default: t("default"),
+"ar-SA": "العربية",
+"bg-BG": "Български",
+"cs-CZ": "čeština",
+"da-DK": "dansk",
+"de-DE": "Deutsch",
+"el-GR": "Ελληνικά",
+"en-GB": "English (UK)",
+"en-US": "English (US)",
+"es-ES": "español (España)",
+"es-MX": "español (Latinoamérica)",
+"fi-FI": "suomi",
+"fr-FR": "français",
+"he-IL": "עברית",
+"hu-HU": "magyar",
+"it-IT": "italiano",
+"ja-JP": "日本語",
+"ko-KR": "한국어",
+"nb-NO": "norsk bokmål",
+"nl-NL": "Nederlands",
+"pl-PL": "polski",
+"pt-BR": "português (Brasil)",
+"pt-PT": "português (Portugal)",
+"ro-RO": "Română",
+"ru-RU": "русский",
+"sk-SK": "slovenčina",
+"sv-SE": "svenska",
+"th-TH": "ไทย",
+"tr-TR": "Türkçe",
+"zh-CN": "中文(简体)",
+"zh-TW": "中文 (繁體)"
}
-function floorToNearest(value, interval) {
- return Math.floor(value / interval) * interval;
+},
+"stream.video.resolution": {
+label: t("target-resolution"),
+default: "auto",
+options: {
+auto: t("default"),
+"720p": "720p",
+"1080p": "1080p",
+"1080p-hq": "1080p (HQ)"
+},
+suggest: {
+lowest: "720p",
+highest: "1080p-hq"
}
-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;
+},
+"stream.video.codecProfile": {
+label: t("visual-quality"),
+default: "default",
+options: getSupportedCodecProfiles(),
+ready: (setting) => {
+let options = setting.options, keys = Object.keys(options);
+if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");
+setting.suggest = {
+lowest: keys.length === 1 ? keys[0] : keys[1],
+highest: keys[keys.length - 1]
+};
}
-function productTitleToSlug(title) {
- return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase();
+},
+"server.ipv6.prefer": {
+label: t("prefer-ipv6-server"),
+default: !1
+},
+"screenshot.applyFilters": {
+requiredVariants: "full",
+label: t("screenshot-apply-filters"),
+default: !1
+},
+"ui.splashVideo.skip": {
+label: t("skip-splash-video"),
+default: !1
+},
+"ui.systemMenu.hideHandle": {
+label: "⣿ " + t("hide-system-menu-icon"),
+default: !1
+},
+"ui.imageQuality": {
+requiredVariants: "full",
+label: t("image-quality"),
+default: 90,
+min: 10,
+max: 90,
+params: {
+steps: 10,
+exactTicks: 20,
+hideSlider: !0,
+customTextValue(value, min, max) {
+if (value === 90) return t("default");
+return value + "%";
}
-function parseDetailsPath(path) {
- let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path);
- if (!matches?.groups) return {};
- let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId;
- return { titleSlug, productId };
}
-function clearAllData() {
- for (let i = 0;i < localStorage.length; i++) {
- let key = localStorage.key(i);
- if (!key) continue;
- if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key);
- }
- try {
- indexedDB.deleteDatabase(LocalDb.DB_NAME);
- } catch (e) {}
- alert(t("clear-data-success"));
+},
+"stream.video.combineAudio": {
+requiredVariants: "full",
+label: t("combine-audio-video-streams"),
+default: !1,
+experimental: !0,
+note: t("combine-audio-video-streams-summary")
+},
+"touchController.mode": {
+requiredVariants: "full",
+label: t("availability"),
+default: "all",
+options: {
+default: t("default"),
+off: t("off"),
+all: t("all-games")
+},
+unsupported: !STATES.userAgent.capabilities.touch,
+unsupportedValue: "default"
+},
+"touchController.autoOff": {
+requiredVariants: "full",
+label: t("tc-auto-off"),
+default: !1,
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.opacity.default": {
+requiredVariants: "full",
+label: t("default-opacity"),
+default: 100,
+min: 10,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10,
+hideSlider: !0
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.style.standard": {
+requiredVariants: "full",
+label: t("tc-standard-layout-style"),
+default: "default",
+options: {
+default: t("default"),
+white: t("tc-all-white"),
+muted: t("tc-muted-colors")
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.style.custom": {
+requiredVariants: "full",
+label: t("tc-custom-layout-style"),
+default: "default",
+options: {
+default: t("default"),
+muted: t("tc-muted-colors")
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"ui.streamMenu.simplify": {
+label: t("simplify-stream-menu"),
+default: !1
+},
+"mkb.cursor.hideIdle": {
+requiredVariants: "full",
+label: t("hide-idle-cursor"),
+default: !1
+},
+"ui.feedbackDialog.disabled": {
+requiredVariants: "full",
+label: t("disable-post-stream-feedback-dialog"),
+default: !1
+},
+"stream.video.maxBitrate": {
+requiredVariants: "full",
+label: t("bitrate-video-maximum"),
+note: "⚠️ " + t("unexpected-behavior"),
+default: 0,
+min: 102400,
+max: 15360000,
+transformValue: {
+get(value) {
+return value === 0 ? this.max : value;
+},
+set(value) {
+return value === this.max ? 0 : value;
}
-function blockAllNotifications() {
- let blockFeatures = getPref("block.features");
- return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value));
+},
+params: {
+steps: 102400,
+exactTicks: 5120000,
+customTextValue: (value, min, max) => {
+if (value = parseInt(value), value === max) return t("unlimited");
+else return (value / 1024000).toFixed(1) + " Mb/s";
}
-class SoundShortcut {
- static adjustGainNodeVolume(amount) {
- if (!getPref("audio.volume.booster.enabled")) return 0;
- let currentValue = getPref("audio.volume"), nearestValue;
- if (amount > 0) nearestValue = ceilToNearest(currentValue, amount);
- else nearestValue = floorToNearest(currentValue, -1 * amount);
- let newValue;
- if (currentValue !== nearestValue) newValue = nearestValue;
- else newValue = currentValue + amount;
- return newValue = 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.volume.booster.enabled") && STATES.currentStream.audioGainNode) {
- let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref("audio.volume"), 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 }), BxEventBus.Stream.emit("speaker.state.changed", {
- state: targetValue === 0 ? 1 : 0
- });
- return;
- }
- let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");
- if ($media) {
- $media.muted = !$media.muted;
- let status = $media.muted ? t("muted") : t("unmuted");
- Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
- state: $media.muted ? 1 : 0
- });
- }
- }
+},
+suggest: {
+highest: 0
}
-class StreamStatsCollector {
- static instance;
- static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
- LOG_TAG = "StreamStatsCollector";
- static INTERVAL_BACKGROUND = 60000;
- calculateGrade(value, grades) {
- return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
- }
- currentStats = {
- ping: {
- current: -1,
- grades: [40, 75, 100],
- toString() {
- return this.current === -1 ? "???" : this.current.toString().padStart(3);
- }
- },
- jit: {
- current: 0,
- grades: [30, 40, 60],
- toString() {
- return `${this.current.toFixed(1)}ms`.padStart(6);
- }
- },
- fps: {
- current: 0,
- toString() {
- let maxFps = getPref("video.maxFps");
- return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString();
- }
- },
- btr: {
- current: 0,
- toString() {
- return `${this.current.toFixed(1)} Mbps`.padStart(9);
- }
- },
- fl: {
- received: 0,
- dropped: 0,
- toString() {
- let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
- return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
- }
- },
- pl: {
- received: 0,
- dropped: 0,
- toString() {
- let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
- return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
- }
- },
- dt: {
- current: 0,
- total: 0,
- grades: [6, 9, 12],
- toString() {
- return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6);
- }
- },
- dl: {
- total: 0,
- toString() {
- return humanFileSize(this.total).padStart(8);
- }
- },
- ul: {
- total: 0,
- toString() {
- return humanFileSize(this.total);
- }
- },
- play: {
- seconds: 0,
- startTime: 0,
- toString() {
- return secondsToHm(this.seconds);
- }
- },
- batt: {
- current: 100,
- start: 100,
- isCharging: !1,
- toString() {
- let text = `${this.current}%`;
- if (this.current !== this.start) {
- let diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : "";
- text += ` (${sign}${diffLevel}%)`;
- }
- return text;
- }
- },
- time: {
- toString() {
- return (new Date()).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- hour12: !1
- });
- }
- }
- };
- lastVideoStat;
- selectedCandidatePairId = null;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- async collect() {
- let stats = await STATES.currentStream.peerConnection?.getStats();
- if (!stats) return;
- if (!this.selectedCandidatePairId) {
- let found = !1;
- stats.forEach((stat) => {
- if (found || stat.type !== "transport") return;
- if (stat = stat, stat.iceState === "connected" && stat.selectedCandidatePairId) this.selectedCandidatePairId = stat.selectedCandidatePairId, found = !0;
- });
- }
- stats.forEach((stat) => {
- if (stat.type === "inbound-rtp" && stat.kind === "video") {
- let fps = this.currentStats["fps"];
- fps.current = stat.framesPerSecond || 0;
- let pl = this.currentStats["pl"];
- pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived;
- let fl = this.currentStats["fl"];
- if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) {
- this.lastVideoStat = stat;
- return;
- }
- let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount;
- if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000;
- let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp;
- btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
- let dt = this.currentStats["dt"];
- dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
- let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
- dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat;
- } else if (this.selectedCandidatePairId && stat.type === "candidate-pair" && stat.id === this.selectedCandidatePairId) {
- let ping = this.currentStats["ping"];
- ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
- let dl = this.currentStats["dl"];
- dl.total = stat.bytesReceived;
- let ul = this.currentStats["ul"];
- ul.total = stat.bytesSent;
- }
- });
- let batteryLevel = 100, isCharging = !1;
- if (STATES.browser.capabilities.batteryApi) try {
- let bm = await navigator.getBattery();
- isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100);
- } catch (e) {}
- let battery = this.currentStats["batt"];
- battery.current = batteryLevel, battery.isCharging = isCharging;
- let playTime = this.currentStats["play"], now = +new Date;
- playTime.seconds = Math.ceil((now - playTime.startTime) / 1000);
- }
- getStat(kind) {
- return this.currentStats[kind];
- }
- reset() {
- let playTime = this.currentStats["play"];
- playTime.seconds = 0, playTime.startTime = +new Date;
- try {
- STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => {
- this.currentStats["batt"].start = Math.round(bm.level * 100);
- });
- } catch (e) {}
- }
- static setupEvents() {
- BxEventBus.Stream.on("state.playing", () => {
- StreamStatsCollector.getInstance().reset();
- });
- }
+},
+"gameBar.position": {
+requiredVariants: "full",
+label: t("position"),
+default: "bottom-left",
+options: {
+off: t("off"),
+"bottom-left": t("bottom-left"),
+"bottom-right": t("bottom-right")
}
-class StreamStats {
- static instance;
- static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
- LOG_TAG = "StreamStats";
- intervalId;
- REFRESH_INTERVAL = 1000;
- stats = {
- time: {
- name: t("clock"),
- $element: CE("span")
- },
- play: {
- name: t("playtime"),
- $element: CE("span")
- },
- batt: {
- name: t("battery"),
- $element: CE("span")
- },
- ping: {
- name: t("stat-ping"),
- $element: CE("span")
- },
- jit: {
- name: t("jitter"),
- $element: CE("span")
- },
- fps: {
- name: t("stat-fps"),
- $element: CE("span")
- },
- btr: {
- name: t("stat-bitrate"),
- $element: CE("span")
- },
- dt: {
- name: t("stat-decode-time"),
- $element: CE("span")
- },
- pl: {
- name: t("stat-packets-lost"),
- $element: CE("span")
- },
- fl: {
- name: t("stat-frames-lost"),
- $element: CE("span")
- },
- dl: {
- name: t("downloaded"),
- $element: CE("span")
- },
- ul: {
- name: t("uploaded"),
- $element: CE("span")
- }
- };
- $container;
- quickGlanceObserver;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.render();
- }
- async start(glancing = !1) {
- if (!this.isHidden() || glancing && this.isGlancing()) return;
- this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
- }
- async stop(glancing = !1) {
- if (glancing && !this.isGlancing()) return;
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");
- }
- async toggle() {
- if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");
- else this.isHidden() ? await this.start() : await this.stop();
- }
- destroy() {
- this.stop(), this.quickGlanceStop(), this.hideSettingsUi();
- }
- isHidden = () => this.$container.classList.contains("bx-gone");
- isGlancing = () => this.$container.dataset.display === "glancing";
- quickGlanceSetup() {
- if (!STATES.isPlaying || this.quickGlanceObserver) return;
- let $uiContainer = document.querySelector("div[data-testid=ui-container]");
- if (!$uiContainer) return;
- this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {
- for (let record of mutationList) {
- let $target = record.target;
- if (!$target.className || !$target.className.startsWith("GripHandle")) continue;
- if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0);
- else this.stop(!0);
- }
- }), this.quickGlanceObserver.observe($uiContainer, {
- attributes: !0,
- attributeFilter: ["aria-expanded"],
- subtree: !0
- });
- }
- quickGlanceStop() {
- this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null;
- }
- update = async (forceUpdate = !1) => {
- if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {
- this.destroy();
- return;
- }
- let PREF_STATS_CONDITIONAL_FORMATTING = getPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();
- await statsCollector.collect();
- let statKey;
- for (statKey in this.stats) {
- grade = "";
- let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;
- if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);
- if ($element.dataset.grade !== grade) $element.dataset.grade = grade;
- }
- };
- refreshStyles() {
- let PREF_ITEMS = getPref("stats.items"), PREF_OPACITY_BG = getPref("stats.opacity.background"), $container = this.$container;
- if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";
- else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
- $container.style.opacity = getPref("stats.opacity.all") + "%", $container.style.fontSize = getPref("stats.textSize");
- }
- hideSettingsUi() {
- if (this.isGlancing() && !getPref("stats.quickGlance.enabled")) this.stop();
- }
- async render() {
- this.$container = CE("div", { class: "bx-stats-bar bx-gone" });
- let statKey;
- for (statKey in this.stats) {
- let stat = this.stats[statKey], $div = CE("div", {
- class: `bx-stat-${statKey}`,
- title: stat.name
- }, CE("label", !1, statKey.toUpperCase()), stat.$element);
- this.$container.appendChild($div);
- }
- this.refreshStyles(), document.documentElement.appendChild(this.$container);
- }
- static setupEvents() {
- BxEventBus.Stream.on("state.playing", () => {
- let PREF_STATS_QUICK_GLANCE = getPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();
- if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();
- else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);
- });
- }
- static refreshStyles() {
- StreamStats.getInstance().refreshStyles();
- }
+},
+"ui.controllerStatus.show": {
+label: t("show-controller-connection-status"),
+default: !0
+},
+"mkb.enabled": {
+requiredVariants: "full",
+label: t("enable-mkb"),
+default: !1,
+unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,
+ready: (setting) => {
+let note, url;
+if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
+else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
+setting.unsupportedNote = () => CE("a", {
+href: url,
+target: "_blank"
+}, "⚠️ " + note);
}
-function onChangeVideoPlayerType() {
- let playerType = getPref("video.player.type"), $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector("video.processing")}`), $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector("video.processing.sharpness")}`), $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector("video.player.powerPreference")}`), $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector("video.maxFps")}`);
- if (!$videoProcessing) return;
- let isDisabled = !1, $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();
+},
+"nativeMkb.mode": {
+requiredVariants: "full",
+label: t("native-mkb"),
+default: "default",
+options: {
+default: t("default"),
+off: t("off"),
+on: t("on")
+},
+ready: (setting) => {
+if (STATES.browser.capabilities.emulatedNativeMkb) ;
+else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];
+else delete setting.options["on"];
}
-function limitVideoPlayerFps(targetFps) {
- STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
+},
+"nativeMkb.forcedGames": {
+label: t("force-native-mkb-games"),
+default: [],
+unsupported: !AppInterface && UserAgent.isMobile(),
+ready: (setting) => {
+if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {
+setting.multipleOptions = payload.data.data;
+});
+},
+params: {
+size: 6
}
-function updateVideoPlayer() {
- let streamPlayer = STATES.currentStream.streamPlayer;
- if (!streamPlayer) return;
- limitVideoPlayerFps(getPref("video.maxFps"));
- let options = {
- processing: getPref("video.processing"),
- sharpness: getPref("video.processing.sharpness"),
- saturation: getPref("video.saturation"),
- contrast: getPref("video.contrast"),
- brightness: getPref("video.brightness")
- };
- streamPlayer.setPlayerType(getPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
+},
+"ui.reduceAnimations": {
+label: t("reduce-animations"),
+default: !1
+},
+"loadingScreen.gameArt.show": {
+requiredVariants: "full",
+label: t("show-game-art"),
+default: !0
+},
+"loadingScreen.waitTime.show": {
+label: t("show-wait-time"),
+default: !0
+},
+"loadingScreen.rocket": {
+label: t("rocket-animation"),
+default: "show",
+options: {
+show: t("rocket-always-show"),
+"hide-queue": t("rocket-hide-queue"),
+hide: t("rocket-always-hide")
}
-window.addEventListener("resize", updateVideoPlayer);
-class KeyHelper {
- static NON_PRINTABLE_KEYS = {
- Backquote: "`",
- Minus: "-",
- Equal: "=",
- BracketLeft: "[",
- BracketRight: "]",
- Backslash: "\\",
- Semicolon: ";",
- Quote: "'",
- Comma: ",",
- Period: ".",
- Slash: "/",
- NumpadMultiply: "Numpad *",
- NumpadAdd: "Numpad +",
- NumpadSubtract: "Numpad -",
- NumpadDecimal: "Numpad .",
- NumpadDivide: "Numpad /",
- NumpadEqual: "Numpad =",
- Mouse0: "Left Click",
- Mouse2: "Right Click",
- Mouse1: "Middle Click",
- ScrollUp: "Scroll Up",
- ScrollDown: "Scroll Down",
- ScrollLeft: "Scroll Left",
- ScrollRight: "Scroll Right"
- };
- static getKeyFromEvent(e) {
- let code = null, modifiers;
- if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0;
- else if (e instanceof WheelEvent) {
- if (e.deltaY < 0) code = "ScrollUp";
- else if (e.deltaY > 0) code = "ScrollDown";
- else if (e.deltaX < 0) code = "ScrollLeft";
- else if (e.deltaX > 0) code = "ScrollRight";
- } else if (e instanceof MouseEvent) code = "Mouse" + e.button;
- if (code) {
- let results = { code };
- if (modifiers) results.modifiers = modifiers;
- return results;
- }
- return null;
- }
- static getFullKeyCodeFromEvent(e) {
- let key = KeyHelper.getKeyFromEvent(e);
- return key ? `${key.code}:${key.modifiers || 0}` : "";
- }
- static parseFullKeyCode(str) {
- if (!str) return null;
- let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]);
- return {
- code,
- modifiers
- };
- }
- static codeToKeyName(key) {
- let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code];
- if (modifiers && modifiers !== 0) {
- if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) {
- if (modifiers & 2) text.unshift("Shift");
- if (modifiers & 4) text.unshift("Alt");
- if (modifiers & 1) text.unshift("Ctrl");
- }
- }
- return text.join(" + ");
- }
+},
+"ui.controllerFriendly": {
+label: t("controller-friendly-ui"),
+default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"
+},
+"ui.layout": {
+requiredVariants: "full",
+label: t("layout"),
+default: "default",
+options: {
+default: t("default"),
+normal: t("normal"),
+tv: t("smart-tv")
}
-class PointerClient {
- static instance;
- static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
- LOG_TAG = "PointerClient";
- REQUIRED_PROTOCOL_VERSION = 2;
- socket;
- mkbHandler;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- start(port, mkbHandler) {
- if (!port) throw new Error("PointerServer port is 0");
- this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {
- BxLogger.info(this.LOG_TAG, "connected");
- }), this.socket.addEventListener("error", (event) => {
- BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
- }), this.socket.addEventListener("close", (event) => {
- this.socket = null;
- }), this.socket.addEventListener("message", (event) => {
- let dataView = new DataView(event.data), messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;
- switch (messageType) {
- case 127:
- let protocolVersion = this.onProtocolVersion(dataView, offset);
- if (BxLogger.info(this.LOG_TAG, "Protocol version", protocolVersion), protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) alert("Required MKB protocol: " + protocolVersion), this.stop();
- break;
- case 1:
- this.onMove(dataView, offset);
- break;
- case 2:
- case 3:
- this.onPress(messageType, dataView, offset);
- break;
- case 4:
- this.onScroll(dataView, offset);
- break;
- case 5:
- this.onPointerCaptureChanged(dataView, offset);
- }
- });
- }
- onProtocolVersion(dataView, offset) {
- return dataView.getUint16(offset);
- }
- onMove(dataView, offset) {
- let x = dataView.getInt16(offset);
- offset += Int16Array.BYTES_PER_ELEMENT;
- let y = dataView.getInt16(offset);
- this.mkbHandler?.handleMouseMove({
- movementX: x,
- movementY: y
- });
- }
- onPress(messageType, dataView, offset) {
- let button = dataView.getUint8(offset);
- this.mkbHandler?.handleMouseClick({
- pointerButton: button,
- pressed: messageType === 2
- });
- }
- onScroll(dataView, offset) {
- let vScroll = dataView.getInt16(offset);
- offset += Int16Array.BYTES_PER_ELEMENT;
- let hScroll = dataView.getInt16(offset);
- this.mkbHandler?.handleMouseWheel({
- vertical: vScroll,
- horizontal: hScroll
- });
- }
- onPointerCaptureChanged(dataView, offset) {
- dataView.getInt8(offset) !== 1 && this.mkbHandler?.stop();
- }
- stop() {
- try {
- this.socket?.close();
- } catch (e) {}
- this.socket = null;
- }
+},
+"ui.hideScrollbar": {
+label: t("hide-scrollbar"),
+default: !1
+},
+"ui.hideSections": {
+requiredVariants: "full",
+label: t("hide-sections"),
+default: [],
+multipleOptions: {
+news: t("section-news"),
+friends: t("section-play-with-friends"),
+"native-mkb": t("section-native-mkb"),
+touch: t("section-touch"),
+"most-popular": t("section-most-popular"),
+"all-games": t("section-all-games")
+},
+params: {
+size: 0
}
-class MouseDataProvider {
- mkbHandler;
- constructor(handler) {
- this.mkbHandler = handler;
- }
- init() {}
- destroy() {}
+},
+"ui.gameCard.waitTime.show": {
+requiredVariants: "full",
+label: t("show-wait-time-in-game-card"),
+default: !0
+},
+"block.tracking": {
+label: t("disable-xcloud-analytics"),
+default: !1
+},
+"block.features": {
+requiredVariants: "full",
+label: t("disable-features"),
+default: [],
+multipleOptions: {
+chat: t("chat"),
+friends: t("friends-followers"),
+byog: t("stream-your-own-game"),
+"notifications-invites": t("notifications") + ": " + t("invites"),
+"notifications-achievements": t("notifications") + ": " + t("achievements")
+}
+},
+"userAgent.profile": {
+label: t("user-agent-profile"),
+note: "⚠️ " + t("unexpected-behavior"),
+default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",
+options: {
+default: t("default"),
+"windows-edge": "Edge + Windows",
+"macos-safari": "Safari + macOS",
+"vr-oculus": "Android TV",
+"smarttv-generic": "Smart TV",
+"smarttv-tizen": "Samsung Smart TV",
+custom: t("custom")
+}
+},
+"audio.mic.onPlaying": {
+label: t("enable-mic-on-startup"),
+default: !1
+},
+"audio.volume.booster.enabled": {
+requiredVariants: "full",
+label: t("enable-volume-control"),
+default: !1
+},
+"xhome.enabled": {
+requiredVariants: "full",
+label: t("enable-remote-play-feature"),
+labelIcon: BxIcon.REMOTE_PLAY,
+default: !1
+},
+"xhome.video.resolution": {
+requiredVariants: "full",
+default: "1080p",
+options: {
+"720p": "720p",
+"1080p": "1080p",
+"1080p-hq": "1080p (HQ)"
+}
+},
+"game.fortnite.forceConsole": {
+requiredVariants: "full",
+label: "🎮 " + t("fortnite-force-console-version"),
+default: !1,
+note: t("fortnite-allow-stw-mode")
+}
+};
+constructor() {
+super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);
+}
+}
+class BaseLocalTable {
+tableName;
+constructor(tableName) {
+this.tableName = tableName;
+}
+async prepareTable(type = "readonly") {
+return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);
+}
+call(method) {
+return new Promise((resolve) => {
+let request = method.call(null, ...Array.from(arguments).slice(1));
+request.onsuccess = (e) => {
+resolve(e.target.result);
+};
+});
+}
+async count() {
+let table = await this.prepareTable();
+return this.call(table.count.bind(table));
+}
+async add(data) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.add.bind(table), ...arguments);
+}
+async put(data) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.put.bind(table), ...arguments);
+}
+async delete(id) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.delete.bind(table), ...arguments);
+}
+async get(id) {
+let table = await this.prepareTable();
+return this.call(table.get.bind(table), ...arguments);
+}
+async getAll() {
+let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};
+return all.forEach((item) => {
+results[item.id] = item;
+}), results;
+}
+}
+class BasePresetsTable extends BaseLocalTable {
+async newPreset(name, data) {
+let newRecord = { name, data };
+return await this.add(newRecord);
+}
+async updatePreset(preset) {
+return await this.put(preset);
+}
+async deletePreset(id) {
+return this.delete(id);
+}
+async getPreset(id) {
+if (id === 0) return null;
+if (id < 0) return this.DEFAULT_PRESETS[id];
+let preset = await this.get(id);
+if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];
+return preset;
+}
+async getPresets() {
+let all = deepClone(this.DEFAULT_PRESETS), presets = {
+default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)),
+custom: [],
+data: {}
+};
+if (await this.count() > 0) {
+let items = await this.getAll(), id;
+for (id in items) {
+let item = items[id];
+presets.custom.push(item.id), all[item.id] = item;
+}
+}
+return presets.data = all, presets;
+}
+async getPresetsData() {
+let presetsData = {};
+for (let id in this.DEFAULT_PRESETS) {
+let preset = this.DEFAULT_PRESETS[id];
+presetsData[id] = deepClone(preset.data);
+}
+if (await this.count() > 0) {
+let items = await this.getAll(), id;
+for (id in items) {
+let item = items[id];
+presetsData[item.id] = item.data;
+}
+}
+return presetsData;
+}
+}
+class KeyboardShortcutsTable extends BasePresetsTable {
+static instance;
+static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);
+LOG_TAG = "KeyboardShortcutsTable";
+TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: t("standard"),
+data: {
+mapping: {
+"mkb.toggle": {
+code: "F8"
+},
+"stream.screenshot.capture": {
+code: "Slash"
+}
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+}
+class MkbMappingPresetsTable extends BasePresetsTable {
+static instance;
+static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable);
+LOG_TAG = "MkbMappingPresetsTable";
+TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: t("standard"),
+data: {
+mapping: {
+16: ["Backquote"],
+12: ["ArrowUp", "Digit1"],
+13: ["ArrowDown", "Digit2"],
+14: ["ArrowLeft", "Digit3"],
+15: ["ArrowRight", "Digit4"],
+100: ["KeyW"],
+101: ["KeyS"],
+102: ["KeyA"],
+103: ["KeyD"],
+200: ["KeyU"],
+201: ["KeyJ"],
+202: ["KeyH"],
+203: ["KeyK"],
+0: ["Space", "KeyE"],
+2: ["KeyR"],
+1: ["KeyC", "Backspace"],
+3: ["KeyV"],
+9: ["Enter"],
+8: ["Tab"],
+4: ["KeyQ"],
+5: ["KeyF"],
+7: ["Mouse0"],
+6: ["Mouse2"],
+10: ["KeyX"],
+11: ["KeyZ"]
+},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+}
+},
+[-2]: {
+id: -2,
+name: "Shooter",
+data: {
+mapping: {
+16: ["Backquote"],
+12: ["ArrowUp"],
+13: ["ArrowDown"],
+14: ["ArrowLeft"],
+15: ["ArrowRight"],
+100: ["KeyW"],
+101: ["KeyS"],
+102: ["KeyA"],
+103: ["KeyD"],
+200: ["KeyI"],
+201: ["KeyK"],
+202: ["KeyJ"],
+203: ["KeyL"],
+0: ["Space", "KeyE"],
+2: ["KeyR"],
+1: ["ControlLeft", "Backspace"],
+3: ["KeyV"],
+9: ["Enter"],
+8: ["Tab"],
+4: ["KeyC", "KeyG"],
+5: ["KeyQ"],
+7: ["Mouse0"],
+6: ["Mouse2"],
+10: ["ShiftLeft"],
+11: ["KeyF"]
+},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+}
+class GameSettingsStorage extends BaseSettingsStorage {
+constructor(id) {
+super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS);
+}
+deleteSetting(pref) {
+if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0;
+return !1;
}
-class MkbHandler {}
-class ControllerShortcutsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);
- LOG_TAG = "ControllerShortcutsTable";
- TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: "Type A",
- data: {
- mapping: {
- 3: AppInterface ? "device.volume.inc" : "stream.volume.inc",
- 0: AppInterface ? "device.volume.dec" : "stream.volume.dec",
- 2: "stream.stats.toggle",
- 1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
- 5: "stream.screenshot.capture",
- 9: "stream.menu.show"
- }
- }
- },
- [-2]: {
- id: -2,
- name: "Type B",
- data: {
- mapping: {
- 12: AppInterface ? "device.volume.inc" : "stream.volume.inc",
- 13: AppInterface ? "device.volume.dec" : "stream.volume.dec",
- 15: "stream.stats.toggle",
- 14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
- 4: "stream.screenshot.capture",
- 8: "stream.menu.show"
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {}
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
}
class ControllerCustomizationsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));
- TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: "ABXY ⇄ BAYX",
- data: {
- mapping: {
- 0: 1,
- 1: 0,
- 2: 3,
- 3: 2
- },
- settings: {
- leftStickDeadzone: [0, 100],
- rightStickDeadzone: [0, 100],
- leftTriggerRange: [0, 100],
- rightTriggerRange: [0, 100],
- vibrationIntensity: 100
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {},
- settings: {
- leftTriggerRange: [0, 100],
- rightTriggerRange: [0, 100],
- leftStickDeadzone: [0, 100],
- rightStickDeadzone: [0, 100],
- vibrationIntensity: 100
- }
- };
- DEFAULT_PRESET_ID = 0;
+static instance;
+static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));
+TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: "ABXY ⇄ BAYX",
+data: {
+mapping: {
+0: 1,
+1: 0,
+2: 3,
+3: 2
+},
+settings: {
+leftStickDeadzone: [0, 100],
+rightStickDeadzone: [0, 100],
+leftTriggerRange: [0, 100],
+rightTriggerRange: [0, 100],
+vibrationIntensity: 100
}
-class ControllerSettingsTable extends BaseLocalTable {
- static instance;
- static getInstance = () => ControllerSettingsTable.instance ?? (ControllerSettingsTable.instance = new ControllerSettingsTable(LocalDb.TABLE_CONTROLLER_SETTINGS));
- static DEFAULT_DATA = {
- shortcutPresetId: -1,
- customizationPresetId: 0
- };
- async getControllerData(id) {
- let setting = await this.get(id);
- if (!setting) return deepClone(ControllerSettingsTable.DEFAULT_DATA);
- return setting.data;
- }
- async getControllersData() {
- let all = await this.getAll(), results = {};
- for (let key in all) {
- if (!all[key]) continue;
- let settings = Object.assign(all[key].data, ControllerSettingsTable.DEFAULT_DATA);
- results[key] = settings;
- }
- return results;
- }
}
-function showGamepadToast(gamepad) {
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) return;
- BxLogger.info("Gamepad", gamepad);
- let text = "🎮";
- if (getPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`;
- let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");
- text += ` - ${gamepadId}`;
- let status;
- if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");
- else status = t("disconnected");
- Toast.show(text, status, { instant: !1 });
}
-function hasGamepad() {
- let gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads)
- if (gamepad?.connected) return !0;
- return !1;
-}
-function generateVirtualControllerMapping(index, override = {}) {
- return Object.assign({}, {
- GamepadIndex: index,
- A: 0,
- B: 0,
- X: 0,
- Y: 0,
- LeftShoulder: 0,
- RightShoulder: 0,
- LeftTrigger: 0,
- RightTrigger: 0,
- View: 0,
- Menu: 0,
- LeftThumb: 0,
- RightThumb: 0,
- DPadUp: 0,
- DPadDown: 0,
- DPadLeft: 0,
- DPadRight: 0,
- Nexus: 0,
- LeftThumbXAxis: 0,
- LeftThumbYAxis: 0,
- RightThumbXAxis: 0,
- RightThumbYAxis: 0,
- PhysicalPhysicality: 0,
- VirtualPhysicality: 0,
- Dirty: !1,
- Virtual: !1
- }, override);
-}
-var XCLOUD_GAMEPAD_KEY_MAPPING = {
- 0: "A",
- 1: "B",
- 2: "X",
- 3: "Y",
- 12: "DPadUp",
- 15: "DPadRight",
- 13: "DPadDown",
- 14: "DPadLeft",
- 4: "LeftShoulder",
- 5: "RightShoulder",
- 6: "LeftTrigger",
- 7: "RightTrigger",
- 10: "LeftThumb",
- 11: "RightThumb",
- 104: "LeftStickAxes",
- 204: "RightStickAxes",
- 8: "View",
- 9: "Menu",
- 16: "Nexus",
- 17: "Share",
- 102: "LeftThumbXAxis",
- 103: "LeftThumbXAxis",
- 100: "LeftThumbYAxis",
- 101: "LeftThumbYAxis",
- 202: "RightThumbXAxis",
- 203: "RightThumbXAxis",
- 200: "RightThumbYAxis",
- 201: "RightThumbYAxis"
};
-function toXcloudGamepadKey(gamepadKey) {
- return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
+BLANK_PRESET_DATA = {
+mapping: {},
+settings: {
+leftTriggerRange: [0, 100],
+rightTriggerRange: [0, 100],
+leftStickDeadzone: [0, 100],
+rightStickDeadzone: [0, 100],
+vibrationIntensity: 100
}
-class StreamSettings {
- static settings = {
- settings: {},
- xCloudPollingMode: "all",
- deviceVibrationIntensity: 0,
- controllerPollingRate: 4,
- controllers: {},
- mkbPreset: null,
- keyboardShortcuts: {}
- };
- static getPref(key) {
- return getPref(key);
- }
- static async refreshControllerSettings() {
- let settings = StreamSettings.settings, controllers = {}, settingsTable = ControllerSettingsTable.getInstance(), shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads) {
- if (!gamepad?.connected) continue;
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
- let settingsData = await settingsTable.getControllerData(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(settingsData.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(settingsData.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data);
- controllers[gamepad.id] = {
- shortcuts: shortcutsMapping,
- customization: customizationData
- };
- }
- settings.controllers = controllers, settings.controllerPollingRate = StreamSettings.getPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration();
- }
- static preCalculateControllerRange(obj, target, values) {
- if (values && Array.isArray(values)) {
- let [from, to] = values;
- if (from > 1 || to < 100) obj[target] = [from / 100, to / 100];
- }
- }
- static convertControllerCustomization(customization) {
- if (!customization) return null;
- let converted = {
- mapping: {},
- ranges: {},
- vibrationIntensity: 1
- }, gamepadKey;
- for (gamepadKey in customization.mapping) {
- let gamepadStr = toXcloudGamepadKey(gamepadKey);
- if (!gamepadStr) continue;
- let mappedKey = customization.mapping[gamepadKey];
- if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey);
- else converted.mapping[gamepadStr] = !1;
- }
- return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted;
- }
- static async refreshDeviceVibration() {
- if (!STATES.browser.capabilities.deviceVibration) return;
- let mode = StreamSettings.getPref("deviceVibration.mode"), intensity = 0;
- if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = StreamSettings.getPref("deviceVibration.intensity") / 100;
- StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Script.emit("deviceVibration.updated", {});
- }
- static async refreshMkbSettings() {
- let settings = StreamSettings.settings, presetId = StreamSettings.getPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = {
- mapping: {},
- mouse: Object.assign({}, orgPresetData.mouse)
- }, key;
- for (key in orgPresetData.mapping) {
- let buttonIndex = parseInt(key);
- if (!orgPresetData.mapping[buttonIndex]) continue;
- for (let keyName of orgPresetData.mapping[buttonIndex])
- if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex;
- }
- let mouse = converted.mouse;
- mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setPref("mkb.p1.preset.mappingId", orgPreset.id), BxEventBus.Script.emit("mkb.setting.updated", {});
- }
- static async refreshKeyboardShortcuts() {
- let settings = StreamSettings.settings, presetId = StreamSettings.getPref("keyboardShortcuts.preset.inGameId");
- if (presetId === 0) {
- settings.keyboardShortcuts = null, setPref("keyboardShortcuts.preset.inGameId", presetId), BxEventBus.Script.emit("keyboardShortcuts.updated", {});
- return;
- }
- let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action;
- for (action in orgPresetData) {
- let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`;
- converted[key] = action;
- }
- settings.keyboardShortcuts = converted, setPref("keyboardShortcuts.preset.inGameId", orgPreset.id), BxEventBus.Script.emit("keyboardShortcuts.updated", {});
- }
- static async refreshAllSettings() {
- window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts();
- }
- static findKeyboardShortcut(targetAction) {
- let shortcuts = StreamSettings.settings.keyboardShortcuts;
- for (let codeStr in shortcuts)
- if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr);
- return null;
- }
- static setup() {
- let listener = () => {
- StreamSettings.refreshControllerSettings();
- };
- window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings();
- }
+};
+DEFAULT_PRESET_ID = 0;
}
+class ControllerShortcutsTable extends BasePresetsTable {
+static instance;
+static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);
+LOG_TAG = "ControllerShortcutsTable";
+TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: "Type A",
+data: {
+mapping: {
+3: AppInterface ? "device.volume.inc" : "stream.volume.inc",
+0: AppInterface ? "device.volume.dec" : "stream.volume.dec",
+2: "stream.stats.toggle",
+1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
+5: "stream.screenshot.capture",
+9: "stream.menu.show"
+}
+}
+},
+[-2]: {
+id: -2,
+name: "Type B",
+data: {
+mapping: {
+12: AppInterface ? "device.volume.inc" : "stream.volume.inc",
+13: AppInterface ? "device.volume.dec" : "stream.volume.dec",
+15: "stream.stats.toggle",
+14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
+4: "stream.screenshot.capture",
+8: "stream.menu.show"
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+}
+class StreamSettingsStorage extends BaseSettingsStorage {
+static DEFINITIONS = {
+"deviceVibration.mode": {
+requiredVariants: "full",
+label: t("device-vibration"),
+default: "off",
+options: {
+off: t("off"),
+on: t("on"),
+auto: t("device-vibration-not-using-gamepad")
+}
+},
+"deviceVibration.intensity": {
+requiredVariants: "full",
+label: t("vibration-intensity"),
+default: 50,
+min: 10,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+exactTicks: 20
+}
+},
+"controller.pollingRate": {
+requiredVariants: "full",
+label: t("polling-rate"),
+default: 4,
+min: 4,
+max: 60,
+params: {
+steps: 4,
+exactTicks: 20,
+reverse: !0,
+customTextValue(value) {
+value = parseInt(value);
+let text = +(1000 / value).toFixed(2) + " Hz";
+if (value === 4) text = `${text} (${t("default")})`;
+return text;
+}
+}
+},
+"controller.settings": {
+default: {}
+},
+"nativeMkb.scroll.sensitivityX": {
+requiredVariants: "full",
+label: t("horizontal-scroll-sensitivity"),
+default: 0,
+min: 0,
+max: 1e4,
+params: {
+steps: 10,
+exactTicks: 2000,
+customTextValue: (value) => {
+if (!value) return t("default");
+return (value / 100).toFixed(1) + "x";
+}
+}
+},
+"nativeMkb.scroll.sensitivityY": {
+requiredVariants: "full",
+label: t("vertical-scroll-sensitivity"),
+default: 0,
+min: 0,
+max: 1e4,
+params: {
+steps: 10,
+exactTicks: 2000,
+customTextValue: (value) => {
+if (!value) return t("default");
+return (value / 100).toFixed(1) + "x";
+}
+}
+},
+"mkb.p1.preset.mappingId": {
+requiredVariants: "full",
+default: -1
+},
+"mkb.p1.slot": {
+requiredVariants: "full",
+default: 1,
+min: 1,
+max: 4,
+params: {
+hideSlider: !0
+}
+},
+"mkb.p2.preset.mappingId": {
+requiredVariants: "full",
+default: 0
+},
+"mkb.p2.slot": {
+requiredVariants: "full",
+default: 0,
+min: 0,
+max: 4,
+params: {
+hideSlider: !0,
+customTextValue(value) {
+return value = parseInt(value), value === 0 ? t("off") : value.toString();
+}
+}
+},
+"keyboardShortcuts.preset.inGameId": {
+requiredVariants: "full",
+default: -1
+},
+"video.player.type": {
+label: t("renderer"),
+default: "default",
+options: {
+default: t("default"),
+webgl2: t("webgl2")
+},
+suggest: {
+lowest: "default",
+highest: "webgl2"
+}
+},
+"video.processing": {
+label: t("clarity-boost"),
+default: "usm",
+options: {
+usm: t("unsharp-masking"),
+cas: t("amd-fidelity-cas")
+},
+suggest: {
+lowest: "usm",
+highest: "cas"
+}
+},
+"video.player.powerPreference": {
+label: t("renderer-configuration"),
+default: "default",
+options: {
+default: t("default"),
+"low-power": t("battery-saving"),
+"high-performance": t("high-performance")
+},
+suggest: {
+highest: "low-power"
+}
+},
+"video.maxFps": {
+label: t("limit-fps"),
+default: 60,
+min: 10,
+max: 60,
+params: {
+steps: 10,
+exactTicks: 10,
+customTextValue: (value) => {
+return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";
+}
+}
+},
+"video.processing.sharpness": {
+label: t("sharpness"),
+default: 0,
+min: 0,
+max: 10,
+params: {
+exactTicks: 2,
+customTextValue: (value) => {
+return value = parseInt(value), value === 0 ? t("off") : value.toString();
+}
+},
+suggest: {
+lowest: 0,
+highest: 2
+}
+},
+"video.ratio": {
+label: t("aspect-ratio"),
+note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
+default: "16:9",
+options: {
+"16:9": `16:9 (${t("default")})`,
+"18:9": "18:9",
+"21:9": "21:9",
+"16:10": "16:10",
+"4:3": "4:3",
+fill: t("stretch")
+}
+},
+"video.position": {
+label: t("position"),
+note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
+default: "center",
+options: {
+top: t("top"),
+"top-half": t("top-half"),
+center: `${t("center")} (${t("default")})`,
+"bottom-half": t("bottom-half"),
+bottom: t("bottom")
+}
+},
+"video.saturation": {
+label: t("saturation"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"video.contrast": {
+label: t("contrast"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"video.brightness": {
+label: t("brightness"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"audio.volume": {
+label: t("volume"),
+default: 100,
+min: 0,
+max: 600,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 100
+}
+},
+"stats.items": {
+label: t("stats"),
+default: ["ping", "fps", "btr", "dt", "pl", "fl"],
+multipleOptions: {
+time: t("clock"),
+play: t("playtime"),
+batt: t("battery"),
+ping: t("stat-ping"),
+jit: t("jitter"),
+fps: t("stat-fps"),
+btr: t("stat-bitrate"),
+dt: t("stat-decode-time"),
+pl: t("stat-packets-lost"),
+fl: t("stat-frames-lost"),
+dl: t("downloaded"),
+ul: t("uploaded")
+},
+params: {
+size: 0
+},
+ready: (setting) => {
+let multipleOptions = setting.multipleOptions;
+if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];
+for (let key in multipleOptions)
+multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];
+}
+},
+"stats.showWhenPlaying": {
+label: t("show-stats-on-startup"),
+default: !1
+},
+"stats.quickGlance.enabled": {
+label: "👀 " + t("enable-quick-glance-mode"),
+default: !0
+},
+"stats.position": {
+label: t("position"),
+default: "top-right",
+options: {
+"top-left": t("top-left"),
+"top-center": t("top-center"),
+"top-right": t("top-right")
+}
+},
+"stats.textSize": {
+label: t("text-size"),
+default: "0.9rem",
+options: {
+"0.9rem": t("small"),
+"1.0rem": t("normal"),
+"1.1rem": t("large")
+}
+},
+"stats.opacity.all": {
+label: t("opacity"),
+default: 80,
+min: 50,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10
+}
+},
+"stats.opacity.background": {
+label: t("background-opacity"),
+default: 100,
+min: 0,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10
+}
+},
+"stats.colors": {
+label: t("conditional-formatting"),
+default: !1
+},
+"localCoOp.enabled": {
+requiredVariants: "full",
+label: t("enable-local-co-op-support"),
+labelIcon: BxIcon.LOCAL_CO_OP,
+default: !1,
+note: () => CE("div", !1, CE("a", {
+href: "https://github.com/redphx/better-xcloud/discussions/275",
+target: "_blank"
+}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))
+}
+};
+gameSettings = {};
+xboxTitleId = -1;
+constructor() {
+super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);
+}
+setGameId(id) {
+this.xboxTitleId = id;
+}
+getGameSettings(id) {
+if (id > -1) {
+if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id);
+return this.gameSettings[id];
+}
+return null;
+}
+getSetting(key, checkUnsupported) {
+return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported);
+}
+getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) {
+let gameSettings = this.getGameSettings(id);
+if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported);
+if (returnBaseValue) return super.getSetting(key, checkUnsupported);
+return;
+}
+setSetting(key, value, origin) {
+return this.setSettingByGame(this.xboxTitleId, key, value, origin);
+}
+setSettingByGame(id, key, value, origin) {
+let gameSettings = this.getGameSettings(id);
+if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);
+return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);
+}
+hasGameSetting(id, key) {
+let gameSettings = this.getGameSettings(id);
+return !!(gameSettings && gameSettings.hasSetting(key));
+}
+getControllerSetting(gamepadId) {
+let controllerSetting = this.getSetting("controller.settings")[gamepadId];
+if (!controllerSetting) controllerSetting = {};
+if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;
+if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;
+return controllerSetting;
+}
+}
+var STORAGE = {
+Global: new GlobalSettingsStorage,
+Stream: new StreamSettingsStorage
+}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage);
+STORAGE.Stream = streamSettingsStorage;
+var globalSettingsStorage = STORAGE.Global, getGlobalPrefDefinition = globalSettingsStorage.getDefinition.bind(globalSettingsStorage), getGlobalPref = globalSettingsStorage.getSetting.bind(globalSettingsStorage), setGlobalPref = globalSettingsStorage.setSetting.bind(globalSettingsStorage);
+function isGlobalPref(prefKey) {
+return ALL_PREFS.global.includes(prefKey);
+}
+function isStreamPref(prefKey) {
+return ALL_PREFS.stream.includes(prefKey);
+}
+function getPrefInfo(prefKey) {
+if (isGlobalPref(prefKey)) return {
+storage: STORAGE.Global,
+definition: getGlobalPrefDefinition(prefKey)
+};
+else if (isStreamPref(prefKey)) return {
+storage: STORAGE.Stream,
+definition: getStreamPrefDefinition(prefKey)
+};
+return alert("Missing pref definition: " + prefKey), {};
+}
+function setPref(prefKey, value, origin) {
+if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin);
+else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin);
+}
+function ceilToNearest(value, interval) {
+return Math.ceil(value / interval) * interval;
+}
+function floorToNearest(value, interval) {
+return Math.floor(value / interval) * interval;
+}
+async function copyToClipboard(text, showToast = !0) {
+try {
+return await navigator.clipboard.writeText(text), showToast && Toast.show("Copied to clipboard", "", { instant: !0 }), !0;
+} catch (err) {
+console.error("Failed to copy: ", err), showToast && Toast.show("Failed to copy", "", { instant: !0 });
+}
+return !1;
+}
+function productTitleToSlug(title) {
+return title.replace(/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g, "").replace(/\|/g, "-").replace(/ {2,}/g, " ").trim().substr(0, 50).replace(/ /g, "-").toLowerCase();
+}
+function parseDetailsPath(path) {
+let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path);
+if (!matches?.groups) return {};
+let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId;
+return { titleSlug, productId };
+}
+function clearAllData() {
+for (let i = 0;i < localStorage.length; i++) {
+let key = localStorage.key(i);
+if (!key) continue;
+if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key);
+}
+try {
+indexedDB.deleteDatabase(LocalDb.DB_NAME);
+} catch (e) {}
+alert(t("clear-data-success"));
+}
+function blockAllNotifications() {
+let blockFeatures = getGlobalPref("block.features");
+return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value));
+}
+function isPlainObject(input) {
+return typeof input === "object" && input !== null && input.constructor === Object;
+}
+class SoundShortcut {
+static adjustGainNodeVolume(amount) {
+if (!getGlobalPref("audio.volume.booster.enabled")) return 0;
+let currentValue = getStreamPref("audio.volume"), nearestValue;
+if (amount > 0) nearestValue = ceilToNearest(currentValue, amount);
+else nearestValue = floorToNearest(currentValue, -1 * amount);
+let newValue;
+if (currentValue !== nearestValue) newValue = nearestValue;
+else newValue = currentValue + amount;
+return newValue = setStreamPref("audio.volume", newValue, "direct"), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue;
+}
+static setGainNodeVolume(value) {
+STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100);
+}
+static muteUnmute() {
+if (getGlobalPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) {
+let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getStreamPref("audio.volume"), targetValue;
+if (settingValue === 0) targetValue = 100, setStreamPref("audio.volume", targetValue, "direct");
+else if (gainValue === 0) targetValue = settingValue;
+else targetValue = 0;
+let status;
+if (targetValue === 0) status = t("muted");
+else status = targetValue + "%";
+SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
+state: targetValue === 0 ? 1 : 0
+});
+return;
+}
+let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");
+if ($media) {
+$media.muted = !$media.muted;
+let status = $media.muted ? t("muted") : t("unmuted");
+Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
+state: $media.muted ? 1 : 0
+});
+}
+}
+}
+class StreamStatsCollector {
+static instance;
+static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
+LOG_TAG = "StreamStatsCollector";
+static INTERVAL_BACKGROUND = 60000;
+calculateGrade(value, grades) {
+return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
+}
+currentStats = {
+ping: {
+current: -1,
+grades: [40, 75, 100],
+toString() {
+return this.current === -1 ? "???" : this.current.toString().padStart(3);
+}
+},
+jit: {
+current: 0,
+grades: [30, 40, 60],
+toString() {
+return `${this.current.toFixed(1)}ms`.padStart(6);
+}
+},
+fps: {
+current: 0,
+toString() {
+let maxFps = getStreamPref("video.maxFps");
+return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString();
+}
+},
+btr: {
+current: 0,
+toString() {
+return `${this.current.toFixed(1)} Mbps`.padStart(9);
+}
+},
+fl: {
+received: 0,
+dropped: 0,
+toString() {
+let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
+return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
+}
+},
+pl: {
+received: 0,
+dropped: 0,
+toString() {
+let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
+return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
+}
+},
+dt: {
+current: 0,
+total: 0,
+grades: [6, 9, 12],
+toString() {
+return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6);
+}
+},
+dl: {
+total: 0,
+toString() {
+return humanFileSize(this.total).padStart(8);
+}
+},
+ul: {
+total: 0,
+toString() {
+return humanFileSize(this.total);
+}
+},
+play: {
+seconds: 0,
+startTime: 0,
+toString() {
+return secondsToHm(this.seconds);
+}
+},
+batt: {
+current: 100,
+start: 100,
+isCharging: !1,
+toString() {
+let text = `${this.current}%`;
+if (this.current !== this.start) {
+let diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : "";
+text += ` (${sign}${diffLevel}%)`;
+}
+return text;
+}
+},
+time: {
+toString() {
+return (new Date()).toLocaleTimeString([], {
+hour: "2-digit",
+minute: "2-digit",
+hour12: !1
+});
+}
+}
+};
+lastVideoStat;
+selectedCandidatePairId = null;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+async collect() {
+let stats = await STATES.currentStream.peerConnection?.getStats();
+if (!stats) return;
+if (!this.selectedCandidatePairId) {
+let found = !1;
+stats.forEach((stat) => {
+if (found || stat.type !== "transport") return;
+if (stat = stat, stat.iceState === "connected" && stat.selectedCandidatePairId) this.selectedCandidatePairId = stat.selectedCandidatePairId, found = !0;
+});
+}
+stats.forEach((stat) => {
+if (stat.type === "inbound-rtp" && stat.kind === "video") {
+let fps = this.currentStats["fps"];
+fps.current = stat.framesPerSecond || 0;
+let pl = this.currentStats["pl"];
+pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived;
+let fl = this.currentStats["fl"];
+if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) {
+this.lastVideoStat = stat;
+return;
+}
+let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount;
+if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000;
+let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp;
+btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
+let dt = this.currentStats["dt"];
+dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
+let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
+dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat;
+} else if (this.selectedCandidatePairId && stat.type === "candidate-pair" && stat.id === this.selectedCandidatePairId) {
+let ping = this.currentStats["ping"];
+ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
+let dl = this.currentStats["dl"];
+dl.total = stat.bytesReceived;
+let ul = this.currentStats["ul"];
+ul.total = stat.bytesSent;
+}
+});
+let batteryLevel = 100, isCharging = !1;
+if (STATES.browser.capabilities.batteryApi) try {
+let bm = await navigator.getBattery();
+isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100);
+} catch (e) {}
+let battery = this.currentStats["batt"];
+battery.current = batteryLevel, battery.isCharging = isCharging;
+let playTime = this.currentStats["play"], now = +new Date;
+playTime.seconds = Math.ceil((now - playTime.startTime) / 1000);
+}
+getStat(kind) {
+return this.currentStats[kind];
+}
+reset() {
+let playTime = this.currentStats["play"];
+playTime.seconds = 0, playTime.startTime = +new Date;
+try {
+STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => {
+this.currentStats["batt"].start = Math.round(bm.level * 100);
+});
+} catch (e) {}
+}
+static setupEvents() {
+BxEventBus.Stream.on("state.playing", () => {
+StreamStatsCollector.getInstance().reset();
+});
+}
+}
+class StreamStats {
+static instance;
+static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
+LOG_TAG = "StreamStats";
+intervalId;
+REFRESH_INTERVAL = 1000;
+stats = {
+time: {
+name: t("clock"),
+$element: CE("span")
+},
+play: {
+name: t("playtime"),
+$element: CE("span")
+},
+batt: {
+name: t("battery"),
+$element: CE("span")
+},
+ping: {
+name: t("stat-ping"),
+$element: CE("span")
+},
+jit: {
+name: t("jitter"),
+$element: CE("span")
+},
+fps: {
+name: t("stat-fps"),
+$element: CE("span")
+},
+btr: {
+name: t("stat-bitrate"),
+$element: CE("span")
+},
+dt: {
+name: t("stat-decode-time"),
+$element: CE("span")
+},
+pl: {
+name: t("stat-packets-lost"),
+$element: CE("span")
+},
+fl: {
+name: t("stat-frames-lost"),
+$element: CE("span")
+},
+dl: {
+name: t("downloaded"),
+$element: CE("span")
+},
+ul: {
+name: t("uploaded"),
+$element: CE("span")
+}
+};
+$container;
+quickGlanceObserver;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.render();
+}
+async start(glancing = !1) {
+if (!this.isHidden() || glancing && this.isGlancing()) return;
+this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
+}
+async stop(glancing = !1) {
+if (glancing && !this.isGlancing()) return;
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");
+}
+async toggle() {
+if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");
+else this.isHidden() ? await this.start() : await this.stop();
+}
+destroy() {
+this.stop(), this.quickGlanceStop(), this.hideSettingsUi();
+}
+isHidden = () => this.$container.classList.contains("bx-gone");
+isGlancing = () => this.$container.dataset.display === "glancing";
+quickGlanceSetup() {
+if (!STATES.isPlaying || this.quickGlanceObserver) return;
+let $uiContainer = document.querySelector("div[data-testid=ui-container]");
+if (!$uiContainer) return;
+this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {
+for (let record of mutationList) {
+let $target = record.target;
+if (!$target.className || !$target.className.startsWith("GripHandle")) continue;
+if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0);
+else this.stop(!0);
+}
+}), this.quickGlanceObserver.observe($uiContainer, {
+attributes: !0,
+attributeFilter: ["aria-expanded"],
+subtree: !0
+});
+}
+quickGlanceStop() {
+this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null;
+}
+update = async (forceUpdate = !1) => {
+if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {
+this.destroy();
+return;
+}
+let PREF_STATS_CONDITIONAL_FORMATTING = getStreamPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();
+await statsCollector.collect();
+let statKey;
+for (statKey in this.stats) {
+grade = "";
+let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;
+if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);
+if ($element.dataset.grade !== grade) $element.dataset.grade = grade;
+}
+};
+refreshStyles() {
+let PREF_ITEMS = getStreamPref("stats.items"), PREF_OPACITY_BG = getStreamPref("stats.opacity.background"), $container = this.$container;
+if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getStreamPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";
+else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
+$container.style.opacity = getStreamPref("stats.opacity.all") + "%", $container.style.fontSize = getStreamPref("stats.textSize");
+}
+hideSettingsUi() {
+if (this.isGlancing() && !getStreamPref("stats.quickGlance.enabled")) this.stop();
+}
+async render() {
+this.$container = CE("div", { class: "bx-stats-bar bx-gone" });
+let statKey;
+for (statKey in this.stats) {
+let stat = this.stats[statKey], $div = CE("div", {
+class: `bx-stat-${statKey}`,
+title: stat.name
+}, CE("label", !1, statKey.toUpperCase()), stat.$element);
+this.$container.appendChild($div);
+}
+this.refreshStyles(), document.documentElement.appendChild(this.$container);
+}
+static setupEvents() {
+BxEventBus.Stream.on("state.playing", () => {
+let PREF_STATS_QUICK_GLANCE = getStreamPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getStreamPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();
+if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();
+else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);
+});
+}
+static refreshStyles() {
+StreamStats.getInstance().refreshStyles();
+}
+}
+class KeyHelper {
+static NON_PRINTABLE_KEYS = {
+Backquote: "`",
+Minus: "-",
+Equal: "=",
+BracketLeft: "[",
+BracketRight: "]",
+Backslash: "\\",
+Semicolon: ";",
+Quote: "'",
+Comma: ",",
+Period: ".",
+Slash: "/",
+NumpadMultiply: "Numpad *",
+NumpadAdd: "Numpad +",
+NumpadSubtract: "Numpad -",
+NumpadDecimal: "Numpad .",
+NumpadDivide: "Numpad /",
+NumpadEqual: "Numpad =",
+Mouse0: "Left Click",
+Mouse2: "Right Click",
+Mouse1: "Middle Click",
+ScrollUp: "Scroll Up",
+ScrollDown: "Scroll Down",
+ScrollLeft: "Scroll Left",
+ScrollRight: "Scroll Right"
+};
+static getKeyFromEvent(e) {
+let code = null, modifiers;
+if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0;
+else if (e instanceof WheelEvent) {
+if (e.deltaY < 0) code = "ScrollUp";
+else if (e.deltaY > 0) code = "ScrollDown";
+else if (e.deltaX < 0) code = "ScrollLeft";
+else if (e.deltaX > 0) code = "ScrollRight";
+} else if (e instanceof MouseEvent) code = "Mouse" + e.button;
+if (code) {
+let results = { code };
+if (modifiers) results.modifiers = modifiers;
+return results;
+}
+return null;
+}
+static getFullKeyCodeFromEvent(e) {
+let key = KeyHelper.getKeyFromEvent(e);
+return key ? `${key.code}:${key.modifiers || 0}` : "";
+}
+static parseFullKeyCode(str) {
+if (!str) return null;
+let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]);
+return {
+code,
+modifiers
+};
+}
+static codeToKeyName(key) {
+let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code];
+if (modifiers && modifiers !== 0) {
+if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) {
+if (modifiers & 2) text.unshift("Shift");
+if (modifiers & 4) text.unshift("Alt");
+if (modifiers & 1) text.unshift("Ctrl");
+}
+}
+return text.join(" + ");
+}
+}
+class PointerClient {
+static instance;
+static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
+LOG_TAG = "PointerClient";
+REQUIRED_PROTOCOL_VERSION = 2;
+socket;
+mkbHandler;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+start(port, mkbHandler) {
+if (!port) throw new Error("PointerServer port is 0");
+this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {
+BxLogger.info(this.LOG_TAG, "connected");
+}), this.socket.addEventListener("error", (event) => {
+BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
+}), this.socket.addEventListener("close", (event) => {
+this.socket = null;
+}), this.socket.addEventListener("message", (event) => {
+let dataView = new DataView(event.data), messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;
+switch (messageType) {
+case 127:
+let protocolVersion = this.onProtocolVersion(dataView, offset);
+if (BxLogger.info(this.LOG_TAG, "Protocol version", protocolVersion), protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) alert("Required MKB protocol: " + protocolVersion), this.stop();
+break;
+case 1:
+this.onMove(dataView, offset);
+break;
+case 2:
+case 3:
+this.onPress(messageType, dataView, offset);
+break;
+case 4:
+this.onScroll(dataView, offset);
+break;
+case 5:
+this.onPointerCaptureChanged(dataView, offset);
+}
+});
+}
+onProtocolVersion(dataView, offset) {
+return dataView.getUint16(offset);
+}
+onMove(dataView, offset) {
+let x = dataView.getInt16(offset);
+offset += Int16Array.BYTES_PER_ELEMENT;
+let y = dataView.getInt16(offset);
+this.mkbHandler?.handleMouseMove({
+movementX: x,
+movementY: y
+});
+}
+onPress(messageType, dataView, offset) {
+let button = dataView.getUint8(offset);
+this.mkbHandler?.handleMouseClick({
+pointerButton: button,
+pressed: messageType === 2
+});
+}
+onScroll(dataView, offset) {
+let vScroll = dataView.getInt16(offset);
+offset += Int16Array.BYTES_PER_ELEMENT;
+let hScroll = dataView.getInt16(offset);
+this.mkbHandler?.handleMouseWheel({
+vertical: vScroll,
+horizontal: hScroll
+});
+}
+onPointerCaptureChanged(dataView, offset) {
+dataView.getInt8(offset) !== 1 && this.mkbHandler?.stop();
+}
+stop() {
+try {
+this.socket?.close();
+} catch (e) {}
+this.socket = null;
+}
+}
+class MouseDataProvider {
+mkbHandler;
+constructor(handler) {
+this.mkbHandler = handler;
+}
+init() {}
+destroy() {}
+}
+class MkbHandler {}
class MkbPopup {
- static instance;
- static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup);
- popupType;
- $popup;
- $title;
- $btnActivate;
- mkbHandler;
- constructor() {
- this.render(), BxEventBus.Script.on("keyboardShortcuts.updated", () => {
- let $newButton = this.createActivateButton();
- this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton;
- });
- }
- attachMkbHandler(handler) {
- this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller");
- }
- toggleVisibility(show) {
- this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1);
- }
- moveOffscreen(doMove) {
- this.$popup.classList.toggle("bx-offscreen", doMove);
- }
- createActivateButton() {
- let options = {
- style: 1 | 1024 | 128,
- label: t("activate"),
- onClick: this.onActivate
- }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) });
- return createButton(options);
- }
- onActivate = (e) => {
- e.preventDefault(), this.mkbHandler.toggle(!0);
- };
- render() {
- this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({
- label: t("ignore"),
- style: 8,
- onClick: (e) => {
- e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1);
- }
- }), createButton({
- label: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64,
- onClick: () => {
- let dialog = SettingsDialog.getInstance();
- dialog.focusTab("mkb"), dialog.show();
- }
- }))), document.documentElement.appendChild(this.$popup);
- }
- reset() {
- this.toggleVisibility(!0), this.moveOffscreen(!1);
- }
+static instance;
+static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup);
+popupType;
+$popup;
+$title;
+$btnActivate;
+mkbHandler;
+constructor() {
+this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => {
+let $newButton = this.createActivateButton();
+this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton;
+});
+}
+attachMkbHandler(handler) {
+this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller");
+}
+toggleVisibility(show) {
+this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1);
+}
+moveOffscreen(doMove) {
+this.$popup.classList.toggle("bx-offscreen", doMove);
+}
+createActivateButton() {
+let options = {
+style: 1 | 1024 | 128,
+label: t("activate"),
+onClick: this.onActivate
+}, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) });
+return createButton(options);
+}
+onActivate = (e) => {
+e.preventDefault(), this.mkbHandler.toggle(!0);
+};
+render() {
+this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({
+label: t("ignore"),
+style: 8,
+onClick: (e) => {
+e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1);
+}
+}), createButton({
+label: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64,
+onClick: () => {
+let dialog = SettingsDialog.getInstance();
+dialog.focusTab("mkb"), dialog.show();
+}
+}))), document.documentElement.appendChild(this.$popup);
+}
+reset() {
+this.toggleVisibility(!0), this.moveOffscreen(!1);
+}
}
class NativeMkbHandler extends MkbHandler {
- static instance;
- static getInstance() {
- if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler;
- else NativeMkbHandler.instance = null;
- return NativeMkbHandler.instance;
- }
- LOG_TAG = "NativeMkbHandler";
- static isAllowed = () => {
- return STATES.browser.capabilities.emulatedNativeMkb && getPref("nativeMkb.mode") === "on";
- };
- pointerClient;
- enabled = !1;
- mouseButtonsPressed = 0;
- mouseVerticalMultiply = 0;
- mouseHorizontalMultiply = 0;
- inputChannel;
- popup;
- constructor() {
- super();
- BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
- }
- onKeyboardEvent(e) {
- if (e.type === "keyup" && e.code === "F8") {
- e.preventDefault(), this.toggle();
- return;
- }
- }
- onPointerLockRequested(e) {
- AppInterface.requestPointerCapture(), this.start();
- }
- onPointerLockExited(e) {
- AppInterface.releasePointerCapture(), this.stop();
- }
- onPollingModeChanged = (e) => {
- let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
- this.popup.moveOffscreen(move);
- };
- onDialogShown = () => {
- document.pointerLockElement && document.exitPointerLock();
- };
- handleEvent(event) {
- switch (event.type) {
- case "keyup":
- this.onKeyboardEvent(event);
- break;
- case BxEvent.POINTER_LOCK_REQUESTED:
- this.onPointerLockRequested(event);
- break;
- case BxEvent.POINTER_LOCK_EXITED:
- this.onPointerLockExited(event);
- break;
- case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
- this.onPollingModeChanged(event);
- break;
- }
- }
- init() {
- this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1);
- try {
- this.pointerClient.start(STATES.pointerServerPort, this);
- } catch (e) {
- Toast.show("Cannot enable Mouse & Keyboard feature");
- }
- this.mouseVerticalMultiply = getPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown);
- let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) {
- let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
- Toast.show(msg, t("native-mkb"), { html: !0 });
- }
- this.waitForMouseData(!1);
- }
- toggle(force) {
- let setEnable;
- if (typeof force !== "undefined") setEnable = force;
- else setEnable = !this.enabled;
- if (setEnable) document.documentElement.requestPointerLock();
- else document.exitPointerLock();
- }
- updateInputConfigurationAsync(enabled) {
- window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
- enableKeyboardInput: enabled,
- enableMouseInput: enabled,
- enableAbsoluteMouse: !1,
- enableTouchInput: !1
- });
- }
- start() {
- this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });
- }
- stop() {
- this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0);
- }
- destroy() {
- this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock();
- }
- handleMouseMove(data) {
- this.sendMouseInput({
- X: data.movementX,
- Y: data.movementY,
- Buttons: this.mouseButtonsPressed,
- WheelX: 0,
- WheelY: 0
- });
- }
- handleMouseClick(data) {
- let { pointerButton, pressed } = data;
- if (pressed) this.mouseButtonsPressed |= pointerButton;
- else this.mouseButtonsPressed ^= pointerButton;
- this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: this.mouseButtonsPressed,
- WheelX: 0,
- WheelY: 0
- });
- }
- handleMouseWheel(data) {
- let { vertical, horizontal } = data, mouseWheelX = horizontal;
- if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply;
- let mouseWheelY = vertical;
- if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply;
- return this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: this.mouseButtonsPressed,
- WheelX: mouseWheelX,
- WheelY: mouseWheelY
- }), !0;
- }
- setVerticalScrollMultiplier(vertical) {
- this.mouseVerticalMultiply = vertical;
- }
- setHorizontalScrollMultiplier(horizontal) {
- this.mouseHorizontalMultiply = horizontal;
- }
- waitForMouseData(showPopup) {
- this.popup.toggleVisibility(showPopup);
- }
- isEnabled() {
- return this.enabled;
- }
- sendMouseInput(data) {
- data.Type = 0, this.inputChannel?.queueMouseInput(data);
- }
- resetMouseInput() {
- this.mouseButtonsPressed = 0, this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: 0,
- WheelX: 0,
- WheelY: 0
- });
- }
+static instance;
+static getInstance() {
+if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler;
+else NativeMkbHandler.instance = null;
+return NativeMkbHandler.instance;
+}
+LOG_TAG = "NativeMkbHandler";
+static isAllowed = () => {
+return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on";
+};
+pointerClient;
+enabled = !1;
+mouseButtonsPressed = 0;
+mouseVerticalMultiply = 0;
+mouseHorizontalMultiply = 0;
+inputChannel;
+popup;
+constructor() {
+super();
+BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
+}
+onKeyboardEvent(e) {
+if (e.type === "keyup" && e.code === "F8") {
+e.preventDefault(), this.toggle();
+return;
+}
+}
+onPointerLockRequested(e) {
+AppInterface.requestPointerCapture(), this.start();
+}
+onPointerLockExited(e) {
+AppInterface.releasePointerCapture(), this.stop();
+}
+onPollingModeChanged = (e) => {
+let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
+this.popup.moveOffscreen(move);
+};
+onDialogShown = () => {
+document.pointerLockElement && document.exitPointerLock();
+};
+handleEvent(event) {
+switch (event.type) {
+case "keyup":
+this.onKeyboardEvent(event);
+break;
+case BxEvent.POINTER_LOCK_REQUESTED:
+this.onPointerLockRequested(event);
+break;
+case BxEvent.POINTER_LOCK_EXITED:
+this.onPointerLockExited(event);
+break;
+case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
+this.onPollingModeChanged(event);
+break;
+}
+}
+init() {
+this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1);
+try {
+this.pointerClient.start(STATES.pointerServerPort, this);
+} catch (e) {
+Toast.show("Cannot enable Mouse & Keyboard feature");
+}
+this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown);
+let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) {
+let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
+Toast.show(msg, t("native-mkb"), { html: !0 });
+}
+this.waitForMouseData(!1);
+}
+toggle(force) {
+let setEnable;
+if (typeof force !== "undefined") setEnable = force;
+else setEnable = !this.enabled;
+if (setEnable) document.documentElement.requestPointerLock();
+else document.exitPointerLock();
+}
+updateInputConfigurationAsync(enabled) {
+window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
+enableKeyboardInput: enabled,
+enableMouseInput: enabled,
+enableAbsoluteMouse: !1,
+enableTouchInput: !1
+});
+}
+start() {
+this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });
+}
+stop() {
+this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0);
+}
+destroy() {
+this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock();
+}
+handleMouseMove(data) {
+this.sendMouseInput({
+X: data.movementX,
+Y: data.movementY,
+Buttons: this.mouseButtonsPressed,
+WheelX: 0,
+WheelY: 0
+});
+}
+handleMouseClick(data) {
+let { pointerButton, pressed } = data;
+if (pressed) this.mouseButtonsPressed |= pointerButton;
+else this.mouseButtonsPressed ^= pointerButton;
+this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: this.mouseButtonsPressed,
+WheelX: 0,
+WheelY: 0
+});
+}
+handleMouseWheel(data) {
+let { vertical, horizontal } = data, mouseWheelX = horizontal;
+if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply;
+let mouseWheelY = vertical;
+if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply;
+return this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: this.mouseButtonsPressed,
+WheelX: mouseWheelX,
+WheelY: mouseWheelY
+}), !0;
+}
+setVerticalScrollMultiplier(vertical) {
+this.mouseVerticalMultiply = vertical;
+}
+setHorizontalScrollMultiplier(horizontal) {
+this.mouseHorizontalMultiply = horizontal;
+}
+waitForMouseData(showPopup) {
+this.popup.toggleVisibility(showPopup);
+}
+isEnabled() {
+return this.enabled;
+}
+sendMouseInput(data) {
+data.Type = 0, this.inputChannel?.queueMouseInput(data);
+}
+resetMouseInput() {
+this.mouseButtonsPressed = 0, this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: 0,
+WheelX: 0,
+WheelY: 0
+});
+}
+}
+function showGamepadToast(gamepad) {
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) return;
+if (gamepad._noToast) return;
+BxLogger.info("Gamepad", gamepad);
+let text = "🎮";
+if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`;
+let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");
+text += ` - ${gamepadId}`;
+let status;
+if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");
+else status = t("disconnected");
+Toast.show(text, status, { instant: !1 });
+}
+function hasGamepad() {
+let gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads)
+if (gamepad?.connected) return !0;
+return !1;
+}
+function generateVirtualControllerMapping(index, override = {}) {
+return Object.assign({}, {
+GamepadIndex: index,
+A: 0,
+B: 0,
+X: 0,
+Y: 0,
+LeftShoulder: 0,
+RightShoulder: 0,
+LeftTrigger: 0,
+RightTrigger: 0,
+View: 0,
+Menu: 0,
+LeftThumb: 0,
+RightThumb: 0,
+DPadUp: 0,
+DPadDown: 0,
+DPadLeft: 0,
+DPadRight: 0,
+Nexus: 0,
+LeftThumbXAxis: 0,
+LeftThumbYAxis: 0,
+RightThumbXAxis: 0,
+RightThumbYAxis: 0,
+PhysicalPhysicality: 0,
+VirtualPhysicality: 0,
+Dirty: !1,
+Virtual: !1
+}, override);
+}
+var XCLOUD_GAMEPAD_KEY_MAPPING = {
+0: "A",
+1: "B",
+2: "X",
+3: "Y",
+12: "DPadUp",
+15: "DPadRight",
+13: "DPadDown",
+14: "DPadLeft",
+4: "LeftShoulder",
+5: "RightShoulder",
+6: "LeftTrigger",
+7: "RightTrigger",
+10: "LeftThumb",
+11: "RightThumb",
+104: "LeftStickAxes",
+204: "RightStickAxes",
+8: "View",
+9: "Menu",
+16: "Nexus",
+17: "Share",
+102: "LeftThumbXAxis",
+103: "LeftThumbXAxis",
+100: "LeftThumbYAxis",
+101: "LeftThumbYAxis",
+202: "RightThumbXAxis",
+203: "RightThumbXAxis",
+200: "RightThumbYAxis",
+201: "RightThumbYAxis"
+};
+function toXcloudGamepadKey(gamepadKey) {
+return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
}
var PointerToMouseButton = {
- 1: 0,
- 2: 2,
- 4: 1
+1: 0,
+2: 2,
+4: 1
}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller";
class WebSocketMouseDataProvider extends MouseDataProvider {
- pointerClient;
- isConnected = !1;
- init() {
- this.pointerClient = PointerClient.getInstance(), this.isConnected = !1;
- try {
- this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0;
- } catch (e) {
- Toast.show("Cannot enable Mouse & Keyboard feature");
- }
- }
- start() {
- this.isConnected && AppInterface.requestPointerCapture();
- }
- stop() {
- this.isConnected && AppInterface.releasePointerCapture();
- }
- destroy() {
- this.isConnected && this.pointerClient?.stop();
- }
+pointerClient;
+isConnected = !1;
+init() {
+this.pointerClient = PointerClient.getInstance(), this.isConnected = !1;
+try {
+this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0;
+} catch (e) {
+Toast.show("Cannot enable Mouse & Keyboard feature");
+}
+}
+start() {
+this.isConnected && AppInterface.requestPointerCapture();
+}
+stop() {
+this.isConnected && AppInterface.releasePointerCapture();
+}
+destroy() {
+this.isConnected && this.pointerClient?.stop();
+}
}
class PointerLockMouseDataProvider extends MouseDataProvider {
- start() {
- window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu);
- }
- stop() {
- document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu);
- }
- onMouseMoveEvent = (e) => {
- this.mkbHandler.handleMouseMove({
- movementX: e.movementX,
- movementY: e.movementY
- });
- };
- onMouseEvent = (e) => {
- e.preventDefault();
- let data = {
- mouseButton: e.button,
- pressed: e.type === "mousedown"
- };
- this.mkbHandler.handleMouseClick(data);
- };
- onWheelEvent = (e) => {
- if (!KeyHelper.getKeyFromEvent(e)) return;
- let data = {
- vertical: e.deltaY,
- horizontal: e.deltaX
- };
- if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault();
- };
- disableContextMenu = (e) => e.preventDefault();
+start() {
+window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu);
+}
+stop() {
+document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu);
+}
+onMouseMoveEvent = (e) => {
+this.mkbHandler.handleMouseMove({
+movementX: e.movementX,
+movementY: e.movementY
+});
+};
+onMouseEvent = (e) => {
+e.preventDefault();
+let data = {
+mouseButton: e.button,
+pressed: e.type === "mousedown"
+};
+this.mkbHandler.handleMouseClick(data);
+};
+onWheelEvent = (e) => {
+if (!KeyHelper.getKeyFromEvent(e)) return;
+let data = {
+vertical: e.deltaY,
+horizontal: e.deltaX
+};
+if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault();
+};
+disableContextMenu = (e) => e.preventDefault();
}
class EmulatedMkbHandler extends MkbHandler {
- static instance;
- static getInstance() {
- if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler;
- else EmulatedMkbHandler.instance = null;
- return EmulatedMkbHandler.instance;
- }
- static LOG_TAG = "EmulatedMkbHandler";
- static isAllowed() {
- return getPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile());
- }
- PRESET;
- VIRTUAL_GAMEPAD = {
- id: VIRTUAL_GAMEPAD_ID,
- index: 0,
- connected: !1,
- hapticActuators: null,
- mapping: "standard",
- axes: [0, 0, 0, 0],
- buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),
- timestamp: performance.now(),
- vibrationActuator: null
- };
- nativeGetGamepads;
- xCloudGamepad = generateVirtualControllerMapping(0);
- initialized = !1;
- enabled = !1;
- mouseDataProvider;
- isPolling = !1;
- prevWheelCode = null;
- wheelStoppedTimeoutId = null;
- detectMouseStoppedTimeoutId = null;
- escKeyDownTime = -1;
- LEFT_STICK_X = [];
- LEFT_STICK_Y = [];
- RIGHT_STICK_X = [];
- RIGHT_STICK_Y = [];
- popup;
- STICK_MAP = {
- 102: [this.LEFT_STICK_X, -1],
- 103: [this.LEFT_STICK_X, 1],
- 100: [this.LEFT_STICK_Y, 1],
- 101: [this.LEFT_STICK_Y, -1],
- 202: [this.RIGHT_STICK_X, -1],
- 203: [this.RIGHT_STICK_X, 1],
- 200: [this.RIGHT_STICK_Y, 1],
- 201: [this.RIGHT_STICK_Y, -1]
- };
- constructor() {
- super();
- BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
- }
- isEnabled = () => this.enabled;
- patchedGetGamepads = () => {
- let gamepads = this.nativeGetGamepads() || [];
- return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads;
- };
- getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
- updateStick(stick, x, y) {
- let gamepad = this.xCloudGamepad;
- if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y;
- else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y;
- window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
- }
- vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
- resetXcloudGamepads() {
- let index = getPref("mkb.p1.slot") - 1;
- this.xCloudGamepad = generateVirtualControllerMapping(0, {
- GamepadIndex: getPref("localCoOp.enabled") ? index : 0,
- Dirty: !0
- }), this.VIRTUAL_GAMEPAD.index = index;
- }
- pressButton(buttonIndex, pressed) {
- let xCloudKey = toXcloudGamepadKey(buttonIndex);
- if (buttonIndex >= 100) {
- let [valueArr] = this.STICK_MAP[buttonIndex];
- for (let i = valueArr.length - 1;i >= 0; i--)
- if (valueArr[i] === buttonIndex) valueArr.splice(i, 1);
- pressed && valueArr.push(buttonIndex);
- let value;
- if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1];
- else value = 0;
- this.xCloudGamepad[xCloudKey] = value;
- } else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
- window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
- }
- onKeyboardEvent = (e) => {
- let isKeyDown = e.type === "keydown";
- if (e.code === "Escape") {
- if (e.preventDefault(), this.enabled && isKeyDown) {
- if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now();
- else if (performance.now() - this.escKeyDownTime >= 1000) this.stop();
- } else this.escKeyDownTime = -1;
- return;
- }
- if (!this.isPolling || !this.PRESET) return;
- if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
- let buttonIndex = this.PRESET.mapping[e.code || e.key];
- if (typeof buttonIndex === "undefined") return;
- if (e.repeat) return;
- e.preventDefault(), this.pressButton(buttonIndex, isKeyDown);
- };
- onMouseStopped = () => {
- if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return;
- let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1;
- this.updateStick(analog, 0, 0);
- };
- handleMouseClick(data) {
- let mouseButton;
- if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton;
- else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton];
- let key = {
- code: "Mouse" + mouseButton
- };
- if (!this.PRESET) return;
- let buttonIndex = this.PRESET.mapping[key.code];
- if (typeof buttonIndex === "undefined") return;
- this.pressButton(buttonIndex, data.pressed);
- }
- handleMouseMove(data) {
- let preset = this.PRESET;
- if (!preset) return;
- let mouseMapTo = preset.mouse["mapTo"];
- if (mouseMapTo === 0) return;
- this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
- let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y);
- if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;
- else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length;
- let analog = mouseMapTo === 1 ? 0 : 1;
- this.updateStick(analog, x, y);
- }
- handleMouseWheel(data) {
- let code = "";
- if (data.vertical < 0) code = "ScrollUp";
- else if (data.vertical > 0) code = "ScrollDown";
- else if (data.horizontal < 0) code = "ScrollLeft";
- else if (data.horizontal > 0) code = "ScrollRight";
- if (!code) return !1;
- if (!this.PRESET) return !1;
- let key = {
- code
- }, buttonIndex = this.PRESET.mapping[key.code];
- if (typeof buttonIndex === "undefined") return !1;
- if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0);
- return this.wheelStoppedTimeoutId = window.setTimeout(() => {
- this.prevWheelCode = null, this.pressButton(buttonIndex, !1);
- }, 20), !0;
- }
- async toggle(force) {
- if (!this.initialized) return;
- if (typeof force !== "undefined") this.enabled = force;
- else this.enabled = !this.enabled;
- if (this.enabled) try {
- await document.body.requestPointerLock({ unadjustedMovement: !0 });
- } catch (e) {
- document.body.requestPointerLock(), console.log(e);
- }
- else document.pointerLockElement && document.exitPointerLock();
- }
- refreshPresetData() {
- this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads();
- }
- waitForMouseData(showPopup) {
- this.popup.toggleVisibility(showPopup);
- }
- onPollingModeChanged = (e) => {
- let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
- this.popup.moveOffscreen(move);
- };
- onDialogShown = () => {
- document.pointerLockElement && document.exitPointerLock();
- };
- onPointerLockChange = () => {
- if (document.pointerLockElement) this.start();
- else this.stop();
- };
- onPointerLockError = (e) => {
- console.log(e), this.stop();
- };
- onPointerLockRequested = () => {
- this.start();
- };
- onPointerLockExited = () => {
- this.mouseDataProvider?.stop();
- };
- handleEvent(event) {
- switch (event.type) {
- case BxEvent.POINTER_LOCK_REQUESTED:
- this.onPointerLockRequested();
- break;
- case BxEvent.POINTER_LOCK_EXITED:
- this.onPointerLockExited();
- break;
- }
- }
- init() {
- if (!STATES.browser.capabilities.mkb) {
- this.initialized = !1;
- return;
- }
- if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this);
- else this.mouseDataProvider = new PointerLockMouseDataProvider(this);
- if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
- else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError);
- if (MkbPopup.getInstance().reset(), AppInterface) {
- let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) {
- let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
- Toast.show(msg, t("native-mkb"), { html: !0 });
- }
- this.waitForMouseData(!1);
- } else this.waitForMouseData(!0);
- }
- destroy() {
- if (!this.initialized) return;
- if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
- else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError);
- window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
- }
- start() {
- if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
- this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start();
- let virtualGamepad = this.getVirtualGamepad();
- virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {
- gamepad: virtualGamepad
- }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
- }
- stop() {
- this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1;
- let virtualGamepad = this.getVirtualGamepad();
- if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {
- gamepad: virtualGamepad
- }), window.navigator.getGamepads = this.nativeGetGamepads;
- this.waitForMouseData(!0), this.mouseDataProvider?.stop();
- }
- static setupEvents() {}
+static instance;
+static getInstance() {
+if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler;
+else EmulatedMkbHandler.instance = null;
+return EmulatedMkbHandler.instance;
+}
+static LOG_TAG = "EmulatedMkbHandler";
+static isAllowed() {
+return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile());
+}
+PRESET;
+VIRTUAL_GAMEPAD = {
+id: VIRTUAL_GAMEPAD_ID,
+index: 0,
+connected: !1,
+hapticActuators: null,
+mapping: "standard",
+axes: [0, 0, 0, 0],
+buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),
+timestamp: performance.now(),
+vibrationActuator: null
+};
+nativeGetGamepads;
+xCloudGamepad = generateVirtualControllerMapping(0);
+initialized = !1;
+enabled = !1;
+mouseDataProvider;
+isPolling = !1;
+prevWheelCode = null;
+wheelStoppedTimeoutId = null;
+detectMouseStoppedTimeoutId = null;
+escKeyDownTime = -1;
+LEFT_STICK_X = [];
+LEFT_STICK_Y = [];
+RIGHT_STICK_X = [];
+RIGHT_STICK_Y = [];
+popup;
+STICK_MAP = {
+102: [this.LEFT_STICK_X, -1],
+103: [this.LEFT_STICK_X, 1],
+100: [this.LEFT_STICK_Y, 1],
+101: [this.LEFT_STICK_Y, -1],
+202: [this.RIGHT_STICK_X, -1],
+203: [this.RIGHT_STICK_X, 1],
+200: [this.RIGHT_STICK_Y, 1],
+201: [this.RIGHT_STICK_Y, -1]
+};
+constructor() {
+super();
+BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
+}
+isEnabled = () => this.enabled;
+patchedGetGamepads = () => {
+let gamepads = this.nativeGetGamepads() || [];
+return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads;
+};
+getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
+updateStick(stick, x, y) {
+let gamepad = this.xCloudGamepad;
+if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y;
+else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y;
+window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
+}
+vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
+resetXcloudGamepads() {
+let index = getStreamPref("mkb.p1.slot") - 1;
+this.xCloudGamepad = generateVirtualControllerMapping(0, {
+GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0,
+Dirty: !0
+}), this.VIRTUAL_GAMEPAD.index = index;
+}
+pressButton(buttonIndex, pressed) {
+let xCloudKey = toXcloudGamepadKey(buttonIndex);
+if (buttonIndex >= 100) {
+let [valueArr] = this.STICK_MAP[buttonIndex];
+for (let i = valueArr.length - 1;i >= 0; i--)
+if (valueArr[i] === buttonIndex) valueArr.splice(i, 1);
+pressed && valueArr.push(buttonIndex);
+let value;
+if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1];
+else value = 0;
+this.xCloudGamepad[xCloudKey] = value;
+} else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
+window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
+}
+onKeyboardEvent = (e) => {
+let isKeyDown = e.type === "keydown";
+if (e.code === "Escape") {
+if (e.preventDefault(), this.enabled && isKeyDown) {
+if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now();
+else if (performance.now() - this.escKeyDownTime >= 1000) this.stop();
+} else this.escKeyDownTime = -1;
+return;
+}
+if (!this.isPolling || !this.PRESET) return;
+if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
+let buttonIndex = this.PRESET.mapping[e.code || e.key];
+if (typeof buttonIndex === "undefined") return;
+if (e.repeat) return;
+e.preventDefault(), this.pressButton(buttonIndex, isKeyDown);
+};
+onMouseStopped = () => {
+if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return;
+let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1;
+this.updateStick(analog, 0, 0);
+};
+handleMouseClick(data) {
+let mouseButton;
+if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton;
+else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton];
+let key = {
+code: "Mouse" + mouseButton
+};
+if (!this.PRESET) return;
+let buttonIndex = this.PRESET.mapping[key.code];
+if (typeof buttonIndex === "undefined") return;
+this.pressButton(buttonIndex, data.pressed);
+}
+handleMouseMove(data) {
+let preset = this.PRESET;
+if (!preset) return;
+let mouseMapTo = preset.mouse["mapTo"];
+if (mouseMapTo === 0) return;
+this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
+let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y);
+if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;
+else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length;
+let analog = mouseMapTo === 1 ? 0 : 1;
+this.updateStick(analog, x, y);
+}
+handleMouseWheel(data) {
+let code = "";
+if (data.vertical < 0) code = "ScrollUp";
+else if (data.vertical > 0) code = "ScrollDown";
+else if (data.horizontal < 0) code = "ScrollLeft";
+else if (data.horizontal > 0) code = "ScrollRight";
+if (!code) return !1;
+if (!this.PRESET) return !1;
+let key = {
+code
+}, buttonIndex = this.PRESET.mapping[key.code];
+if (typeof buttonIndex === "undefined") return !1;
+if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0);
+return this.wheelStoppedTimeoutId = window.setTimeout(() => {
+this.prevWheelCode = null, this.pressButton(buttonIndex, !1);
+}, 20), !0;
+}
+async toggle(force) {
+if (!this.initialized) return;
+if (typeof force !== "undefined") this.enabled = force;
+else this.enabled = !this.enabled;
+if (this.enabled) try {
+await document.body.requestPointerLock({ unadjustedMovement: !0 });
+} catch (e) {
+document.body.requestPointerLock(), console.log(e);
+}
+else document.pointerLockElement && document.exitPointerLock();
+}
+refreshPresetData() {
+this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads();
+}
+waitForMouseData(showPopup) {
+this.popup.toggleVisibility(showPopup);
+}
+onPollingModeChanged = (e) => {
+let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
+this.popup.moveOffscreen(move);
+};
+onDialogShown = () => {
+document.pointerLockElement && document.exitPointerLock();
+};
+onPointerLockChange = () => {
+if (document.pointerLockElement) this.start();
+else this.stop();
+};
+onPointerLockError = (e) => {
+console.log(e), this.stop();
+};
+onPointerLockRequested = () => {
+this.start();
+};
+onPointerLockExited = () => {
+this.mouseDataProvider?.stop();
+};
+handleEvent(event) {
+switch (event.type) {
+case BxEvent.POINTER_LOCK_REQUESTED:
+this.onPointerLockRequested();
+break;
+case BxEvent.POINTER_LOCK_EXITED:
+this.onPointerLockExited();
+break;
+}
+}
+init() {
+if (!STATES.browser.capabilities.mkb) {
+this.initialized = !1;
+return;
+}
+if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this);
+else this.mouseDataProvider = new PointerLockMouseDataProvider(this);
+if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
+else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError);
+if (MkbPopup.getInstance().reset(), AppInterface) {
+let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) {
+let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
+Toast.show(msg, t("native-mkb"), { html: !0 });
+}
+this.waitForMouseData(!1);
+} else this.waitForMouseData(!0);
+}
+destroy() {
+if (!this.initialized) return;
+if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
+else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError);
+window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
+}
+start() {
+if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
+this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start();
+let virtualGamepad = this.getVirtualGamepad();
+virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {
+gamepad: virtualGamepad
+}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
+}
+stop() {
+this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1;
+let virtualGamepad = this.getVirtualGamepad();
+if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {
+gamepad: virtualGamepad
+}), window.navigator.getGamepads = this.nativeGetGamepads;
+this.waitForMouseData(!0), this.mouseDataProvider?.stop();
+}
+static setupEvents() {}
+}
+class StreamSettings {
+static settings = {
+settings: {},
+xCloudPollingMode: "all",
+deviceVibrationIntensity: 0,
+controllerPollingRate: 4,
+controllers: {},
+mkbPreset: null,
+keyboardShortcuts: {}
+};
+static async refreshControllerSettings() {
+let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads) {
+if (!gamepad?.connected) continue;
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
+let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data);
+controllers[gamepad.id] = {
+shortcuts: shortcutsMapping,
+customization: customizationData
+};
+}
+settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration();
+}
+static preCalculateControllerRange(obj, target, values) {
+if (values && Array.isArray(values)) {
+let [from, to] = values;
+if (from > 1 || to < 100) obj[target] = [from / 100, to / 100];
+}
+}
+static convertControllerCustomization(customization) {
+if (!customization) return null;
+let converted = {
+mapping: {},
+ranges: {},
+vibrationIntensity: 1
+}, gamepadKey;
+for (gamepadKey in customization.mapping) {
+let gamepadStr = toXcloudGamepadKey(gamepadKey);
+if (!gamepadStr) continue;
+let mappedKey = customization.mapping[gamepadKey];
+if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey);
+else converted.mapping[gamepadStr] = !1;
+}
+return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted;
+}
+static async refreshDeviceVibration() {
+if (!STATES.browser.capabilities.deviceVibration) return;
+let mode = getStreamPref("deviceVibration.mode"), intensity = 0;
+if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100;
+StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {});
+}
+static async refreshMkbSettings() {
+let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = {
+mapping: {},
+mouse: Object.assign({}, orgPresetData.mouse)
+}, key;
+for (key in orgPresetData.mapping) {
+let buttonIndex = parseInt(key);
+if (!orgPresetData.mapping[buttonIndex]) continue;
+for (let keyName of orgPresetData.mapping[buttonIndex])
+if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex;
+}
+let mouse = converted.mouse;
+mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {});
+}
+static async refreshKeyboardShortcuts() {
+let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId");
+if (presetId === 0) {
+settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
+return;
+}
+let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action;
+for (action in orgPresetData) {
+let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`;
+converted[key] = action;
+}
+settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
+}
+static async refreshAllSettings() {
+window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts();
+}
+static findKeyboardShortcut(targetAction) {
+let shortcuts = StreamSettings.settings.keyboardShortcuts;
+for (let codeStr in shortcuts)
+if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr);
+return null;
+}
+static setup() {
+let listener = () => {
+StreamSettings.refreshControllerSettings();
+};
+window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings();
+}
}
class BxNumberStepper extends HTMLInputElement {
- intervalId = null;
- isHolding;
- controlValue;
- controlMin;
- controlMax;
- uiMin;
- uiMax;
- steps;
- options;
- onChange;
- $text;
- $btnInc;
- $btnDec;
- $range;
- onRangeInput;
- onClick;
- onPointerUp;
- onPointerDown;
- setValue;
- normalizeValue;
- static create(key, value, min, max, options = {}, onChange) {
- options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
- let $text, $btnInc, $btnDec, $range, self = CE("div", {
- class: "bx-number-stepper",
- id: `bx_setting_${escapeCssSelector(key)}`
- }, CE("div", !1, $btnDec = CE("button", {
- _dataset: {
- type: "dec"
- },
- type: "button",
- class: options.hideSlider ? "bx-focusable" : "",
- tabindex: options.hideSlider ? 0 : -1
- }, "-"), $text = CE("span"), $btnInc = CE("button", {
- _dataset: {
- type: "inc"
- },
- type: "button",
- class: options.hideSlider ? "bx-focusable" : "",
- tabindex: options.hideSlider ? 0 : -1
- }, "+")));
- if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self;
- if ($range = CE("input", {
- id: `bx_inp_setting_${key}`,
- type: "range",
- min: self.uiMin,
- max: self.uiMax,
- value: options.reverse ? -value : value,
- step: self.steps,
- tabindex: 0
- }), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) {
- let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });
- if ($range.setAttribute("list", markersId), options.exactTicks) {
- let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks;
- if (start === min) start += options.exactTicks;
- for (let i = start;i < max; i += options.exactTicks)
- $markers.appendChild(CE("option", {
- value: options.reverse ? -i : i
- }));
- } else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks)
- $markers.appendChild(CE("option", { value: i }));
- self.appendChild($markers);
- }
- return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, {
- focus: options.hideSlider ? $btnInc : $range
- }), Object.defineProperty(self, "value", {
- get() {
- return self.controlValue;
- },
- set(value2) {
- BxNumberStepper.setValue.call(self, value2);
- }
- }), self;
- }
- static setValue(value) {
- if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString();
- BxNumberStepper.updateButtonsVisibility.call(this);
- }
- static normalizeValue(value) {
- return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value;
- }
- static onRangeInput(e) {
- let value = parseInt(e.target.value);
- if (this.options.reverse) value *= -1;
- if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value);
- }
- static onClick(e) {
- if (e.preventDefault(), this.isHolding) return;
- let $btn = e.target.closest("button");
- $btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
- }
- static onPointerDown(e) {
- BxNumberStepper.clearIntervalId.call(this);
- let $btn = e.target.closest("button");
- if (!$btn) return;
- this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => {
- BxNumberStepper.buttonPressed.call(this, e2, $btn);
- }, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 });
- }
- static onPointerUp(e) {
- BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
- }
- static onContextMenu(e) {
- e.preventDefault();
- }
- static updateTextValue() {
- let value = this.controlValue, textContent = null;
- if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);
- if (textContent === null) textContent = value.toString() + this.options.suffix;
- return textContent;
- }
- static buttonPressed(e, $btn) {
- BxNumberStepper.change.call(this, $btn.dataset.type);
- }
- static change(direction) {
- let value = this.controlValue;
- if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps);
- else value = Math.min(this.uiMax, value + this.steps);
- value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue);
- }
- static clearIntervalId() {
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
- }
- static updateButtonsVisibility() {
- if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this);
- }
+intervalId = null;
+isHolding;
+controlValue;
+controlMin;
+controlMax;
+uiMin;
+uiMax;
+steps;
+options;
+onChange;
+$text;
+$btnInc;
+$btnDec;
+$range;
+onRangeInput;
+onClick;
+onPointerUp;
+onPointerDown;
+setValue;
+normalizeValue;
+static create(key, value, min, max, options = {}, onChange) {
+options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
+let $text, $btnInc, $btnDec, $range, self = CE("div", {
+class: "bx-number-stepper",
+id: `bx_setting_${escapeCssSelector(key)}`
+}, CE("div", !1, $btnDec = CE("button", {
+_dataset: {
+type: "dec"
+},
+type: "button",
+class: options.hideSlider ? "bx-focusable" : "",
+tabindex: options.hideSlider ? 0 : -1
+}, "-"), $text = CE("span"), $btnInc = CE("button", {
+_dataset: {
+type: "inc"
+},
+type: "button",
+class: options.hideSlider ? "bx-focusable" : "",
+tabindex: options.hideSlider ? 0 : -1
+}, "+")));
+if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self;
+if ($range = CE("input", {
+id: `bx_inp_setting_${key}`,
+type: "range",
+min: self.uiMin,
+max: self.uiMax,
+value: options.reverse ? -value : value,
+step: self.steps,
+tabindex: 0
+}), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) {
+let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });
+if ($range.setAttribute("list", markersId), options.exactTicks) {
+let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks;
+if (start === min) start += options.exactTicks;
+for (let i = start;i < max; i += options.exactTicks)
+$markers.appendChild(CE("option", {
+value: options.reverse ? -i : i
+}));
+} else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks)
+$markers.appendChild(CE("option", { value: i }));
+self.appendChild($markers);
}
-class NavigationDialog {
- dialogManager;
- onMountedCallbacks = [];
- constructor() {
- this.dialogManager = NavigationDialogManager.getInstance();
- }
- isCancellable() {
- return !0;
- }
- isOverlayVisible() {
- return !0;
- }
- show(configs = {}, clearStack = !1) {
- if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();
- }
- hide() {
- NavigationDialogManager.getInstance().hide();
- }
- getFocusedElement() {
- let $activeElement = document.activeElement;
- if (!$activeElement) return null;
- if (this.$container.contains($activeElement)) return $activeElement;
- return null;
- }
- onBeforeMount(configs = {}) {}
- onMounted(configs = {}) {
- for (let callback of this.onMountedCallbacks)
- callback.call(this);
- }
- onBeforeUnmount() {}
- onUnmounted() {}
- handleKeyPress(key) {
- return !1;
- }
- handleGamepad(button) {
- return !1;
- }
+return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, {
+focus: options.hideSlider ? $btnInc : $range
+}), Object.defineProperty(self, "value", {
+get() {
+return self.controlValue;
+},
+set(value2) {
+BxNumberStepper.setValue.call(self, value2);
}
-class NavigationDialogManager {
- static instance;
- static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
- LOG_TAG = "NavigationDialogManager";
- static GAMEPAD_POLLING_INTERVAL = 50;
- static GAMEPAD_KEYS = [
- 0,
- 1,
- 2,
- 3,
- 12,
- 15,
- 13,
- 14,
- 4,
- 5,
- 6,
- 7,
- 10,
- 11,
- 8,
- 9
- ];
- static GAMEPAD_DIRECTION_MAP = {
- 12: 1,
- 13: 3,
- 14: 4,
- 15: 2,
- 100: 1,
- 101: 3,
- 102: 4,
- 103: 2
- };
- static SIBLING_PROPERTY_MAP = {
- horizontal: {
- 4: "previousElementSibling",
- 2: "nextElementSibling"
- },
- vertical: {
- 1: "previousElementSibling",
- 3: "nextElementSibling"
- }
- };
- gamepadPollingIntervalId = null;
- gamepadLastStates = [];
- gamepadHoldingIntervalId = null;
- $overlay;
- $container;
- dialog = null;
- dialogsStack = [];
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
- e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();
- }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {
- if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;
- let $dialog = mutationList[0].addedNodes[0];
- if (!$dialog || !($dialog instanceof HTMLElement)) return;
- calculateSelectBoxes($dialog);
- }).observe(this.$container, { childList: !0 });
- }
- updateActiveInput(input) {
- document.documentElement.dataset.activeInput = input;
- }
- handleEvent(event) {
- switch (event.type) {
- case "keydown":
- this.updateActiveInput("keyboard");
- let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);
- if (handled) {
- event.preventDefault(), event.stopPropagation();
- return;
- }
- if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);
- else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
- if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
- } else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
- if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
- } else if (keyCode === "Escape") handled = !0, this.hide();
- if (handled) event.preventDefault(), event.stopPropagation();
- break;
- }
- }
- isShowing() {
- return this.$container && !this.$container.classList.contains("bx-gone");
- }
- pollGamepad = () => {
- let gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads) {
- if (!gamepad || !gamepad.connected) continue;
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
- let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;
- if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;
- if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;
- for (let key of NavigationDialogManager.GAMEPAD_KEYS)
- if (lastKey === key && !buttons[key].pressed) {
- releasedButton = key;
- break;
- } else if (buttons[key].pressed) {
- heldButton = key;
- break;
- }
- if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
- if (lastKey) {
- let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);
- if (releasedHorizontal || releasedVertical) releasedButton = lastKey;
- else heldButton = lastKey;
- } else if (axes[0] < -0.5) heldButton = 102;
- else if (axes[0] > 0.5) heldButton = 103;
- else if (axes[1] < -0.5) heldButton = 100;
- else if (axes[1] > 0.5) heldButton = 101;
- }
- if (heldButton !== null) {
- if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {
- let lastState2 = this.gamepadLastStates[gamepad.index];
- if (lastState2) {
- if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {
- this.handleGamepad(gamepad, heldButton);
- return;
- }
- }
- this.clearGamepadHoldingInterval();
- }, 100);
- continue;
- }
- if (releasedButton === null) {
- this.clearGamepadHoldingInterval();
- continue;
- }
- if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
- if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;
- if (releasedButton === 0) {
- document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
- return;
- } else if (releasedButton === 1) {
- this.hide();
- return;
- }
- }
- };
- handleGamepad(gamepad, key) {
- let handled = this.dialog?.handleGamepad(key);
- if (handled) return !0;
- let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];
- if (!direction) return !1;
- if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {
- let $range = document.activeElement;
- if (direction === 4 || direction === 2) {
- let $numberStepper = $range.closest(".bx-number-stepper");
- if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");
- else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));
- handled = !0;
- }
- }
- if (!handled) this.focusDirection(direction);
- return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;
- }
- clearGamepadHoldingInterval() {
- this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;
- }
- show(dialog, configs = {}, clearStack = !1) {
- this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();
- }
- hide() {
- if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;
- if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {
- let dialogIndex = this.dialogsStack.indexOf(this.dialog);
- if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
- }
- if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();
- }
- focus($elm) {
- if (!$elm) return !1;
- if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);
- else return $elm.nearby.focus();
- return $elm.focus(), $elm === document.activeElement;
- }
- getOrientation($elm) {
- let nearby = $elm.nearby || {};
- if (nearby.selfOrientation) return nearby.selfOrientation;
- let orientation, $current = $elm.parentElement;
- while ($current !== this.$container) {
- let tmp = $current.nearby?.orientation;
- if ($current.nearby && tmp) {
- orientation = tmp;
- break;
- }
- $current = $current.parentElement;
- }
- return orientation = orientation || "vertical", setNearby($elm, {
- selfOrientation: orientation
- }), orientation;
- }
- findNextTarget($focusing, direction, checkParent = !1, checked = []) {
- if (!$focusing || $focusing === this.$container) return null;
- if (checked.includes($focusing)) return null;
- checked.push($focusing);
- let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);
- if (nearby[1] && direction === 1) return nearby[1];
- else if (nearby[3] && direction === 3) return nearby[3];
- else if (nearby[4] && direction === 4) return nearby[4];
- else if (nearby[2] && direction === 2) return nearby[2];
- let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];
- if (siblingProperty) {
- let $sibling = $target;
- while ($sibling[siblingProperty]) {
- $sibling = $sibling[siblingProperty];
- let $focusable = this.findFocusableElement($sibling, direction);
- if ($focusable) return $focusable;
- }
- }
- if (nearby.loop) {
- if (nearby.loop(direction)) return null;
- }
- if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);
- return null;
- }
- findFocusableElement($elm, direction) {
- if (!$elm) return null;
- if (!!$elm.disabled) return null;
- if (!isElementVisible($elm)) return null;
- if ($elm.tabIndex > -1) return $elm;
- let focus = $elm.nearby?.focus;
- if (focus) {
- if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);
- else if (typeof focus === "function") {
- if (focus()) return document.activeElement;
- }
- }
- let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";
- if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();
- for (let $child of children) {
- if (!$child || !($child instanceof HTMLElement)) return null;
- let $target = this.findFocusableElement($child, direction);
- if ($target) return $target;
- }
- return null;
- }
- startGamepadPolling() {
- this.stopGamepadPolling();
- }
- stopGamepadPolling() {
- this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;
- }
- focusDirection(direction) {
- let dialog = this.dialog;
- if (!dialog) return;
- let $focusing = dialog.getFocusedElement();
- if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;
- let $target = this.findNextTarget($focusing, direction, !0);
- this.focus($target);
- }
- unmountCurrentDialog() {
- let dialog = this.dialog;
- dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;
- }
+}), Object.defineProperty(self, "disabled", {
+get() {
+return $range.disabled;
+},
+set(value2) {
+$btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2;
+}
+}), self;
+}
+static setValue(value) {
+if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString();
+BxNumberStepper.updateButtonsVisibility.call(this);
+}
+static normalizeValue(value) {
+return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value;
+}
+static onRangeInput(e) {
+let value = parseInt(e.target.value);
+if (this.options.reverse) value *= -1;
+if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value);
+}
+static onClick(e) {
+if (e.preventDefault(), this.isHolding) return;
+let $btn = e.target.closest("button");
+$btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
+}
+static onPointerDown(e) {
+BxNumberStepper.clearIntervalId.call(this);
+let $btn = e.target.closest("button");
+if (!$btn) return;
+this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => {
+BxNumberStepper.buttonPressed.call(this, e2, $btn);
+}, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 });
+}
+static onPointerUp(e) {
+BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
+}
+static onContextMenu(e) {
+e.preventDefault();
+}
+static updateTextValue() {
+let value = this.controlValue, textContent = null;
+if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);
+if (textContent === null) textContent = value.toString() + this.options.suffix;
+return textContent;
+}
+static buttonPressed(e, $btn) {
+BxNumberStepper.change.call(this, $btn.dataset.type);
+}
+static change(direction) {
+let value = this.controlValue;
+if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps);
+else value = Math.min(this.uiMax, value + this.steps);
+value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue);
+}
+static clearIntervalId() {
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
+}
+static updateButtonsVisibility() {
+if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this);
}
-class BxSelectElement extends HTMLSelectElement {
- isControllerFriendly;
- optionsList;
- indicatorsList;
- $indicators;
- visibleIndex;
- isMultiple;
- $select;
- $btnNext;
- $btnPrev;
- $label;
- $checkBox;
- static create($select, forceFriendly = !1) {
- let isControllerFriendly = forceFriendly || getPref("ui.controllerFriendly");
- if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;
- $select.removeAttribute("tabindex");
- let $wrapper = CE("div", {
- class: "bx-select",
- _dataset: {
- controllerFriendly: isControllerFriendly
- }
- });
- if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");
- let $content, self = $wrapper;
- self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];
- let $btnPrev, $btnNext;
- if (isControllerFriendly) {
- $btnPrev = createButton({
- label: "<",
- style: 64
- }), $btnNext = createButton({
- label: ">",
- style: 64
- }), setNearby($wrapper, {
- orientation: "horizontal",
- focus: $btnNext
- }), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;
- let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);
- $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);
- } else $select.addEventListener("change", (e) => {
- self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- });
- if (self.isMultiple) $content = CE("button", {
- class: "bx-select-value bx-focusable",
- tabindex: 0
- }, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {
- self.$checkBox.click();
- }), self.$checkBox.addEventListener("input", (e) => {
- let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);
- $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");
- });
- else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);
- return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {
- mutationList.forEach((mutation) => {
- if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- });
- }).observe($select, {
- subtree: !0,
- childList: !0,
- attributes: !0
- }), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {
- get() {
- return $select.value;
- },
- set(value) {
- self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- }
- }), Object.defineProperty(self, "disabled", {
- get() {
- return $select.disabled;
- },
- set(value) {
- $select.disabled = value;
- }
- }), self.addEventListener = function() {
- $select.addEventListener.apply($select, arguments);
- }, self.removeEventListener = function() {
- $select.removeEventListener.apply($select, arguments);
- }, self.dispatchEvent = function() {
- return $select.dispatchEvent.apply($select, arguments);
- }, self.appendChild = function(node) {
- return $select.appendChild(node), node;
- }, self;
- }
- static resetIndicators() {
- let {
- optionsList,
- indicatorsList,
- $indicators
- } = this, targetSize = optionsList.length;
- if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)
- indicatorsList.pop()?.remove();
- else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {
- let $indicator = CE("span", {});
- indicatorsList.push($indicator), $indicators.appendChild($indicator);
- }
- for (let $indicator of indicatorsList)
- clearDataSet($indicator);
- $indicators.classList.toggle("bx-invisible", targetSize <= 1);
- }
- static getOptionAtIndex(index) {
- return this.optionsList[index];
- }
- static render(e) {
- let {
- $label,
- $btnNext,
- $btnPrev,
- $checkBox,
- optionsList,
- indicatorsList
- } = this;
- if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;
- this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);
- let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";
- if ($option) {
- let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");
- if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {
- let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";
- $label.innerHTML = "";
- let fragment = document.createDocumentFragment();
- fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);
- } else $label.textContent = content;
- } else $label.textContent = content;
- if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);
- let disableButtons = optionsList.length <= 1;
- $btnPrev?.classList.toggle("bx-inactive", disableButtons), $btnNext?.classList.toggle("bx-inactive", disableButtons);
- for (let i = 0;i < optionsList.length; i++) {
- let $option2 = optionsList[i], $indicator = indicatorsList[i];
- if (!$option2 || !$indicator) continue;
- if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";
- if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";
- }
- }
- static normalizeIndex(index) {
- return Math.min(Math.max(index, 0), this.optionsList.length - 1);
- }
- static onPrevNext(e) {
- if (!e.target) return;
- let {
- $btnNext,
- $select,
- isMultiple,
- visibleIndex: currentIndex
- } = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;
- if (newIndex > this.optionsList.length - 1) newIndex = 0;
- else if (newIndex < 0) newIndex = this.optionsList.length - 1;
- if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;
- if (isMultiple) BxSelectElement.render.call(this);
- else BxEvent.dispatch($select, "input");
- }
}
class SettingElement {
- static renderOptions(key, setting, currentValue, onChange) {
- let $control = CE("select", {
- tabindex: 0
- }), $parent;
- if (setting.optionsGroup) $parent = CE("optgroup", {
- label: setting.optionsGroup
- }), $control.appendChild($parent);
- else $parent = $control;
- for (let value in setting.options) {
- let label = setting.options[value], $option = CE("option", { value }, label);
- $parent.appendChild($option);
- }
- return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {
- let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
- !e.ignoreOnChange && onChange(e, value);
- }), $control.setValue = (value) => {
- $control.value = value;
- }, $control;
- }
- static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
- let $control = CE("select", {
- multiple: !0,
- tabindex: 0
- }), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;
- $control.setAttribute("size", size.toString());
- for (let value in setting.multipleOptions) {
- let label = setting.multipleOptions[value], $option = CE("option", { value }, label);
- $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {
- e.preventDefault();
- let target = e.target;
- target.selected = !target.selected;
- let $parent = target.parentElement;
- $parent.focus(), BxEvent.dispatch($parent, "input");
- }), $control.appendChild($option);
- }
- return $control.addEventListener("mousedown", function(e) {
- let self = this, orgScrollTop = self.scrollTop;
- window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
- }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {
- let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);
- !e.ignoreOnChange && onChange(e, values);
- }), $control;
- }
- static renderCheckbox(key, setting, currentValue, onChange) {
- let $control = CE("input", { type: "checkbox", tabindex: 0 });
- return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {
- !e.ignoreOnChange && onChange(e, e.target.checked);
- }), $control.setValue = (value) => {
- $control.checked = !!value;
- }, $control;
- }
- static renderNumberStepper(key, setting, value, onChange, options = {}) {
- return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);
- }
- static METHOD_MAP = {
- options: SettingElement.renderOptions,
- "multiple-options": SettingElement.renderMultipleOptions,
- "number-stepper": SettingElement.renderNumberStepper,
- checkbox: SettingElement.renderCheckbox
- };
- static render(type, key, setting, currentValue, onChange, options) {
- let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));
- if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;
- if (type === "options" || type === "multiple-options") $control.name = $control.id;
- return $control;
- }
- static fromPref(key, storage, onChange, overrideParams = {}) {
- let definition = storage.getDefinition(key), currentValue = storage.getSetting(key), type;
- if ("options" in definition) type = "options";
- else if ("multipleOptions" in definition) type = "multiple-options";
- else if (typeof definition.default === "number") type = "number-stepper";
- else type = "checkbox";
- let params = {};
- if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});
- if (params.disabled) currentValue = definition.default;
- return SettingElement.render(type, key, definition, currentValue, (e, value) => {
- storage.setSetting(key, value), onChange && onChange(e, value);
- }, params);
- }
+static renderOptions(key, setting, currentValue, onChange) {
+let $control = CE("select", {
+tabindex: 0
+}), $parent;
+if (setting.optionsGroup) $parent = CE("optgroup", {
+label: setting.optionsGroup
+}), $control.appendChild($parent);
+else $parent = $control;
+for (let value in setting.options) {
+let label = setting.options[value], $option = CE("option", { value }, label);
+$parent.appendChild($option);
+}
+return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {
+let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
+!e.ignoreOnChange && onChange(e, value);
+}), $control.setValue = (value) => {
+$control.value = value;
+}, $control;
+}
+static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
+let $control = CE("select", {
+multiple: !0,
+tabindex: 0
+}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;
+$control.setAttribute("size", size.toString());
+for (let value in setting.multipleOptions) {
+let label = setting.multipleOptions[value], $option = CE("option", { value }, label);
+$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {
+e.preventDefault();
+let target = e.target;
+target.selected = !target.selected;
+let $parent = target.parentElement;
+$parent.focus(), BxEvent.dispatch($parent, "input");
+}), $control.appendChild($option);
+}
+return $control.addEventListener("mousedown", function(e) {
+let self = this, orgScrollTop = self.scrollTop;
+window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
+}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {
+let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);
+!e.ignoreOnChange && onChange(e, values);
+}), Object.defineProperty($control, "value", {
+get() {
+return Array.from($control.options).filter((option) => option.selected).map((option) => option.value);
+},
+set(value) {
+let values = value.split(",");
+Array.from($control.options).forEach((option) => {
+option.selected = values.includes(option.value);
+});
+}
+}), $control;
+}
+static renderCheckbox(key, setting, currentValue, onChange) {
+let $control = CE("input", { type: "checkbox", tabindex: 0 });
+return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {
+!e.ignoreOnChange && onChange(e, e.target.checked);
+}), $control.setValue = (value) => {
+$control.checked = !!value;
+}, $control;
+}
+static renderNumberStepper(key, setting, value, onChange, options = {}) {
+return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);
+}
+static METHOD_MAP = {
+options: SettingElement.renderOptions,
+"multiple-options": SettingElement.renderMultipleOptions,
+"number-stepper": SettingElement.renderNumberStepper,
+checkbox: SettingElement.renderCheckbox
+};
+static render(type, key, setting, currentValue, onChange, options) {
+let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));
+if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;
+if (type === "options" || type === "multiple-options") $control.name = $control.id;
+return $control;
+}
+static fromPref(key, onChange, overrideParams = {}) {
+let { definition, storage } = getPrefInfo(key);
+if (!definition) return null;
+let currentValue = storage.getSetting(key), type;
+if ("options" in definition) type = "options";
+else if ("multipleOptions" in definition) type = "multiple-options";
+else if (typeof definition.default === "number") type = "number-stepper";
+else type = "checkbox";
+let params = {};
+if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});
+if (params.disabled) currentValue = definition.default;
+return SettingElement.render(type, key, definition, currentValue, (e, value) => {
+if (isGlobalPref(key)) setGlobalPref(key, value, "ui");
+else {
+let id = SettingsManager.getInstance().getTargetGameId();
+setGamePref(id, key, value, "ui");
+}
+onChange && onChange(e, value);
+}, params);
+}
+}
+class BxSelectElement extends HTMLSelectElement {
+isControllerFriendly;
+optionsList;
+indicatorsList;
+$indicators;
+visibleIndex;
+isMultiple;
+$select;
+$btnNext;
+$btnPrev;
+$label;
+$checkBox;
+static create($select, forceFriendly = !1) {
+let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly");
+if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;
+$select.removeAttribute("tabindex");
+let $wrapper = CE("div", {
+class: "bx-select",
+_dataset: {
+controllerFriendly: isControllerFriendly
+}
+});
+if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");
+let $content, self = $wrapper;
+self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];
+let $btnPrev, $btnNext;
+if (isControllerFriendly) {
+$btnPrev = createButton({
+label: "<",
+style: 64
+}), $btnNext = createButton({
+label: ">",
+style: 64
+}), setNearby($wrapper, {
+orientation: "horizontal",
+focus: $btnNext
+}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;
+let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);
+$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);
+} else $select.addEventListener("change", (e) => {
+self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+});
+if (self.isMultiple) $content = CE("button", {
+class: "bx-select-value bx-focusable",
+tabindex: 0
+}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {
+self.$checkBox.click();
+}), self.$checkBox.addEventListener("input", (e) => {
+let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);
+$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");
+});
+else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);
+return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {
+mutationList.forEach((mutation) => {
+if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+});
+}).observe($select, {
+subtree: !0,
+childList: !0,
+attributes: !0
+}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {
+get() {
+return $select.value;
+},
+set(value) {
+self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+}
+}), Object.defineProperty(self, "disabled", {
+get() {
+return $select.disabled;
+},
+set(value) {
+$select.disabled = value;
+}
+}), self.addEventListener = function() {
+$select.addEventListener.apply($select, arguments);
+}, self.removeEventListener = function() {
+$select.removeEventListener.apply($select, arguments);
+}, self.dispatchEvent = function() {
+return $select.dispatchEvent.apply($select, arguments);
+}, self.appendChild = function(node) {
+return $select.appendChild(node), node;
+}, self;
+}
+static resetIndicators() {
+let {
+optionsList,
+indicatorsList,
+$indicators
+} = this, targetSize = optionsList.length;
+if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)
+indicatorsList.pop()?.remove();
+else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {
+let $indicator = CE("span", {});
+indicatorsList.push($indicator), $indicators.appendChild($indicator);
+}
+for (let $indicator of indicatorsList)
+clearDataSet($indicator);
+$indicators.classList.toggle("bx-invisible", targetSize <= 1);
+}
+static getOptionAtIndex(index) {
+return this.optionsList[index];
+}
+static render(e) {
+let {
+$label,
+$btnNext,
+$btnPrev,
+$checkBox,
+optionsList,
+indicatorsList
+} = this;
+if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;
+this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);
+let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";
+if ($option) {
+let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");
+if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {
+let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";
+$label.innerHTML = "";
+let fragment = document.createDocumentFragment();
+fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);
+} else $label.textContent = content;
+} else $label.textContent = content;
+if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);
+let disableButtons = optionsList.length <= 1;
+$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons);
+for (let i = 0;i < optionsList.length; i++) {
+let $option2 = optionsList[i], $indicator = indicatorsList[i];
+if (!$option2 || !$indicator) continue;
+if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";
+if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";
+}
+}
+static normalizeIndex(index) {
+return Math.min(Math.max(index, 0), this.optionsList.length - 1);
+}
+static onPrevNext(e) {
+if (!e.target) return;
+let {
+$btnNext,
+$select,
+isMultiple,
+visibleIndex: currentIndex
+} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;
+if (newIndex > this.optionsList.length - 1) newIndex = 0;
+else if (newIndex < 0) newIndex = this.optionsList.length - 1;
+if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;
+if (isMultiple) BxSelectElement.render.call(this);
+else BxEvent.dispatch($select, "input");
+}
+}
+class XboxApi {
+static CACHED_TITLES = {};
+static async getProductTitle(xboxTitleId) {
+if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];
+try {
+let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;
+return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle;
+} catch (e) {}
+return;
+}
+}
+class SettingsManager {
+static instance;
+static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);
+$streamSettingsSelection;
+$tips;
+playingGameId = -1;
+targetGameId = -1;
+SETTINGS = {
+"localCoOp.enabled": {
+onChange: () => {
+BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));
+}
+},
+"deviceVibration.mode": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"deviceVibration.intensity": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"controller.pollingRate": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"controller.settings": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"nativeMkb.scroll.sensitivityX": {
+onChange: () => {
+let value = getStreamPref("nativeMkb.scroll.sensitivityX");
+NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);
+}
+},
+"nativeMkb.scroll.sensitivityY": {
+onChange: () => {
+let value = getStreamPref("nativeMkb.scroll.sensitivityY");
+NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);
+}
+},
+"video.player.type": {
+onChange: () => {
+if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer();
+},
+alwaysTriggerOnChange: !0
+},
+"video.player.powerPreference": {
+onChange: () => {
+let streamPlayer = STATES.currentStream.streamPlayer;
+if (!streamPlayer) return;
+streamPlayer.reloadPlayer(), updateVideoPlayer();
+}
+},
+"video.processing": {
+onChange: updateVideoPlayer
+},
+"video.processing.sharpness": {
+onChange: updateVideoPlayer
+},
+"video.maxFps": {
+onChange: () => {
+let value = getStreamPref("video.maxFps");
+limitVideoPlayerFps(value);
+}
+},
+"video.ratio": {
+onChange: updateVideoPlayer
+},
+"video.brightness": {
+onChange: updateVideoPlayer
+},
+"video.contrast": {
+onChange: updateVideoPlayer
+},
+"video.saturation": {
+onChange: updateVideoPlayer
+},
+"video.position": {
+onChange: updateVideoPlayer
+},
+"audio.volume": {
+onChange: () => {
+let value = getStreamPref("audio.volume");
+SoundShortcut.setGainNodeVolume(value);
+}
+},
+"stats.items": {
+onChange: StreamStats.refreshStyles
+},
+"stats.quickGlance.enabled": {
+onChange: () => {
+let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();
+value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
+}
+},
+"stats.position": {
+onChange: StreamStats.refreshStyles
+},
+"stats.textSize": {
+onChange: StreamStats.refreshStyles
+},
+"stats.opacity.all": {
+onChange: StreamStats.refreshStyles
+},
+"stats.opacity.background": {
+onChange: StreamStats.refreshStyles
+},
+"stats.colors": {
+onChange: StreamStats.refreshStyles
+},
+"mkb.p1.preset.mappingId": {
+onChange: StreamSettings.refreshMkbSettings
+},
+"mkb.p1.slot": {
+onChange: () => {
+EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
+}
+},
+"keyboardShortcuts.preset.inGameId": {
+onChange: StreamSettings.refreshKeyboardShortcuts
+}
+};
+constructor() {
+BxEventBus.Stream.on("setting.changed", (data) => {
+if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);
+}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
+this.switchGameSettings(id);
+}), this.renderStreamSettingsSelection();
+}
+updateStreamElement(key, onChanges) {
+let info = this.SETTINGS[key];
+if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange);
+else info.onChange();
+let $elm = info.$element;
+if (!$elm) return;
+let value = getGamePref(this.targetGameId, key, !0);
+if ("setValue" in $elm) $elm.setValue(value);
+else $elm.value = value.toString();
+this.updateDataset($elm, key);
+}
+switchGameSettings(id) {
+if (setGameIdPref(id), this.targetGameId === id) return;
+let onChanges = new Set, oldGameId = this.targetGameId;
+this.targetGameId = id;
+let key;
+for (key in this.SETTINGS) {
+if (!isStreamPref(key)) continue;
+let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0);
+if (oldValue === newValue) continue;
+this.updateStreamElement(key, onChanges);
+}
+onChanges.forEach((onChange) => {
+onChange && onChange();
+}), this.$tips.classList.toggle("bx-gone", id < 0);
+}
+setElement(pref, $elm) {
+if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
+this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;
+}
+getElement(pref, params) {
+if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
+let $elm = this.SETTINGS[pref].$element;
+if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;
+return this.updateDataset($elm, pref), $elm;
+}
+hasElement(pref) {
+return !!this.SETTINGS[pref]?.$element;
+}
+updateDataset($elm, pref) {
+if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";
+else delete $elm.dataset.override;
+}
+renderStreamSettingsSelection() {
+this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`);
+let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0);
+$select.addEventListener("input", (e) => {
+let id = parseInt($select.value);
+BxEventBus.Stream.emit("gameSettings.switched", { id });
+}), this.$streamSettingsSelection = CE("div", {
+class: "bx-stream-settings-selection bx-gone",
+_nearby: { orientation: "vertical" }
+}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {
+this.playingGameId = id, setGameIdPref(id);
+let $optGroup = $select.querySelector("optgroup");
+while ($optGroup.childElementCount > 1)
+$optGroup.lastElementChild?.remove();
+if (id >= 0) {
+let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id);
+$optGroup.appendChild(CE("option", {
+value: id
+}, title)), $select.value = id.toString();
+} else $select.value = "-1";
+BxEventBus.Stream.emit("gameSettings.switched", { id });
+});
+}
+getStreamSettingsSelection() {
+return this.$streamSettingsSelection;
+}
+getTargetGameId() {
+return this.targetGameId;
+}
+}
+function onChangeVideoPlayerType() {
+let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();
+if (!settingsManager.hasElement("video.processing")) return;
+let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);
+if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);
+else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
+$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2");
+}
+function limitVideoPlayerFps(targetFps) {
+STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
+}
+function updateVideoPlayer() {
+let streamPlayer = STATES.currentStream.streamPlayer;
+if (!streamPlayer) return;
+limitVideoPlayerFps(getStreamPref("video.maxFps"));
+let options = {
+processing: getStreamPref("video.processing"),
+sharpness: getStreamPref("video.processing.sharpness"),
+saturation: getStreamPref("video.saturation"),
+contrast: getStreamPref("video.contrast"),
+brightness: getStreamPref("video.brightness")
+};
+streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
+}
+window.addEventListener("resize", updateVideoPlayer);
+class NavigationDialog {
+dialogManager;
+onMountedCallbacks = [];
+constructor() {
+this.dialogManager = NavigationDialogManager.getInstance();
+}
+isCancellable() {
+return !0;
+}
+isOverlayVisible() {
+return !0;
+}
+show(configs = {}, clearStack = !1) {
+if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();
+}
+hide() {
+NavigationDialogManager.getInstance().hide();
+}
+getFocusedElement() {
+let $activeElement = document.activeElement;
+if (!$activeElement) return null;
+if (this.$container.contains($activeElement)) return $activeElement;
+return null;
+}
+onBeforeMount(configs = {}) {}
+onMounted(configs = {}) {
+for (let callback of this.onMountedCallbacks)
+callback.call(this);
+}
+onBeforeUnmount() {}
+onUnmounted() {}
+handleKeyPress(key) {
+return !1;
+}
+handleGamepad(button) {
+return !1;
+}
+}
+class NavigationDialogManager {
+static instance;
+static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
+LOG_TAG = "NavigationDialogManager";
+static GAMEPAD_POLLING_INTERVAL = 50;
+static GAMEPAD_KEYS = [
+0,
+1,
+2,
+3,
+12,
+15,
+13,
+14,
+4,
+5,
+6,
+7,
+10,
+11,
+8,
+9
+];
+static GAMEPAD_DIRECTION_MAP = {
+12: 1,
+13: 3,
+14: 4,
+15: 2,
+100: 1,
+101: 3,
+102: 4,
+103: 2
+};
+static SIBLING_PROPERTY_MAP = {
+horizontal: {
+4: "previousElementSibling",
+2: "nextElementSibling"
+},
+vertical: {
+1: "previousElementSibling",
+3: "nextElementSibling"
+}
+};
+gamepadPollingIntervalId = null;
+gamepadLastStates = [];
+gamepadHoldingIntervalId = null;
+$overlay;
+$container;
+dialog = null;
+dialogsStack = [];
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
+e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();
+}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {
+if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;
+let $dialog = mutationList[0].addedNodes[0];
+if (!$dialog || !($dialog instanceof HTMLElement)) return;
+calculateSelectBoxes($dialog);
+}).observe(this.$container, { childList: !0 });
+}
+updateActiveInput(input) {
+document.documentElement.dataset.activeInput = input;
+}
+handleEvent(event) {
+switch (event.type) {
+case "keydown":
+this.updateActiveInput("keyboard");
+let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);
+if (handled) {
+event.preventDefault(), event.stopPropagation();
+return;
+}
+if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);
+else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
+if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
+} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
+if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
+} else if (keyCode === "Escape") handled = !0, this.hide();
+if (handled) event.preventDefault(), event.stopPropagation();
+break;
+}
+}
+isShowing() {
+return this.$container && !this.$container.classList.contains("bx-gone");
+}
+pollGamepad = () => {
+let gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads) {
+if (!gamepad || !gamepad.connected) continue;
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
+let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;
+if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;
+if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;
+for (let key of NavigationDialogManager.GAMEPAD_KEYS)
+if (lastKey === key && !buttons[key].pressed) {
+releasedButton = key;
+break;
+} else if (buttons[key].pressed) {
+heldButton = key;
+break;
+}
+if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
+if (lastKey) {
+let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);
+if (releasedHorizontal || releasedVertical) releasedButton = lastKey;
+else heldButton = lastKey;
+} else if (axes[0] < -0.5) heldButton = 102;
+else if (axes[0] > 0.5) heldButton = 103;
+else if (axes[1] < -0.5) heldButton = 100;
+else if (axes[1] > 0.5) heldButton = 101;
+}
+if (heldButton !== null) {
+if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {
+let lastState2 = this.gamepadLastStates[gamepad.index];
+if (lastState2) {
+if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {
+this.handleGamepad(gamepad, heldButton);
+return;
+}
+}
+this.clearGamepadHoldingInterval();
+}, 100);
+continue;
+}
+if (releasedButton === null) {
+this.clearGamepadHoldingInterval();
+continue;
+}
+if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
+if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;
+if (releasedButton === 0) {
+document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
+return;
+} else if (releasedButton === 1) {
+this.hide();
+return;
+}
+}
+};
+handleGamepad(gamepad, key) {
+let handled = this.dialog?.handleGamepad(key);
+if (handled) return !0;
+let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];
+if (!direction) return !1;
+if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {
+let $range = document.activeElement;
+if (direction === 4 || direction === 2) {
+let $numberStepper = $range.closest(".bx-number-stepper");
+if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");
+else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));
+handled = !0;
+}
+}
+if (!handled) this.focusDirection(direction);
+return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;
+}
+clearGamepadHoldingInterval() {
+this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;
+}
+show(dialog, configs = {}, clearStack = !1) {
+this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();
+}
+hide() {
+if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;
+if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {
+let dialogIndex = this.dialogsStack.indexOf(this.dialog);
+if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
+}
+if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();
+}
+focus($elm) {
+if (!$elm) return !1;
+if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);
+else return $elm.nearby.focus();
+return $elm.focus(), $elm === document.activeElement;
+}
+getOrientation($elm) {
+let nearby = $elm.nearby || {};
+if (nearby.selfOrientation) return nearby.selfOrientation;
+let orientation, $current = $elm.parentElement;
+while ($current !== this.$container) {
+let tmp = $current.nearby?.orientation;
+if ($current.nearby && tmp) {
+orientation = tmp;
+break;
+}
+$current = $current.parentElement;
+}
+return orientation = orientation || "vertical", setNearby($elm, {
+selfOrientation: orientation
+}), orientation;
+}
+findNextTarget($focusing, direction, checkParent = !1, checked = []) {
+if (!$focusing || $focusing === this.$container) return null;
+if (checked.includes($focusing)) return null;
+checked.push($focusing);
+let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);
+if (nearby[1] && direction === 1) return nearby[1];
+else if (nearby[3] && direction === 3) return nearby[3];
+else if (nearby[4] && direction === 4) return nearby[4];
+else if (nearby[2] && direction === 2) return nearby[2];
+let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];
+if (siblingProperty) {
+let $sibling = $target;
+while ($sibling[siblingProperty]) {
+$sibling = $sibling[siblingProperty];
+let $focusable = this.findFocusableElement($sibling, direction);
+if ($focusable) return $focusable;
+}
+}
+if (nearby.loop) {
+if (nearby.loop(direction)) return null;
+}
+if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);
+return null;
+}
+findFocusableElement($elm, direction) {
+if (!$elm) return null;
+if (!!$elm.disabled) return null;
+if (!isElementVisible($elm)) return null;
+if ($elm.tabIndex > -1) return $elm;
+let focus = $elm.nearby?.focus;
+if (focus) {
+if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);
+else if (typeof focus === "function") {
+if (focus()) return document.activeElement;
+}
+}
+let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";
+if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();
+for (let $child of children) {
+if (!$child || !($child instanceof HTMLElement)) return null;
+let $target = this.findFocusableElement($child, direction);
+if ($target) return $target;
+}
+return null;
+}
+startGamepadPolling() {
+this.stopGamepadPolling();
+}
+stopGamepadPolling() {
+this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;
+}
+focusDirection(direction) {
+let dialog = this.dialog;
+if (!dialog) return;
+let $focusing = dialog.getFocusedElement();
+if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;
+let $target = this.findNextTarget($focusing, direction, !0);
+this.focus($target);
+}
+unmountCurrentDialog() {
+let dialog = this.dialog;
+dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;
+}
}
class FullscreenText {
- static instance;
- static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
- LOG_TAG = "FullscreenText";
- $text;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {
- class: "bx-fullscreen-text bx-gone"
- }), document.documentElement.appendChild(this.$text);
- }
- show(msg) {
- document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg;
- }
- hide() {
- document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone");
- }
+static instance;
+static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
+LOG_TAG = "FullscreenText";
+$text;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {
+class: "bx-fullscreen-text bx-gone"
+}), document.documentElement.appendChild(this.$text);
+}
+show(msg) {
+document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg;
+}
+hide() {
+document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone");
+}
}
class SuggestionsSetting {
- static async renderSuggestions(e) {
- let $btnSuggest = e.target.closest("div");
- $btnSuggest.toggleAttribute("bx-open");
- let $content = $btnSuggest.nextElementSibling;
- if ($content) {
- BxEvent.dispatch($content.querySelector("select"), "input");
- return;
- }
- let settingTabGroup;
- for (settingTabGroup in this.SETTINGS_UI) {
- let settingTab = this.SETTINGS_UI[settingTabGroup];
- if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;
- for (let settingTabContent of settingTab.items) {
- if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue;
- for (let setting of settingTabContent.items) {
- let prefKey;
- if (typeof setting === "string") prefKey = setting;
- else if (typeof setting === "object") prefKey = setting.pref;
- if (prefKey) this.suggestedSettingLabels[prefKey] = settingTabContent.label;
- }
- }
- }
- let recommendedDevice = "";
- if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
- if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
- }
- let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
- if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on");
- else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto");
- else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off");
- SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
- let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality")));
- $select.addEventListener("input", (e2) => {
- let profile = $select.value;
- removeChildElements($suggestedSettings);
- let fragment = document.createDocumentFragment(), note;
- if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice });
- else if (profile === "highest") note = "⚠️ " + t("highest-quality-note");
- note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note));
- let settings = this.suggestedSettings[profile], prefKey;
- for (prefKey in settings) {
- let suggestedValue, definition = getPrefDefinition(prefKey);
- if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
- else suggestedValue = settings[prefKey];
- let currentValue = getPref(prefKey, !1), currentValueText = STORAGE.Global.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value;
- if (isSameValue) $value = currentValueText;
- else {
- let suggestedValueText = STORAGE.Global.getValueText(prefKey, suggestedValue);
- $value = currentValueText + " ➔ " + suggestedValueText;
- }
- let $checkbox, breadcrumb = this.suggestedSettingLabels[prefKey] + " ❯ " + STORAGE.Global.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`);
- if ($child = CE("div", {
- class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}`
- }, $checkbox = CE("input", {
- type: "checkbox",
- tabindex: 0,
- checked: !0,
- id
- }), CE("label", {
- for: id
- }, CE("div", {
- class: "bx-suggest-label"
- }, breadcrumb), CE("div", {
- class: "bx-suggest-value"
- }, $value))), isSameValue)
- $checkbox.disabled = !0, $checkbox.checked = !0;
- fragment.appendChild($child);
- }
- $suggestedSettings.appendChild(fragment);
- }), BxEvent.dispatch($select, "input");
- let onClickApply = () => {
- let profile = $select.value, settings = this.suggestedSettings[profile], prefKey;
- for (prefKey in settings) {
- let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`);
- if (!$checkBox.checked || $checkBox.disabled) continue;
- let $control = this.settingElements[prefKey];
- if (!$control) {
- setPref(prefKey, suggestedValue);
- continue;
- }
- let settingDefinition = getPrefDefinition(prefKey);
- if (settingDefinition.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
- if ("setValue" in $control) $control.setValue(suggestedValue);
- else $control.value = suggestedValue;
- BxEvent.dispatch($control, "input", {
- manualTrigger: !0
- });
- }
- BxEvent.dispatch($select, "input");
- }, $btnApply = createButton({
- label: t("apply"),
- style: 128 | 64,
- onClick: onClickApply
- });
- $content = CE("div", {
- class: "bx-sub-content-box bx-suggest-box",
- _nearby: {
- orientation: "vertical"
- }
- }, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", {
- class: "bx-suggest-link bx-focusable",
- href: "https://better-xcloud.github.io/guide/android-webview-tweaks/",
- target: "_blank",
- tabindex: 0
- }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", {
- class: "bx-suggest-link bx-focusable",
- href: "https://github.com/redphx/better-xcloud-devices",
- target: "_blank",
- tabindex: 0
- }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);
- }
- static async getRecommendedSettings(androidInfo) {
- function normalize(str) {
- return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
- }
- try {
- let { brand, board, model } = androidInfo;
- brand = normalize(brand), board = normalize(board), model = normalize(model);
- let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {};
- if (json.schema_version !== 2) return null;
- let scriptSettings = json.settings.script;
- if (scriptSettings._base) {
- let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base;
- for (let profile of base)
- Object.assign(recommended, this.suggestedSettings[profile]);
- delete scriptSettings._base;
- }
- let key;
- for (key in scriptSettings)
- recommended[key] = scriptSettings[key];
- return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name;
- } catch (e) {}
- return null;
- }
- static addDefaultSuggestedSetting(prefKey, value) {
- let key;
- for (key in this.suggestedSettings)
- if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value;
- }
- static generateDefaultSuggestedSettings() {
- let key;
- for (key in this.suggestedSettings) {
- if (key === "default") continue;
- let prefKey;
- for (prefKey in this.suggestedSettings[key])
- if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefDefinition(prefKey).default;
- }
- }
+static async renderSuggestions(e) {
+let $btnSuggest = e.target.closest("div");
+$btnSuggest.toggleAttribute("bx-open");
+let $content = $btnSuggest.nextElementSibling;
+if ($content) {
+BxEvent.dispatch($content.querySelector("select"), "input");
+return;
+}
+let settingTabGroup;
+for (settingTabGroup in this.SETTINGS_UI) {
+let settingTab = this.SETTINGS_UI[settingTabGroup];
+if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;
+for (let settingTabContent of settingTab.items) {
+if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue;
+for (let setting of settingTabContent.items) {
+let prefKey;
+if (typeof setting === "string") prefKey = setting;
+else if (typeof setting === "object") prefKey = setting.pref;
+if (prefKey) this.settingLabels[prefKey] = settingTabContent.label;
+}
+}
+}
+let recommendedDevice = "";
+if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
+if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
+}
+let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
+if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on");
+else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto");
+else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off");
+SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
+let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality")));
+$select.addEventListener("input", (e2) => {
+let profile = $select.value;
+removeChildElements($suggestedSettings);
+let fragment = document.createDocumentFragment(), note;
+if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice });
+else if (profile === "highest") note = "⚠️ " + t("highest-quality-note");
+note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note));
+let settings = this.suggestedSettings[profile];
+for (let key in settings) {
+let { storage, definition } = getPrefInfo(key), prefKey;
+if (storage === STORAGE.Stream) prefKey = key;
+else prefKey = key;
+let suggestedValue;
+if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
+else suggestedValue = settings[prefKey];
+let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value;
+if (isSameValue) $value = currentValueText;
+else {
+let suggestedValueText = storage.getValueText(prefKey, suggestedValue);
+$value = currentValueText + " ➔ " + suggestedValueText;
+}
+let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`);
+if ($child = CE("div", {
+class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}`
+}, $checkbox = CE("input", {
+type: "checkbox",
+tabindex: 0,
+checked: !0,
+id
+}), CE("label", {
+for: id
+}, CE("div", {
+class: "bx-suggest-label"
+}, breadcrumb), CE("div", {
+class: "bx-suggest-value"
+}, $value))), isSameValue)
+$checkbox.disabled = !0, $checkbox.checked = !0;
+fragment.appendChild($child);
+}
+$suggestedSettings.appendChild(fragment);
+}), BxEvent.dispatch($select, "input");
+let onClickApply = () => {
+let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance();
+for (prefKey in settings) {
+let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`);
+if (!$checkBox.checked || $checkBox.disabled) continue;
+let $control = settingsManager.getElement(prefKey);
+if (!$control) {
+setPref(prefKey, suggestedValue, "direct");
+continue;
+}
+let { definition: settingDefinition } = getPrefInfo(prefKey);
+if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
+if ("setValue" in $control) $control.setValue(suggestedValue);
+else $control.value = suggestedValue;
+BxEvent.dispatch($control, "input", {
+manualTrigger: !0
+});
+}
+BxEvent.dispatch($select, "input");
+}, $btnApply = createButton({
+label: t("apply"),
+style: 128 | 64,
+onClick: onClickApply
+});
+$content = CE("div", {
+class: "bx-sub-content-box bx-suggest-box",
+_nearby: {
+orientation: "vertical"
+}
+}, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", {
+class: "bx-suggest-link bx-focusable",
+href: "https://better-xcloud.github.io/guide/android-webview-tweaks/",
+target: "_blank",
+tabindex: 0
+}, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", {
+class: "bx-suggest-link bx-focusable",
+href: "https://github.com/redphx/better-xcloud-devices",
+target: "_blank",
+tabindex: 0
+}, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);
+}
+static async getRecommendedSettings(androidInfo) {
+function normalize(str) {
+return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
+}
+try {
+let { brand, board, model } = androidInfo;
+brand = normalize(brand), board = normalize(board), model = normalize(model);
+let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {};
+if (json.schema_version !== 2) return null;
+let scriptSettings = json.settings.script;
+if (scriptSettings._base) {
+let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base;
+for (let profile of base)
+Object.assign(recommended, this.suggestedSettings[profile]);
+delete scriptSettings._base;
+}
+let key;
+for (key in scriptSettings)
+recommended[key] = scriptSettings[key];
+return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name;
+} catch (e) {}
+return null;
+}
+static addDefaultSuggestedSetting(prefKey, value) {
+let key;
+for (key in this.suggestedSettings)
+if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value;
+}
+static generateDefaultSuggestedSettings() {
+let key;
+for (key in this.suggestedSettings) {
+if (key === "default") continue;
+let prefKey;
+for (prefKey in this.suggestedSettings[key])
+if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default;
+}
+}
}
class SettingsDialog extends NavigationDialog {
- static instance;
- static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);
- LOG_TAG = "SettingsNavigationDialog";
- $container;
- $tabs;
- $tabContents;
- $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) => {
- let PREF_LATEST_VERSION = getPref("version.latest"), topButtons = [];
- if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
- let opts = {
- label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),
- style: 1 | 64 | 128
- };
- if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();
- else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";
- topButtons.push(createButton(opts));
- }
- if (AppInterface) topButtons.push(createButton({
- label: t("app-settings"),
- icon: BxIcon.STREAM_SETTINGS,
- style: 128 | 64,
- onClick: (e) => {
- AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();
- }
- }));
- else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({
- label: "🔥 " + t("install-android"),
- style: 128 | 64,
- url: "https://better-xcloud.github.io/android"
- }));
- this.$btnGlobalReload = createButton({
- label: t("settings-reload"),
- classes: ["bx-settings-reload-button", "bx-gone"],
- style: 64 | 128,
- onClick: (e) => {
- this.reloadPage();
- }
- }), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {
- class: "bx-settings-reload-note"
- }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {
- class: "bx-suggest-toggler bx-focusable",
- tabindex: 0
- }, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);
- let $div = CE("div", {
- class: "bx-top-buttons",
- _nearby: {
- orientation: "vertical"
- }
- }, ...topButtons);
- $parent.appendChild($div);
- },
- {
- pref: "bx.locale",
- multiLines: !0
- },
- "server.bypassRestriction",
- "ui.controllerFriendly",
- "xhome.enabled"
- ]
- }, {
- group: "server",
- label: t("server"),
- items: [
- {
- pref: "server.region",
- multiLines: !0
- },
- {
- pref: "stream.locale",
- multiLines: !0
- },
- "server.ipv6.prefer"
- ]
- }, {
- group: "stream",
- label: t("stream"),
- items: [
- "stream.video.resolution",
- "stream.video.codecProfile",
- "stream.video.maxBitrate",
- "audio.volume.booster.enabled",
- "screenshot.applyFilters",
- "audio.mic.onPlaying",
- "game.fortnite.forceConsole",
- "stream.video.combineAudio"
- ]
- }, {
- requiredVariants: "full",
- group: "mkb",
- label: t("mouse-and-keyboard"),
- items: [
- "nativeMkb.mode",
- {
- pref: "nativeMkb.forcedGames",
- multiLines: !0,
- note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))
- },
- "mkb.enabled",
- "mkb.cursor.hideIdle"
- ],
- ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {
- unsupported: !0,
- unsupportedNote: CE("a", {
- href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",
- target: "_blank"
- }, "⚠️ " + t("browser-unsupported-feature"))
- } : {}
- }, {
- requiredVariants: "full",
- group: "touch-control",
- label: t("touch-controller"),
- items: [
- {
- pref: "touchController.mode",
- note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))
- },
- "touchController.autoOff",
- "touchController.opacity.default",
- "touchController.style.standard",
- "touchController.style.custom"
- ],
- ...!STATES.userAgent.capabilities.touch ? {
- unsupported: !0,
- unsupportedNote: "⚠️ " + t("device-unsupported-touch")
- } : {}
- }, {
- group: "ui",
- label: t("ui"),
- items: [
- "ui.layout",
- "ui.imageQuality",
- "ui.gameCard.waitTime.show",
- "ui.controllerStatus.show",
- "ui.streamMenu.simplify",
- "ui.splashVideo.skip",
- !AppInterface && "ui.hideScrollbar",
- "ui.systemMenu.hideHandle",
- "ui.feedbackDialog.disabled",
- "ui.reduceAnimations",
- {
- pref: "ui.hideSections",
- multiLines: !0
- },
- {
- pref: "block.features",
- multiLines: !0
- }
- ]
- }, {
- requiredVariants: "full",
- group: "game-bar",
- label: t("game-bar"),
- items: [
- "gameBar.position"
- ]
- }, {
- group: "loading-screen",
- label: t("loading-screen"),
- items: [
- "loadingScreen.gameArt.show",
- "loadingScreen.waitTime.show",
- "loadingScreen.rocket"
- ]
- }, {
- group: "other",
- label: t("other"),
- items: [
- "block.tracking"
- ]
- }, !1, {
- group: "footer",
- items: [
- ($parent) => {
- try {
- let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);
- $parent.appendChild(CE("div", {
- class: "bx-settings-app-version"
- }, `xCloud website version ${appVersion} (${appDate})`));
- } catch (e) {}
- },
- ($parent) => {
- $parent.appendChild(CE("a", {
- class: "bx-donation-link",
- href: "https://ko-fi.com/redphx",
- target: "_blank",
- tabindex: 0
- }, `❤️ ${t("support-better-xcloud")}`));
- },
- ($parent) => {
- $parent.appendChild(createButton({
- label: t("clear-data"),
- style: 8 | 128 | 64,
- onClick: (e) => {
- if (confirm(t("clear-data-confirm"))) clearAllData();
- }
- }));
- },
- ($parent) => {
- $parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({
- label: "Debug info",
- style: 8 | 128 | 64,
- onClick: (e) => {
- let $button = e.target.closest("button");
- if (!$button) return;
- let $pre = $button.nextElementSibling;
- if (!$pre) {
- let debugInfo = deepClone(BX_FLAGS.DeviceInfo);
- debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {
- class: "bx-focusable bx-gone",
- tabindex: 0,
- _on: {
- click: async (e2) => {
- await copyToClipboard(e2.target.innerText);
- }
- }
- }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);
- }
- $pre.classList.toggle("bx-gone"), $pre.scrollIntoView();
- }
- })));
- }
- ]
- }];
- TAB_DISPLAY_ITEMS = [{
- requiredVariants: "full",
- group: "audio",
- label: t("audio"),
- helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",
- items: [{
- pref: "audio.volume",
- onChange: (e, value) => {
- SoundShortcut.setGainNodeVolume(value);
- },
- params: {
- disabled: !getPref("audio.volume.booster.enabled")
- },
- onCreated: (setting, $elm) => {
- let $range = $elm.querySelector("input[type=range");
- BxEventBus.Script.on("setting.changed", (payload) => {
- let { storageKey, settingKey, settingValue } = payload;
- if (storageKey === "BetterXcloud" && settingKey === "audio.volume") $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.maxFps",
- onChange: (e) => {
- limitVideoPlayerFps(parseInt(e.target.value));
- }
- }, {
- pref: "video.player.powerPreference",
- onChange: () => {
- let streamPlayer = STATES.currentStream.streamPlayer;
- if (!streamPlayer) return;
- streamPlayer.reloadPlayer(), updateVideoPlayer();
- }
- }, {
- pref: "video.processing",
- onChange: updateVideoPlayer
- }, {
- pref: "video.ratio",
- onChange: updateVideoPlayer
- }, {
- pref: "video.position",
- onChange: updateVideoPlayer
- }, {
- pref: "video.processing.sharpness",
- onChange: updateVideoPlayer
- }, {
- pref: "video.saturation",
- onChange: updateVideoPlayer
- }, {
- pref: "video.contrast",
- onChange: updateVideoPlayer
- }, {
- pref: "video.brightness",
- onChange: updateVideoPlayer
- }]
- }];
- TAB_CONTROLLER_ITEMS = [];
- TAB_MKB_ITEMS = () => [];
- TAB_STATS_ITEMS = [{
- group: "stats",
- label: t("stream-stats"),
- helpUrl: "https://better-xcloud.github.io/stream-stats/",
- items: [
- {
- pref: "stats.showWhenPlaying"
- },
- {
- pref: "stats.quickGlance.enabled",
- onChange: (e) => {
- let streamStats = StreamStats.getInstance();
- e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
- }
- },
- {
- pref: "stats.items",
- onChange: StreamStats.refreshStyles
- },
- {
- pref: "stats.position",
- onChange: StreamStats.refreshStyles
- },
- {
- pref: "stats.textSize",
- onChange: StreamStats.refreshStyles
- },
- {
- pref: "stats.opacity.all",
- onChange: StreamStats.refreshStyles
- },
- {
- pref: "stats.opacity.background",
- onChange: StreamStats.refreshStyles
- },
- {
- pref: "stats.colors",
- onChange: StreamStats.refreshStyles
- }
- ]
- }];
- SETTINGS_UI = {
- global: {
- group: "global",
- icon: BxIcon.HOME,
- items: this.TAB_GLOBAL_ITEMS
- },
- stream: {
- group: "stream",
- icon: BxIcon.DISPLAY,
- items: this.TAB_DISPLAY_ITEMS
- },
- controller: {
- group: "controller",
- icon: BxIcon.CONTROLLER,
- items: this.TAB_CONTROLLER_ITEMS,
- requiredVariants: "full"
- },
- mkb: !1,
- stats: {
- group: "stats",
- icon: BxIcon.STREAM_STATS,
- items: this.TAB_STATS_ITEMS
- }
- };
- constructor() {
- super();
- BxLogger.info(this.LOG_TAG, "constructor()"), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {
- if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
- let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);
- if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;
- });
- }
- getDialog() {
- return this;
- }
- getContent() {
- return this.$container;
- }
- onMounted() {
- super.onMounted();
- }
- isOverlayVisible() {
- return !STATES.isPlaying;
- }
- reloadPage() {
- this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
- }
- isSupportedVariant(requiredVariants) {
- if (typeof requiredVariants === "undefined") return !0;
- return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);
- }
- onTabClicked = (e) => {
- let $svg = e.target.closest("svg");
- if ($svg.dataset.lazy) {
- delete $svg.dataset.lazy;
- let settingTab = this.SETTINGS_UI[$svg.dataset.group];
- if (!settingTab) return;
- let items = settingTab.items(), $tabContent = this.renderSettingsSection.call(this, settingTab, items);
- this.$tabContents.appendChild($tabContent);
- }
- let $child, children = Array.from(this.$tabContents.children);
- for ($child of children)
- if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);
- else $child.classList.add("bx-gone");
- for (let $child2 of Array.from(this.$tabs.children))
- $child2.classList.remove("bx-active");
- $svg.classList.add("bx-active");
- };
- renderTab(settingTab) {
- let $svg = createSvgIcon(settingTab.icon);
- return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, settingTab.lazyContent && ($svg.dataset.lazy = settingTab.lazyContent.toString()), $svg.addEventListener("click", this.onTabClicked), $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 = getPref("server.region"), continents = {
- "america-north": {
- label: t("continent-north-america")
- },
- "america-south": {
- label: t("continent-south-america")
- },
- asia: {
- label: t("continent-asia")
- },
- australia: {
- label: t("continent-australia")
- },
- europe: {
- label: t("continent-europe")
- },
- other: {
- label: t("other")
- }
- }, $control = CE("select", {
- id: `bx_setting_${escapeCssSelector(setting.pref)}`,
- tabindex: 0
- });
- $control.name = $control.id, $control.addEventListener("input", (e) => {
- setPref(setting.pref, e.target.value), this.onGlobalSettingChanged(e);
- }), setting.options = {};
- for (let regionName in STATES.serverRegions) {
- let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
- if (region.isDefault) {
- if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
- }
- setting.options[value] = label;
- let $option = CE("option", { value }, label), continent = continents[region.contintent];
- if (!continent.children) continent.children = [];
- continent.children.push($option);
- }
- let fragment = document.createDocumentFragment(), key;
- for (key in continents) {
- let continent = continents[key];
- if (!continent.children) continue;
- fragment.appendChild(CE("optgroup", {
- label: continent.label
- }, ...continent.children));
- }
- return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
- }
- renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
- if (typeof setting === "string") setting = {
- pref: setting
- };
- let pref = setting.pref, $control;
- if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);
- else $control = setting.content;
- else if (!setting.unsupported) {
- if (pref === "server.region") $control = this.renderServerSetting(setting);
- else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, STORAGE.Global, async (e) => {
- let newLocale = e.target.value;
- if (getPref("ui.controllerFriendly")) {
- let timeoutId = e.target.timeoutId;
- timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {
- Translations.refreshLocale(newLocale), Translations.updateTranslations();
- }, 500);
- } else Translations.refreshLocale(newLocale), Translations.updateTranslations();
- this.onGlobalSettingChanged(e);
- });
- else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", STORAGE.Global, (e) => {
- let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);
- UserAgent.updateStorage(value);
- let $inp = $control.nextElementSibling;
- $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);
- });
- else {
- let onChange = setting.onChange;
- if (!onChange && settingTab.group === "global") onChange = this.onGlobalSettingChanged;
- $control = SettingElement.fromPref(pref, STORAGE.Global, onChange, setting.params);
- }
- if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($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, experimental = prefDefinition?.experimental || setting.experimental;
- if (typeof note === "function") note = note();
- if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();
- if (settingTabContent.label && setting.pref) {
- if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);
- }
- if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");
- else note = `${t("experimental")}: ${note}`;
- let $note;
- if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);
- else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);
- let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
- $note,
- multiLines: setting.multiLines,
- icon: prefDefinition?.labelIcon
- });
- if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;
- $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
- }
- renderSettingsSection(settingTab, sections) {
- let $tabContent = CE("div", {
- class: "bx-gone",
- "data-tab-group": settingTab.group
- });
- for (let section of sections) {
- if (!section) continue;
- if (section instanceof HTMLElement) {
- $tabContent.appendChild(section);
- continue;
- }
- if (!this.isSupportedVariant(section.requiredVariants)) continue;
- if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;
- let label = section.label;
- if (label === t("better-xcloud")) {
- if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
- label = createButton({
- label,
- url: "https://github.com/redphx/better-xcloud/releases",
- style: 4096 | 16 | 64
- });
- }
- if (label) {
- let $title = CE("h2", {
- _nearby: {
- orientation: "horizontal"
- }
- }, CE("span", !1, label), section.helpUrl && createButton({
- icon: BxIcon.QUESTION,
- style: 8 | 64,
- url: section.helpUrl,
- title: t("help")
- }));
- $tabContent.appendChild($title);
- }
- if (section.unsupportedNote) {
- let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);
- $tabContent.appendChild($note);
- }
- if (section.unsupported) continue;
- if (section.content) {
- $tabContent.appendChild(section.content);
- continue;
- }
- section.items = section.items || [];
- for (let setting of section.items) {
- if (setting === !1) continue;
- if (typeof setting === "function") {
- setting.apply(this, [$tabContent]);
- continue;
- }
- this.renderSettingRow(settingTab, $tabContent, section, setting);
- }
- }
- return $tabContent;
- }
- setupDialog() {
- let $tabs, $tabContents, $container = CE("div", {
- class: "bx-settings-dialog",
- _nearby: {
- orientation: "horizontal"
- }
- }, CE("div", {
- class: "bx-settings-tabs-container",
- _nearby: {
- orientation: "vertical",
- focus: () => {
- return this.dialogManager.focus($tabs);
- },
- loop: (direction) => {
- if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;
- return !1;
- }
- }
- }, $tabs = CE("div", {
- class: "bx-settings-tabs bx-hide-scroll-bar",
- _nearby: {
- focus: () => this.focusActiveTab()
- }
- }), CE("div", !1, this.$btnReload = createButton({
- icon: BxIcon.REFRESH,
- style: 64 | 32,
- onClick: (e) => {
- this.reloadPage();
- }
- }), createButton({
- icon: BxIcon.CLOSE,
- style: 64 | 32,
- onClick: (e) => {
- this.dialogManager.hide();
- }
- }))), $tabContents = 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.$tabContents = $tabContents, $container.addEventListener("click", (e) => {
- if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();
- });
- let settingTabGroup;
- for (settingTabGroup in this.SETTINGS_UI) {
- let settingTab = this.SETTINGS_UI[settingTabGroup];
- if (!settingTab) continue;
- if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;
- if (settingTab.group !== "global" && !this.renderFullSettings) continue;
- let $svg = this.renderTab(settingTab);
- if ($tabs.appendChild($svg), typeof settingTab.items === "function") continue;
- let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);
- $tabContents.appendChild($tabContent);
- }
- $tabs.firstElementChild.dispatchEvent(new Event("click"));
- }
- focusTab(tabId) {
- let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);
- $tab && $tab.dispatchEvent(new Event("click"));
- }
- focusIfNeeded() {
- this.jumpToSettingGroup("next");
- }
- focusActiveTab() {
- let $currentTab = this.$tabs.querySelector(".bx-active");
- return $currentTab && $currentTab.focus(), !0;
- }
- focusVisibleSetting(type = "first") {
- let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
- if (!controls.length) return !1;
- if (type === "last") controls.reverse();
- for (let $control of controls) {
- if (!($control instanceof HTMLElement)) continue;
- let $focusable = this.dialogManager.findFocusableElement($control);
- if ($focusable) {
- if (this.dialogManager.focus($focusable)) return !0;
- }
- }
- return !1;
- }
- focusVisibleTab(type = "first") {
- let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));
- if (!tabs.length) return !1;
- if (type === "last") tabs.reverse();
- for (let $tab of tabs)
- if (this.dialogManager.focus($tab)) return !0;
- return !1;
- }
- jumpToSettingGroup(direction) {
- let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");
- if (!$tabContent) return !1;
- let $header, $focusing = document.activeElement;
- if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");
- else {
- let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;
- while (!0) {
- if (!$tmp) break;
- if ($tmp.tagName === "H2") {
- if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {
- if (++times, direction === "next" || times >= 2) break;
- }
- }
- $tmp = $tmp[siblingProperty];
- }
- }
- let $target;
- if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);
- if ($target) return this.dialogManager.focus($target);
- return !1;
- }
- 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 1:
- let $focusing = document.activeElement;
- if ($focusing && this.$tabs.contains($focusing)) this.hide();
- else this.focusActiveTab();
- break;
- case 4:
- case 5:
- this.focusActiveTab();
- break;
- case 6:
- this.jumpToSettingGroup("previous");
- break;
- case 7:
- this.jumpToSettingGroup("next");
- break;
- default:
- handled = !1;
- break;
- }
- return handled;
- }
+static instance;
+static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);
+LOG_TAG = "SettingsNavigationDialog";
+$container;
+$tabs;
+$tabContents;
+$btnReload;
+$btnGlobalReload;
+$noteGlobalReload;
+$btnSuggestion;
+$streamSettingsSelection;
+renderFullSettings;
+boundOnContextMenu;
+suggestedSettings = {
+recommended: {},
+default: {},
+lowest: {},
+highest: {}
+};
+settingLabels = {};
+settingsManager;
+TAB_GLOBAL_ITEMS = [{
+group: "general",
+label: t("better-xcloud"),
+helpUrl: "https://better-xcloud.github.io/features/",
+items: [
+($parent) => {
+let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];
+if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
+let opts = {
+label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),
+style: 1 | 64 | 128
+};
+if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();
+else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";
+topButtons.push(createButton(opts));
+}
+if (AppInterface) topButtons.push(createButton({
+label: t("app-settings"),
+icon: BxIcon.STREAM_SETTINGS,
+style: 128 | 64,
+onClick: (e) => {
+AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();
+}
+}));
+else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({
+label: "🔥 " + t("install-android"),
+style: 128 | 64,
+url: "https://better-xcloud.github.io/android"
+}));
+this.$btnGlobalReload = createButton({
+label: t("settings-reload"),
+classes: ["bx-settings-reload-button", "bx-gone"],
+style: 64 | 128,
+onClick: (e) => {
+this.reloadPage();
+}
+}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {
+class: "bx-settings-reload-note"
+}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {
+class: "bx-suggest-toggler bx-focusable",
+tabindex: 0
+}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);
+let $div = CE("div", {
+class: "bx-top-buttons",
+_nearby: {
+orientation: "vertical"
+}
+}, ...topButtons);
+$parent.appendChild($div);
+},
+{
+pref: "bx.locale",
+multiLines: !0
+},
+"server.bypassRestriction",
+"ui.controllerFriendly",
+"xhome.enabled"
+]
+}, {
+group: "server",
+label: t("server"),
+items: [
+{
+pref: "server.region",
+multiLines: !0
+},
+{
+pref: "stream.locale",
+multiLines: !0
+},
+"server.ipv6.prefer"
+]
+}, {
+group: "stream",
+label: t("stream"),
+items: [
+"stream.video.resolution",
+"stream.video.codecProfile",
+"stream.video.maxBitrate",
+"audio.volume.booster.enabled",
+"screenshot.applyFilters",
+"audio.mic.onPlaying",
+"game.fortnite.forceConsole",
+"stream.video.combineAudio"
+]
+}, {
+requiredVariants: "full",
+group: "mkb",
+label: t("mouse-and-keyboard"),
+items: [
+"nativeMkb.mode",
+{
+pref: "nativeMkb.forcedGames",
+multiLines: !0,
+note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))
+},
+"mkb.enabled",
+"mkb.cursor.hideIdle"
+],
+...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {
+unsupported: !0,
+unsupportedNote: CE("a", {
+href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",
+target: "_blank"
+}, "⚠️ " + t("browser-unsupported-feature"))
+} : {}
+}, {
+requiredVariants: "full",
+group: "touch-control",
+label: t("touch-controller"),
+items: [
+{
+pref: "touchController.mode",
+note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))
+},
+"touchController.autoOff",
+"touchController.opacity.default",
+"touchController.style.standard",
+"touchController.style.custom"
+],
+...!STATES.userAgent.capabilities.touch ? {
+unsupported: !0,
+unsupportedNote: "⚠️ " + t("device-unsupported-touch")
+} : {}
+}, {
+group: "ui",
+label: t("ui"),
+items: [
+"ui.layout",
+"ui.imageQuality",
+"ui.gameCard.waitTime.show",
+"ui.controllerStatus.show",
+"ui.streamMenu.simplify",
+"ui.splashVideo.skip",
+!AppInterface && "ui.hideScrollbar",
+"ui.systemMenu.hideHandle",
+"ui.feedbackDialog.disabled",
+"ui.reduceAnimations",
+{
+pref: "ui.hideSections",
+multiLines: !0
+},
+{
+pref: "block.features",
+multiLines: !0
+}
+]
+}, {
+requiredVariants: "full",
+group: "game-bar",
+label: t("game-bar"),
+items: [
+"gameBar.position"
+]
+}, {
+group: "loading-screen",
+label: t("loading-screen"),
+items: [
+"loadingScreen.gameArt.show",
+"loadingScreen.waitTime.show",
+"loadingScreen.rocket"
+]
+}, {
+group: "other",
+label: t("other"),
+items: [
+"block.tracking"
+]
+}, !1, {
+group: "footer",
+items: [
+($parent) => {
+try {
+let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);
+$parent.appendChild(CE("div", {
+class: "bx-settings-app-version"
+}, `xCloud website version ${appVersion} (${appDate})`));
+} catch (e) {}
+},
+($parent) => {
+$parent.appendChild(CE("a", {
+class: "bx-donation-link",
+href: "https://ko-fi.com/redphx",
+target: "_blank",
+tabindex: 0
+}, `❤️ ${t("support-better-xcloud")}`));
+},
+($parent) => {
+$parent.appendChild(createButton({
+label: t("clear-data"),
+style: 8 | 128 | 64,
+onClick: (e) => {
+if (confirm(t("clear-data-confirm"))) clearAllData();
+}
+}));
+},
+($parent) => {
+$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({
+label: "Debug info",
+style: 8 | 128 | 64,
+onClick: (e) => {
+let $button = e.target.closest("button");
+if (!$button) return;
+let $pre = $button.nextElementSibling;
+if (!$pre) {
+let debugInfo = deepClone(BX_FLAGS.DeviceInfo);
+debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {
+class: "bx-focusable bx-gone",
+tabindex: 0,
+_on: {
+click: async (e2) => {
+await copyToClipboard(e2.target.innerText);
+}
+}
+}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);
+}
+$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();
+}
+})));
+}
+]
+}];
+TAB_DISPLAY_ITEMS = [{
+requiredVariants: "full",
+group: "audio",
+label: t("audio"),
+helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",
+items: [{
+pref: "audio.volume",
+params: {
+disabled: !getGlobalPref("audio.volume.booster.enabled")
+},
+onCreated: (setting, $elm) => {
+let $range = $elm.querySelector("input[type=range");
+BxEventBus.Stream.on("setting.changed", (payload) => {
+let { settingKey } = payload;
+if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });
+});
+}
+}]
+}, {
+group: "video",
+label: t("video"),
+helpUrl: "https://better-xcloud.github.io/ingame-features/#video",
+items: [
+"video.player.type",
+"video.maxFps",
+"video.player.powerPreference",
+"video.processing",
+"video.ratio",
+"video.position",
+"video.processing.sharpness",
+"video.saturation",
+"video.contrast",
+"video.brightness"
+]
+}];
+TAB_CONTROLLER_ITEMS = [];
+TAB_MKB_ITEMS = [];
+TAB_STATS_ITEMS = [{
+group: "stats",
+label: t("stream-stats"),
+helpUrl: "https://better-xcloud.github.io/stream-stats/",
+items: [
+"stats.showWhenPlaying",
+"stats.quickGlance.enabled",
+"stats.items",
+"stats.position",
+"stats.textSize",
+"stats.opacity.all",
+"stats.opacity.background",
+"stats.colors"
+]
+}];
+SETTINGS_UI = {
+global: {
+group: "global",
+icon: BxIcon.HOME,
+items: this.TAB_GLOBAL_ITEMS
+},
+stream: {
+group: "stream",
+icon: BxIcon.DISPLAY,
+items: this.TAB_DISPLAY_ITEMS
+},
+controller: {
+group: "controller",
+icon: BxIcon.CONTROLLER,
+items: this.TAB_CONTROLLER_ITEMS,
+requiredVariants: "full"
+},
+mkb: !1,
+stats: {
+group: "stats",
+icon: BxIcon.STREAM_STATS,
+items: this.TAB_STATS_ITEMS
+}
+};
+constructor() {
+super();
+BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {
+if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
+let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);
+if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;
+}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
+this.$tabContents.dataset.gameId = id.toString();
+});
+}
+getDialog() {
+return this;
+}
+getContent() {
+return this.$container;
+}
+onMounted() {
+super.onMounted();
+}
+isOverlayVisible() {
+return !STATES.isPlaying;
+}
+reloadPage() {
+this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
+}
+isSupportedVariant(requiredVariants) {
+if (typeof requiredVariants === "undefined") return !0;
+return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);
+}
+onTabClicked = (e) => {
+let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);
+for ($child of children)
+if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);
+else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");
+this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");
+for (let $child2 of Array.from(this.$tabs.children))
+$child2.classList.remove("bx-active");
+$svg.classList.add("bx-active");
+};
+renderTab(settingTab) {
+let $svg = createSvgIcon(settingTab.icon);
+return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;
+}
+onGlobalSettingChanged = (e) => {
+this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
+};
+onContextMenu(e) {
+e.preventDefault();
+let $elm = e.target;
+$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);
+}
+renderServerSetting(setting) {
+let selectedValue = getGlobalPref("server.region"), continents = {
+"america-north": {
+label: t("continent-north-america")
+},
+"america-south": {
+label: t("continent-south-america")
+},
+asia: {
+label: t("continent-asia")
+},
+australia: {
+label: t("continent-australia")
+},
+europe: {
+label: t("continent-europe")
+},
+other: {
+label: t("other")
+}
+}, $control = CE("select", {
+id: `bx_setting_${escapeCssSelector(setting.pref)}`,
+tabindex: 0
+});
+$control.name = $control.id, $control.addEventListener("input", (e) => {
+setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);
+}), setting.options = {};
+for (let regionName in STATES.serverRegions) {
+let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
+if (region.isDefault) {
+if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
+}
+setting.options[value] = label;
+let $option = CE("option", { value }, label), continent = continents[region.contintent];
+if (!continent.children) continent.children = [];
+continent.children.push($option);
+}
+let fragment = document.createDocumentFragment(), key;
+for (key in continents) {
+let continent = continents[key];
+if (!continent.children) continue;
+fragment.appendChild(CE("optgroup", {
+label: continent.label
+}, ...continent.children));
+}
+return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
+}
+renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
+if (typeof setting === "string") setting = {
+pref: setting
+};
+let pref = setting.pref, $control;
+if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);
+else $control = setting.content;
+else if (!setting.unsupported) {
+if (pref === "server.region") $control = this.renderServerSetting(setting);
+else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {
+let newLocale = e.target.value;
+if (getGlobalPref("ui.controllerFriendly")) {
+let timeoutId = e.target.timeoutId;
+timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {
+Translations.refreshLocale(newLocale), Translations.updateTranslations();
+}, 500);
+} else Translations.refreshLocale(newLocale), Translations.updateTranslations();
+this.onGlobalSettingChanged(e);
+});
+else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {
+let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);
+UserAgent.updateStorage(value);
+let $inp = $control.nextElementSibling;
+$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);
+});
+else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);
+if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);
+}
+let prefDefinition = null;
+if (pref) prefDefinition = getPrefInfo(pref).definition;
+if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;
+let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;
+if (typeof note === "function") note = note();
+if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();
+if (settingTabContent.label && setting.pref) {
+if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);
+}
+if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");
+else note = `${t("experimental")}: ${note}`;
+let $note;
+if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);
+else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);
+let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
+$note,
+multiLines: setting.multiLines,
+icon: prefDefinition?.labelIcon,
+onContextMenu: this.boundOnContextMenu,
+pref
+});
+if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;
+$row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
+}
+renderSettingsSection(settingTab, sections) {
+let $tabContent = CE("div", {
+class: "bx-gone",
+_dataset: {
+tabGroup: settingTab.group
+}
+});
+for (let section of sections) {
+if (!section) continue;
+if (section instanceof HTMLElement) {
+$tabContent.appendChild(section);
+continue;
+}
+if (!this.isSupportedVariant(section.requiredVariants)) continue;
+if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;
+let label = section.label;
+if (label === t("better-xcloud")) {
+if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
+label = createButton({
+label,
+url: "https://github.com/redphx/better-xcloud/releases",
+style: 4096 | 16 | 64
+});
+}
+if (label) {
+let $title = CE("h2", {
+_nearby: {
+orientation: "horizontal"
+}
+}, CE("span", !1, label), section.helpUrl && createButton({
+icon: BxIcon.QUESTION,
+style: 8 | 64,
+url: section.helpUrl,
+title: t("help")
+}));
+$tabContent.appendChild($title);
+}
+if (section.unsupportedNote) {
+let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);
+$tabContent.appendChild($note);
+}
+if (section.unsupported) continue;
+if (section.content) {
+$tabContent.appendChild(section.content);
+continue;
+}
+section.items = section.items || [];
+for (let setting of section.items) {
+if (setting === !1) continue;
+if (typeof setting === "function") {
+setting.apply(this, [$tabContent]);
+continue;
+}
+this.renderSettingRow(settingTab, $tabContent, section, setting);
+}
+}
+return $tabContent;
+}
+setupDialog() {
+let $tabs, $tabContents, $container = CE("div", {
+class: "bx-settings-dialog",
+_nearby: {
+orientation: "horizontal"
+}
+}, CE("div", {
+class: "bx-settings-tabs-container",
+_nearby: {
+orientation: "vertical",
+focus: () => {
+return this.dialogManager.focus($tabs);
+},
+loop: (direction) => {
+if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;
+return !1;
+}
+}
+}, $tabs = CE("div", {
+class: "bx-settings-tabs bx-hide-scroll-bar",
+_nearby: {
+focus: () => this.focusActiveTab()
+}
+}), CE("div", !1, this.$btnReload = createButton({
+icon: BxIcon.REFRESH,
+style: 64 | 32,
+onClick: (e) => {
+this.reloadPage();
+}
+}), createButton({
+icon: BxIcon.CLOSE,
+style: 64 | 32,
+onClick: (e) => {
+this.dialogManager.hide();
+}
+}))), CE("div", {
+class: "bx-settings-tab-contents",
+_nearby: {
+orientation: "vertical",
+loop: (direction) => {
+if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;
+return !1;
+}
+}
+}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {
+class: "bx-settings-tab-content",
+_nearby: {
+orientation: "vertical",
+focus: () => this.jumpToSettingGroup("next")
+}
+})));
+this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {
+if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();
+});
+let settingTabGroup;
+for (settingTabGroup in this.SETTINGS_UI) {
+let settingTab = this.SETTINGS_UI[settingTabGroup];
+if (!settingTab) continue;
+if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;
+if (settingTab.group !== "global" && !this.renderFullSettings) continue;
+let $svg = this.renderTab(settingTab);
+$tabs.appendChild($svg);
+let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);
+$tabContents.appendChild($tabContent);
+}
+$tabs.firstElementChild.dispatchEvent(new Event("click"));
+}
+focusTab(tabId) {
+let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);
+$tab && $tab.dispatchEvent(new Event("click"));
+}
+focusIfNeeded() {
+this.jumpToSettingGroup("next");
+}
+focusActiveTab() {
+let $currentTab = this.$tabs.querySelector(".bx-active");
+return $currentTab && $currentTab.focus(), !0;
+}
+focusVisibleSetting(type = "first") {
+let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
+if (!controls.length) return !1;
+if (type === "last") controls.reverse();
+for (let $control of controls) {
+if (!($control instanceof HTMLElement)) continue;
+let $focusable = this.dialogManager.findFocusableElement($control);
+if ($focusable) {
+if (this.dialogManager.focus($focusable)) return !0;
+}
+}
+return !1;
+}
+focusVisibleTab(type = "first") {
+let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));
+if (!tabs.length) return !1;
+if (type === "last") tabs.reverse();
+for (let $tab of tabs)
+if (this.dialogManager.focus($tab)) return !0;
+return !1;
+}
+jumpToSettingGroup(direction) {
+let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");
+if (!$tabContent) return !1;
+let $header, $focusing = document.activeElement;
+if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");
+else {
+let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;
+while (!0) {
+if (!$tmp) break;
+if ($tmp.tagName === "H2") {
+if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {
+if (++times, direction === "next" || times >= 2) break;
+}
+}
+$tmp = $tmp[siblingProperty];
+}
+}
+let $target;
+if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);
+if ($target) return this.dialogManager.focus($target);
+return !1;
+}
+resetHighlightedSetting($elm) {
+let targetGameId = SettingsManager.getInstance().getTargetGameId();
+if (targetGameId < 0) return;
+if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;
+let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");
+if (!$row) return;
+let pref = $row.prefKey;
+if (!pref) alert("Pref not found: " + $row.id);
+if (!isStreamPref(pref)) return;
+let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref);
+if (deleted) BxEventBus.Stream.emit("setting.changed", {
+storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,
+settingKey: pref
+});
+return deleted;
+}
+handleKeyPress(key) {
+let handled = !0;
+switch (key) {
+case "Tab":
+this.focusActiveTab();
+break;
+case "Home":
+this.focusVisibleSetting("first");
+break;
+case "End":
+this.focusVisibleSetting("last");
+break;
+case "PageUp":
+this.jumpToSettingGroup("previous");
+break;
+case "PageDown":
+this.jumpToSettingGroup("next");
+break;
+case "KeyQ":
+this.resetHighlightedSetting();
+break;
+default:
+handled = !1;
+break;
+}
+return handled;
+}
+handleGamepad(button) {
+let handled = !0;
+switch (button) {
+case 1:
+let $focusing = document.activeElement;
+if ($focusing && this.$tabs.contains($focusing)) this.hide();
+else this.focusActiveTab();
+break;
+case 4:
+case 5:
+this.focusActiveTab();
+break;
+case 6:
+this.jumpToSettingGroup("previous");
+break;
+case 7:
+this.jumpToSettingGroup("next");
+break;
+case 2:
+this.resetHighlightedSetting();
+break;
+default:
+handled = !1;
+break;
+}
+return handled;
+}
}
var FeatureGates = {
- PwaPrompt: !1,
- EnableWifiWarnings: !1,
- EnableUpdateRequiredPage: !1,
- ShowForcedUpdateScreen: !1,
- EnableTakControlResizing: !0
-}, nativeMkbMode = getPref("nativeMkb.mode");
+PwaPrompt: !1,
+EnableWifiWarnings: !1,
+EnableUpdateRequiredPage: !1,
+ShowForcedUpdateScreen: !1,
+EnableTakControlResizing: !0
+}, nativeMkbMode = getGlobalPref("nativeMkb.mode");
if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on";
-var blockFeatures = getPref("block.features");
+var blockFeatures = getGlobalPref("block.features");
if (blockFeatures.includes("chat")) FeatureGates.EnableGuideChatTab = !1;
if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers = !1;
if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1;
if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
var BxExposed = {
- getTitleInfo: () => STATES.currentStream.titleInfo,
- modifyPreloadedState: !1,
- 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 {
- let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();
- source.connect(gainNode).connect(audioCtx.destination);
- } catch (e) {
- BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;
- }
- },
- handleControllerShortcut: () => {},
- resetControllerShortcut: () => {},
- overrideSettings: {
- Tv_settings: {
- hasCompletedOnboarding: !0
- }
- },
- disableGamepadPolling: !1,
- backButtonPressed: () => {
- let navigationDialogManager = NavigationDialogManager.getInstance();
- if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;
- let dict = {
- bubbles: !0,
- cancelable: !0,
- key: "XF86Back",
- code: "XF86Back",
- keyCode: 4,
- which: 4
- };
- return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;
- },
- GameSlugRegexes: [
- /[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g,
- / {2,}/g,
- / /g
- ],
- toggleLocalCoOp(enable) {},
- beforePageLoad: () => {},
- localCoOpManager: null,
- reactCreateElement: function(...args) {},
- createReactLocalCoOpIcon: () => {}
+getTitleInfo: () => STATES.currentStream.titleInfo,
+modifyPreloadedState: !1,
+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 {
+let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();
+source.connect(gainNode).connect(audioCtx.destination);
+} catch (e) {
+BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;
+}
+},
+handleControllerShortcut: () => {},
+resetControllerShortcut: () => {},
+overrideSettings: {
+Tv_settings: {
+hasCompletedOnboarding: !0
+}
+},
+disableGamepadPolling: !1,
+backButtonPressed: () => {
+let navigationDialogManager = NavigationDialogManager.getInstance();
+if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;
+let dict = {
+bubbles: !0,
+cancelable: !0,
+key: "XF86Back",
+code: "XF86Back",
+keyCode: 4,
+which: 4
+};
+return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;
+},
+GameSlugRegexes: [
+/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g,
+/ {2,}/g,
+/ /g
+],
+toggleLocalCoOp(enable) {},
+beforePageLoad: () => {},
+localCoOpManager: null,
+reactCreateElement: function(...args) {},
+createReactLocalCoOpIcon: () => {}
};
function getPreferredServerRegion(shortName = !1) {
- let preferredRegion = getPref("server.region"), serverRegions = STATES.serverRegions;
- if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;
- else return preferredRegion;
- for (let regionName in serverRegions) {
- let region = serverRegions[regionName];
- if (!region.isDefault) continue;
- if (shortName && region.shortName) return region.shortName;
- else return regionName;
- }
- return null;
+let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions;
+if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;
+else return preferredRegion;
+for (let regionName in serverRegions) {
+let region = serverRegions[regionName];
+if (!region.isDefault) continue;
+if (shortName && region.shortName) return region.shortName;
+else return regionName;
+}
+return null;
}
class HeaderSection {
- static instance;
- static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);
- LOG_TAG = "HeaderSection";
- $btnRemotePlay;
- $btnSettings;
- $buttonsWrapper;
- observer;
- timeoutId;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = null, this.$btnSettings = createButton({
- classes: ["bx-header-settings-button"],
- label: "???",
- style: 16 | 32 | 64 | 256,
- onClick: (e) => SettingsDialog.getInstance().show()
- }), this.$buttonsWrapper = CE("div", !1, getPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);
- }
- injectSettingsButton($parent) {
- if (!$parent) return;
- let PREF_LATEST_VERSION = getPref("version.latest"), $btnSettings = this.$btnSettings;
- if (isElementVisible(this.$buttonsWrapper)) return;
- if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
- $parent.appendChild(this.$buttonsWrapper);
- }
- checkHeader = () => {
- let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
- if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");
- $target && this.injectSettingsButton($target);
- };
- watchHeader() {
- let $root = document.querySelector("#PageContent header") || document.querySelector("#root");
- if (!$root) return;
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);
- }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();
- }
- showRemotePlayButton() {
- this.$btnRemotePlay?.classList.remove("bx-gone");
- }
- static watchHeader() {
- HeaderSection.getInstance().watchHeader();
- }
+static instance;
+static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);
+LOG_TAG = "HeaderSection";
+$btnRemotePlay;
+$btnSettings;
+$buttonsWrapper;
+observer;
+timeoutId;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = null, this.$btnSettings = createButton({
+classes: ["bx-header-settings-button"],
+label: "???",
+style: 16 | 32 | 64 | 256,
+onClick: (e) => SettingsDialog.getInstance().show()
+}), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);
+}
+injectSettingsButton($parent) {
+if (!$parent) return;
+let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings;
+if (isElementVisible(this.$buttonsWrapper)) return;
+if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
+$parent.appendChild(this.$buttonsWrapper);
+}
+checkHeader = () => {
+let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
+if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");
+$target && this.injectSettingsButton($target);
+};
+watchHeader() {
+let $root = document.querySelector("#PageContent header") || document.querySelector("#root");
+if (!$root) return;
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);
+}), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();
+}
+showRemotePlayButton() {
+this.$btnRemotePlay?.classList.remove("bx-gone");
+}
+static watchHeader() {
+HeaderSection.getInstance().watchHeader();
+}
}
class LoadingScreen {
- static $bgStyle;
- static $waitTimeBox;
- static waitTimeInterval = null;
- static orgWebTitle;
- static secondsToString(seconds) {
- let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");
- return mDisplay + sDisplay;
- }
- static setup() {
- let titleInfo = STATES.currentStream.titleInfo;
- if (!titleInfo) return;
- if (!LoadingScreen.$bgStyle) {
- let $bgStyle = CE("style");
- document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;
- }
- if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();
- }
- static hideRocket() {
- let $bgStyle = LoadingScreen.$bgStyle;
- $bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";
- }
- static setBackground(imageUrl) {
- let $bgStyle = LoadingScreen.$bgStyle;
- imageUrl = imageUrl + "?w=1920";
- let imageQuality = getPref("ui.imageQuality");
- if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;
- $bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;
- let bg = new Image;
- bg.onload = (e) => {
- $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';
- }, bg.src = imageUrl;
- }
- static setupWaitTime(waitTime) {
- if (getPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();
- let secondsLeft = waitTime, $countDown, $estimated;
- LoadingScreen.orgWebTitle = document.title;
- let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
- endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
- let endDateStr = endDate.toISOString().slice(0, 19);
- endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;
- let $waitTimeBox = LoadingScreen.$waitTimeBox;
- if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;
- else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");
- $estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {
- if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
- }, 1000);
- }
- static hide() {
- if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {
- let $rocketBg = document.querySelector('#game-stream rect[width="800"]');
- $rocketBg && $rocketBg.addEventListener("transitionend", (e) => {
- LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";
- }), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';
- }
- setTimeout(LoadingScreen.reset, 2000);
- }
- static reset() {
- LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
- }
+static $bgStyle;
+static $waitTimeBox;
+static waitTimeInterval = null;
+static orgWebTitle;
+static secondsToString(seconds) {
+let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");
+return mDisplay + sDisplay;
+}
+static setup() {
+let titleInfo = STATES.currentStream.titleInfo;
+if (!titleInfo) return;
+if (!LoadingScreen.$bgStyle) {
+let $bgStyle = CE("style");
+document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;
+}
+if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();
+}
+static hideRocket() {
+let $bgStyle = LoadingScreen.$bgStyle;
+$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";
+}
+static setBackground(imageUrl) {
+let $bgStyle = LoadingScreen.$bgStyle;
+imageUrl = imageUrl + "?w=1920";
+let imageQuality = getGlobalPref("ui.imageQuality");
+if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;
+$bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;
+let bg = new Image;
+bg.onload = (e) => {
+$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';
+}, bg.src = imageUrl;
+}
+static setupWaitTime(waitTime) {
+if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();
+let secondsLeft = waitTime, $countDown, $estimated;
+LoadingScreen.orgWebTitle = document.title;
+let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
+endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
+let endDateStr = endDate.toISOString().slice(0, 19);
+endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;
+let $waitTimeBox = LoadingScreen.$waitTimeBox;
+if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;
+else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");
+$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {
+if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
+}, 1000);
+}
+static hide() {
+if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {
+let $rocketBg = document.querySelector('#game-stream rect[width="800"]');
+$rocketBg && $rocketBg.addEventListener("transitionend", (e) => {
+LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";
+}), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';
+}
+setTimeout(LoadingScreen.reset, 2000);
+}
+static reset() {
+LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
+}
}
class GuideMenu {
- static instance;
- static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);
- $renderedButtons;
- closeGuideMenu() {
- if (window.BX_EXPOSED.dialogRoutes) {
- window.BX_EXPOSED.dialogRoutes.closeAll();
- return;
- }
- let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");
- $btnClose && $btnClose.click();
- }
- renderButtons() {
- if (this.$renderedButtons) return this.$renderedButtons;
- let buttons = {
- scriptSettings: createButton({
- label: t("better-xcloud"),
- icon: BxIcon.BETTER_XCLOUD,
- style: 128 | 64 | 1,
- onClick: () => {
- BxEventBus.Script.once("dialog.dismissed", () => {
- setTimeout(() => SettingsDialog.getInstance().show(), 50);
- }), this.closeGuideMenu();
- }
- }),
- closeApp: AppInterface && createButton({
- icon: BxIcon.POWER,
- label: t("close-app"),
- title: t("close-app"),
- style: 128 | 64 | 4,
- onClick: (e) => {
- AppInterface.closeApp();
- },
- attributes: {
- "data-state": "normal"
- }
- }),
- reloadPage: createButton({
- icon: BxIcon.REFRESH,
- label: t("reload-page"),
- title: t("reload-page"),
- style: 128 | 64,
- onClick: () => {
- if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
- else window.location.reload();
- }
- }),
- backToHome: createButton({
- icon: BxIcon.HOME,
- label: t("back-to-home"),
- title: t("back-to-home"),
- style: 128 | 64,
- onClick: () => {
- this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
- },
- attributes: {
- "data-state": "playing"
- }
- })
- }, buttonsLayout = [
- buttons.scriptSettings,
- [
- buttons.backToHome,
- buttons.reloadPage,
- buttons.closeApp
- ]
- ], $div = CE("div", {
- class: "bx-guide-home-buttons"
- });
- if (STATES.userAgent.isTv || getPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";
- for (let $button of buttonsLayout) {
- if (!$button) continue;
- if ($button instanceof HTMLElement) $div.appendChild($button);
- else if (Array.isArray($button)) {
- let $wrapper = CE("div", {});
- for (let $child of $button)
- $child && $wrapper.appendChild($child);
- $div.appendChild($wrapper);
- }
- }
- return this.$renderedButtons = $div, $div;
- }
- injectHome($root, isPlaying = !1) {
- let $target = null;
- if (isPlaying) {
- $target = $root.querySelector("a[class*=QuitGameButton]");
- let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");
- $btnXcloudHome && ($btnXcloudHome.style.display = "none");
- } else {
- let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");
- if ($dividers) $target = $dividers[$dividers.length - 1];
- }
- if (!$target) return !1;
- let $buttons = this.renderButtons();
- $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);
- }
- onShown = async (e) => {
- if (e.where === "home") {
- let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");
- $root && this.injectHome($root, STATES.isPlaying);
- }
- };
- addEventListeners() {
- window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
- }
- observe($addedElm) {
- let className = $addedElm.className;
- if (!className) className = $addedElm.firstElementChild?.className ?? "";
- if (!className || className.startsWith("bx-")) return;
- if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
- let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
- if ($selectedTab) {
- let $elm = $selectedTab, index;
- for (index = 0;$elm = $elm?.previousElementSibling; index++)
- ;
- if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });
- }
- }
+static instance;
+static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);
+$renderedButtons;
+closeGuideMenu() {
+if (window.BX_EXPOSED.dialogRoutes) {
+window.BX_EXPOSED.dialogRoutes.closeAll();
+return;
+}
+let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");
+$btnClose && $btnClose.click();
+}
+renderButtons() {
+if (this.$renderedButtons) return this.$renderedButtons;
+let buttons = {
+scriptSettings: createButton({
+label: t("better-xcloud"),
+icon: BxIcon.BETTER_XCLOUD,
+style: 128 | 64 | 1,
+onClick: () => {
+BxEventBus.Script.once("dialog.dismissed", () => {
+setTimeout(() => SettingsDialog.getInstance().show(), 50);
+}), this.closeGuideMenu();
+}
+}),
+closeApp: AppInterface && createButton({
+icon: BxIcon.POWER,
+label: t("close-app"),
+title: t("close-app"),
+style: 128 | 64 | 4,
+onClick: (e) => {
+AppInterface.closeApp();
+},
+attributes: {
+"data-state": "normal"
+}
+}),
+reloadPage: createButton({
+icon: BxIcon.REFRESH,
+label: t("reload-page"),
+title: t("reload-page"),
+style: 128 | 64,
+onClick: () => {
+if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
+else window.location.reload();
+}
+}),
+backToHome: createButton({
+icon: BxIcon.HOME,
+label: t("back-to-home"),
+title: t("back-to-home"),
+style: 128 | 64,
+onClick: () => {
+this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
+},
+attributes: {
+"data-state": "playing"
+}
+})
+}, buttonsLayout = [
+buttons.scriptSettings,
+[
+buttons.backToHome,
+buttons.reloadPage,
+buttons.closeApp
+]
+], $div = CE("div", {
+class: "bx-guide-home-buttons"
+});
+if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";
+for (let $button of buttonsLayout) {
+if (!$button) continue;
+if ($button instanceof HTMLElement) $div.appendChild($button);
+else if (Array.isArray($button)) {
+let $wrapper = CE("div", {});
+for (let $child of $button)
+$child && $wrapper.appendChild($child);
+$div.appendChild($wrapper);
+}
+}
+return this.$renderedButtons = $div, $div;
+}
+injectHome($root, isPlaying = !1) {
+let $target = null;
+if (isPlaying) {
+$target = $root.querySelector("a[class*=QuitGameButton]");
+let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");
+$btnXcloudHome && ($btnXcloudHome.style.display = "none");
+} else {
+let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");
+if ($dividers) $target = $dividers[$dividers.length - 1];
+}
+if (!$target) return !1;
+let $buttons = this.renderButtons();
+$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);
+}
+onShown = async (e) => {
+if (e.where === "home") {
+let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");
+$root && this.injectHome($root, STATES.isPlaying);
+}
+};
+addEventListeners() {
+window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
+}
+observe($addedElm) {
+let className = $addedElm.className;
+if (!className) className = $addedElm.firstElementChild?.className ?? "";
+if (!className || className.startsWith("bx-")) return;
+if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
+let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
+if ($selectedTab) {
+let $elm = $selectedTab, index;
+for (index = 0;$elm = $elm?.previousElementSibling; index++)
+;
+if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });
+}
+}
}
class StreamBadges {
- static instance;
- static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
- LOG_TAG = "StreamBadges";
- serverInfo = {};
- badges = {
- playtime: {
- name: t("playtime"),
- icon: BxIcon.PLAYTIME,
- color: "#ff004d"
- },
- battery: {
- name: t("battery"),
- icon: BxIcon.BATTERY,
- color: "#00b543"
- },
- download: {
- name: t("download"),
- icon: BxIcon.DOWNLOAD,
- color: "#29adff"
- },
- upload: {
- name: t("upload"),
- icon: BxIcon.UPLOAD,
- color: "#ff77a8"
- },
- server: {
- name: t("server"),
- icon: BxIcon.SERVER,
- color: "#ff6c24"
- },
- video: {
- name: t("video"),
- icon: BxIcon.DISPLAY,
- color: "#742f29"
- },
- audio: {
- name: t("audio"),
- icon: BxIcon.AUDIO,
- color: "#5f574f"
- }
- };
- $container;
- intervalId;
- REFRESH_INTERVAL = 3000;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- setRegion(region) {
- this.serverInfo.server = {
- region
- };
- }
- renderBadge(name, value) {
- let badgeInfo = this.badges[name], $badge;
- if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;
- if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");
- return this.badges[name].$element = $badge, $badge;
- }
- updateBadges = async (forceUpdate = !1) => {
- if (!this.$container || !forceUpdate && !this.$container.isConnected) {
- this.stop();
- return;
- }
- let statsCollector = StreamStatsCollector.getInstance();
- await statsCollector.collect();
- let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {
- download: dl.toString(),
- upload: ul.toString(),
- playtime: play.toString(),
- battery: batt.toString()
- }, name;
- for (name in badges) {
- let value = badges[name];
- if (value === null) continue;
- let $elm = this.badges[name].$element;
- if (!$elm) continue;
- if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");
- else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");
- }
- };
- async start() {
- await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);
- }
- stop() {
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
- }
- destroy() {
- this.serverInfo = {}, delete this.$container;
- }
- async render() {
- if (this.$container) return this.start(), this.$container;
- await this.getServerStats();
- let batteryLevel = "";
- if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";
- let BADGES = [
- ["playtime", "1m"],
- ["battery", batteryLevel],
- ["download", humanFileSize(0)],
- ["upload", humanFileSize(0)],
- this.badges.server.$element ?? ["server", "?"],
- this.serverInfo.video ? this.badges.video.$element : ["video", "?"],
- this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]
- ], $container = CE("div", { class: "bx-badges" });
- for (let item of BADGES) {
- if (!item) continue;
- let $badge;
- if (!(item instanceof HTMLElement)) $badge = this.renderBadge(...item);
- else $badge = item;
- $container.appendChild($badge);
- }
- return this.$container = $container, await this.start(), $container;
- }
- async getServerStats() {
- let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;
- if (stats.forEach((stat) => {
- if (stat.type === "codec") {
- let mimeType = stat.mimeType.split("/")[0];
- if (mimeType === "video") allVideoCodecs[stat.id] = stat;
- else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;
- } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {
- if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;
- else if (stat.kind === "audio") audioCodecId = stat.codecId;
- } else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;
- else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;
- else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;
- }), videoCodecId) {
- let videoStat = allVideoCodecs[videoCodecId], video = {
- width: videoWidth,
- height: videoHeight,
- codec: videoStat.mimeType.substring(6)
- };
- if (video.codec === "H264") {
- let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
- match && (video.profile = match[1]);
- }
- let text = videoHeight + "p";
- if (text && (text += "/"), text += video.codec, video.profile) {
- let profile = video.profile, quality = profile;
- if (profile.startsWith("4d")) quality = t("visual-quality-high");
- else if (profile.startsWith("42e")) quality = t("visual-quality-normal");
- else if (profile.startsWith("420")) quality = t("visual-quality-low");
- text += ` (${quality})`;
- }
- this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;
- }
- if (audioCodecId) {
- let audioStat = allAudioCodecs[audioCodecId], audio = {
- codec: audioStat.mimeType.substring(6),
- bitrate: audioStat.clockRate
- }, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;
- this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;
- }
- if (candidatePairId) {
- BxLogger.info("candidate", candidatePairId, allCandidatePairs);
- let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;
- if (server && server.region) text += server.region;
- text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);
- }
- }
- static setupEvents() {
- window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, async (e) => {
- if (e.where !== "home" || !STATES.isPlaying) return;
- let $btnQuit = document.querySelector("#gamepass-dialog-root a[class*=QuitGameButton]");
- if ($btnQuit) $btnQuit.insertAdjacentElement("beforebegin", await StreamBadges.getInstance().render());
- });
- }
+static instance;
+static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
+LOG_TAG = "StreamBadges";
+serverInfo = {};
+badges = {
+playtime: {
+name: t("playtime"),
+icon: BxIcon.PLAYTIME,
+color: "#ff004d"
+},
+battery: {
+name: t("battery"),
+icon: BxIcon.BATTERY,
+color: "#00b543"
+},
+download: {
+name: t("download"),
+icon: BxIcon.DOWNLOAD,
+color: "#29adff"
+},
+upload: {
+name: t("upload"),
+icon: BxIcon.UPLOAD,
+color: "#ff77a8"
+},
+server: {
+name: t("server"),
+icon: BxIcon.SERVER,
+color: "#ff6c24"
+},
+video: {
+name: t("video"),
+icon: BxIcon.DISPLAY,
+color: "#742f29"
+},
+audio: {
+name: t("audio"),
+icon: BxIcon.AUDIO,
+color: "#5f574f"
+}
+};
+$container;
+intervalId;
+REFRESH_INTERVAL = 3000;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+setRegion(region) {
+this.serverInfo.server = {
+region
+};
+}
+renderBadge(name, value) {
+let badgeInfo = this.badges[name], $badge;
+if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;
+if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");
+return this.badges[name].$element = $badge, $badge;
+}
+updateBadges = async (forceUpdate = !1) => {
+if (!this.$container || !forceUpdate && !this.$container.isConnected) {
+this.stop();
+return;
+}
+let statsCollector = StreamStatsCollector.getInstance();
+await statsCollector.collect();
+let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {
+download: dl.toString(),
+upload: ul.toString(),
+playtime: play.toString(),
+battery: batt.toString()
+}, name;
+for (name in badges) {
+let value = badges[name];
+if (value === null) continue;
+let $elm = this.badges[name].$element;
+if (!$elm) continue;
+if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");
+else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");
+}
+};
+async start() {
+await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);
+}
+stop() {
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
+}
+destroy() {
+this.serverInfo = {}, delete this.$container;
+}
+async render() {
+if (this.$container) return this.start(), this.$container;
+await this.getServerStats();
+let batteryLevel = "";
+if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";
+let BADGES = [
+["playtime", "1m"],
+["battery", batteryLevel],
+["download", humanFileSize(0)],
+["upload", humanFileSize(0)],
+this.badges.server.$element ?? ["server", "?"],
+this.serverInfo.video ? this.badges.video.$element : ["video", "?"],
+this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]
+], $container = CE("div", { class: "bx-badges" });
+for (let item of BADGES) {
+if (!item) continue;
+let $badge;
+if (!(item instanceof HTMLElement)) $badge = this.renderBadge(...item);
+else $badge = item;
+$container.appendChild($badge);
+}
+return this.$container = $container, await this.start(), $container;
+}
+async getServerStats() {
+let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;
+if (stats.forEach((stat) => {
+if (stat.type === "codec") {
+let mimeType = stat.mimeType.split("/")[0];
+if (mimeType === "video") allVideoCodecs[stat.id] = stat;
+else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;
+} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {
+if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;
+else if (stat.kind === "audio") audioCodecId = stat.codecId;
+} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;
+else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;
+else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;
+}), videoCodecId) {
+let videoStat = allVideoCodecs[videoCodecId], video = {
+width: videoWidth,
+height: videoHeight,
+codec: videoStat.mimeType.substring(6)
+};
+if (video.codec === "H264") {
+let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
+match && (video.profile = match[1]);
+}
+let text = videoHeight + "p";
+if (text && (text += "/"), text += video.codec, video.profile) {
+let profile = video.profile, quality = profile;
+if (profile.startsWith("4d")) quality = t("visual-quality-high");
+else if (profile.startsWith("42e")) quality = t("visual-quality-normal");
+else if (profile.startsWith("420")) quality = t("visual-quality-low");
+text += ` (${quality})`;
+}
+this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;
+}
+if (audioCodecId) {
+let audioStat = allAudioCodecs[audioCodecId], audio = {
+codec: audioStat.mimeType.substring(6),
+bitrate: audioStat.clockRate
+}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;
+this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;
+}
+if (candidatePairId) {
+BxLogger.info("candidate", candidatePairId, allCandidatePairs);
+let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;
+if (server && server.region) text += server.region;
+text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);
+}
+}
+static setupEvents() {
+window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, async (e) => {
+if (e.where !== "home" || !STATES.isPlaying) return;
+let $btnQuit = document.querySelector("#gamepass-dialog-root a[class*=QuitGameButton]");
+if ($btnQuit) $btnQuit.insertAdjacentElement("beforebegin", await StreamBadges.getInstance().render());
+});
+}
}
class XcloudInterceptor {
- static SERVER_EXTRA_INFO = {
- EastUS: ["🇺🇸", "america-north"],
- EastUS2: ["🇺🇸", "america-north"],
- NorthCentralUs: ["🇺🇸", "america-north"],
- SouthCentralUS: ["🇺🇸", "america-north"],
- WestUS: ["🇺🇸", "america-north"],
- WestUS2: ["🇺🇸", "america-north"],
- MexicoCentral: ["🇲🇽", "america-north"],
- BrazilSouth: ["🇧🇷", "america-south"],
- JapanEast: ["🇯🇵", "asia"],
- KoreaCentral: ["🇰🇷", "asia"],
- AustraliaEast: ["🇦🇺", "australia"],
- AustraliaSouthEast: ["🇦🇺", "australia"],
- SwedenCentral: ["🇸🇪", "europe"],
- UKSouth: ["🇬🇧", "europe"],
- WestEurope: ["🇪🇺", "europe"]
- };
- static async handleLogin(request, init) {
- let bypassServer = getPref("server.bypassRestriction");
- if (bypassServer !== "off") {
- let ip = BypassServerIps[bypassServer];
- ip && request.headers.set("X-Forwarded-For", ip);
- }
- let response = await NATIVE_FETCH(request, init);
- if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;
- let obj = await response.clone().json(), serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
- for (region of obj.offeringSettings.regions) {
- let { name: regionName, name: shortName } = region;
- if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
- let match = serverRegex.exec(region.baseUri);
- if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
- else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);
- region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
- }
- BxEventBus.Script.emit("xcloud.server.ready", {});
- let preferredRegion = getPreferredServerRegion();
- if (preferredRegion && preferredRegion in STATES.serverRegions) {
- let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
- tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;
- }
- return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
- }
- static async handlePlay(request, init) {
- BxEventBus.Stream.emit("state.loading", {});
- let PREF_STREAM_TARGET_RESOLUTION = getPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];
- for (let regionName in STATES.serverRegions) {
- let region = STATES.serverRegions[regionName];
- if (region && parsedUrl.origin === region.baseUri) {
- badgeRegion = regionName;
- break;
- }
- }
- StreamBadges.getInstance().setRegion(badgeRegion);
- let clone = request.clone(), body = await clone.json(), headers = {};
- for (let pair of clone.headers.entries())
- headers[pair[0]] = pair[1];
- if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {
- let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);
- headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;
- }
- if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
- let newRequest = new Request(request, {
- body: JSON.stringify(body),
- headers
- });
- return NATIVE_FETCH(newRequest);
- }
- static async handleWaitTime(request, init) {
- let response = await NATIVE_FETCH(request, init);
- if (getPref("loadingScreen.waitTime.show")) {
- let json = await response.clone().json();
- if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
- }
- return response;
- }
- static async handleConfiguration(request, init) {
- if (request.method !== "GET") return NATIVE_FETCH(request, init);
- let response = await NATIVE_FETCH(request, init), text = await response.clone().text();
- if (!text.length) return response;
- BxEventBus.Stream.emit("state.starting", {});
- let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
- overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
- let overrideMkb = null;
- if (getPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;
- if (getPref("nativeMkb.mode") === "off") overrideMkb = !1;
- if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
- enableMouseInput: overrideMkb,
- enableKeyboardInput: overrideMkb
- });
- if (getPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;
- return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
- }
- static async handle(request, init) {
- let url = typeof request === "string" ? request : request.url;
- if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);
- else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);
- else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
- else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);
- else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
- return NATIVE_FETCH(request, init);
- }
+static SERVER_EXTRA_INFO = {
+EastUS: ["🇺🇸", "america-north"],
+EastUS2: ["🇺🇸", "america-north"],
+NorthCentralUs: ["🇺🇸", "america-north"],
+SouthCentralUS: ["🇺🇸", "america-north"],
+WestUS: ["🇺🇸", "america-north"],
+WestUS2: ["🇺🇸", "america-north"],
+MexicoCentral: ["🇲🇽", "america-north"],
+BrazilSouth: ["🇧🇷", "america-south"],
+JapanEast: ["🇯🇵", "asia"],
+KoreaCentral: ["🇰🇷", "asia"],
+AustraliaEast: ["🇦🇺", "australia"],
+AustraliaSouthEast: ["🇦🇺", "australia"],
+SwedenCentral: ["🇸🇪", "europe"],
+UKSouth: ["🇬🇧", "europe"],
+WestEurope: ["🇪🇺", "europe"]
+};
+static async handleLogin(request, init) {
+let bypassServer = getGlobalPref("server.bypassRestriction");
+if (bypassServer !== "off") {
+let ip = BypassServerIps[bypassServer];
+ip && request.headers.set("X-Forwarded-For", ip);
+}
+let response = await NATIVE_FETCH(request, init);
+if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;
+let obj = await response.clone().json(), serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
+for (region of obj.offeringSettings.regions) {
+let { name: regionName, name: shortName } = region;
+if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
+let match = serverRegex.exec(region.baseUri);
+if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
+else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);
+region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
+}
+BxEventBus.Script.emit("xcloud.server.ready", {});
+let preferredRegion = getPreferredServerRegion();
+if (preferredRegion && preferredRegion in STATES.serverRegions) {
+let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
+tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;
+}
+return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
+}
+static async handlePlay(request, init) {
+BxEventBus.Stream.emit("state.loading", {});
+let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];
+for (let regionName in STATES.serverRegions) {
+let region = STATES.serverRegions[regionName];
+if (region && parsedUrl.origin === region.baseUri) {
+badgeRegion = regionName;
+break;
+}
+}
+StreamBadges.getInstance().setRegion(badgeRegion);
+let clone = request.clone(), body = await clone.json(), headers = {};
+for (let pair of clone.headers.entries())
+headers[pair[0]] = pair[1];
+if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {
+let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);
+headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;
+}
+if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
+let newRequest = new Request(request, {
+body: JSON.stringify(body),
+headers
+});
+return NATIVE_FETCH(newRequest);
+}
+static async handleWaitTime(request, init) {
+let response = await NATIVE_FETCH(request, init);
+if (getGlobalPref("loadingScreen.waitTime.show")) {
+let json = await response.clone().json();
+if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
+}
+return response;
+}
+static async handleConfiguration(request, init) {
+if (request.method !== "GET") return NATIVE_FETCH(request, init);
+let response = await NATIVE_FETCH(request, init), text = await response.clone().text();
+if (!text.length) return response;
+BxEventBus.Stream.emit("state.starting", {});
+let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
+overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
+let overrideMkb = null;
+if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;
+if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;
+if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
+enableMouseInput: overrideMkb,
+enableKeyboardInput: overrideMkb
+});
+if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;
+return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+}
+static async handle(request, init) {
+let url = typeof request === "string" ? request : request.url;
+if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);
+else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);
+else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
+else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);
+else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
+return NATIVE_FETCH(request, init);
+}
}
function clearApplicationInsightsBuffers() {
- window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");
+window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");
}
function clearDbLogs(dbName, table) {
- let request = window.indexedDB.open(dbName);
- request.onsuccess = (e) => {
- let db = e.target.result;
- try {
- let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
- objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);
- } catch (ex) {}
- };
+let request = window.indexedDB.open(dbName);
+request.onsuccess = (e) => {
+let db = e.target.result;
+try {
+let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
+objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);
+} catch (ex) {}
+};
}
function clearAllLogs() {
- clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");
+clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");
}
function updateIceCandidates(candidates, options) {
- let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = [];
- for (let item of candidates) {
- if (item.candidate == "a=end-of-candidates") continue;
- let groups = pattern.exec(item.candidate).groups;
- lst.push(groups);
- }
- if (options.preferIpv6Server) lst.sort((a, b) => {
- let firstIp = a.ip, secondIp = b.ip;
- return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;
- });
- let newCandidates = [], foundation = 1, newCandidate = (candidate) => {
- return {
- candidate,
- messageType: "iceCandidate",
- sdpMLineIndex: "0",
- sdpMid: "0"
- };
- };
- if (lst.forEach((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;
+let 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;
+let groups = pattern.exec(item.candidate).groups;
+lst.push(groups);
+}
+if (options.preferIpv6Server) lst.sort((a, b) => {
+let firstIp = a.ip, secondIp = b.ip;
+return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;
+});
+let newCandidates = [], foundation = 1, newCandidate = (candidate) => {
+return {
+candidate,
+messageType: "iceCandidate",
+sdpMLineIndex: "0",
+sdpMid: "0"
+};
+};
+if (lst.forEach((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) {
- let response = await NATIVE_FETCH(request), text = await response.clone().text();
- if (!text.length) return response;
- let options = {
- preferIpv6Server: getPref("server.ipv6.prefer"),
- consoleAddrs
- }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse);
- return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+let response = await NATIVE_FETCH(request), text = await response.clone().text();
+if (!text.length) return response;
+let options = {
+preferIpv6Server: getGlobalPref("server.ipv6.prefer"),
+consoleAddrs
+}, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse);
+return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
function interceptHttpRequests() {
- let BLOCKED_URLS = [];
- if (getPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net");
- let blockFeatures2 = getPref("block.features");
- if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox");
- if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations");
- if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/");
- let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;
- xhrPrototype.open = function(method, url) {
- return this._url = url, nativeXhrOpen.apply(this, arguments);
- }, xhrPrototype.send = function(...arg) {
- for (let url of BLOCKED_URLS)
- if (this._url.startsWith(url)) {
- if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000);
- return BxLogger.warning("Blocked URL", url), !1;
- }
- return nativeXhrSend.apply(this, arguments);
- };
- let gamepassAllGames = [], IGNORED_DOMAINS = [
- "accounts.xboxlive.com",
- "chat.xboxlive.com",
- "notificationinbox.xboxlive.com",
- "peoplehub.xboxlive.com",
- "peoplehub-public.xboxlive.com",
- "rta.xboxlive.com",
- "userpresence.xboxlive.com",
- "xblmessaging.xboxlive.com",
- "consent.config.office.com",
- "arc.msn.com",
- "browser.events.data.microsoft.com",
- "dc.services.visualstudio.com",
- "2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
- ];
- window.BX_FETCH = window.fetch = async (request, init) => {
- let url = typeof request === "string" ? request : request.url;
- for (let blocked of BLOCKED_URLS)
- if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', {
- status: 200,
- statusText: "200 OK"
- });
- try {
- let domain = new URL(url).hostname;
- if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);
- } catch (e) {
- return NATIVE_FETCH(request, init);
- }
- if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {
- let response = await NATIVE_FETCH(request, init), json = await response.json();
- if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)
- json.exp.treatments[key] = FeatureGates[key];
- return response.json = () => Promise.resolve(json), response;
- } catch (e) {
- return console.log(e), NATIVE_FETCH(request, init);
- }
- if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {
- let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
- if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++)
- gamepassAllGames.push(obj[i].id);
- else if (!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")) {
- let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
- try {
- let 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 (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net");
+let blockFeatures2 = getGlobalPref("block.features");
+if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox");
+if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations");
+if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/");
+let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;
+xhrPrototype.open = function(method, url) {
+return this._url = url, nativeXhrOpen.apply(this, arguments);
+}, xhrPrototype.send = function(...arg) {
+for (let url of BLOCKED_URLS)
+if (this._url.startsWith(url)) {
+if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000);
+return BxLogger.warning("Blocked URL", url), !1;
+}
+return nativeXhrSend.apply(this, arguments);
+};
+let gamepassAllGames = [], IGNORED_DOMAINS = [
+"accounts.xboxlive.com",
+"chat.xboxlive.com",
+"notificationinbox.xboxlive.com",
+"peoplehub.xboxlive.com",
+"peoplehub-public.xboxlive.com",
+"rta.xboxlive.com",
+"userpresence.xboxlive.com",
+"xblmessaging.xboxlive.com",
+"consent.config.office.com",
+"arc.msn.com",
+"browser.events.data.microsoft.com",
+"dc.services.visualstudio.com",
+"2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
+];
+window.BX_FETCH = window.fetch = async (request, init) => {
+let url = typeof request === "string" ? request : request.url;
+for (let blocked of BLOCKED_URLS)
+if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', {
+status: 200,
+statusText: "200 OK"
+});
+try {
+let domain = new URL(url).hostname;
+if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);
+} catch (e) {
+return NATIVE_FETCH(request, init);
+}
+if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {
+let response = await NATIVE_FETCH(request, init), json = await response.json();
+if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)
+json.exp.treatments[key] = FeatureGates[key];
+return response.json = () => Promise.resolve(json), response;
+} catch (e) {
+return console.log(e), NATIVE_FETCH(request, init);
+}
+if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {
+let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
+if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++)
+gamepassAllGames.push(obj[i].id);
+else if (!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")) {
+let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
+try {
+let 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 generateMsDeviceInfo(osName) {
- return {
- appInfo: {
- env: {
- clientAppId: window.location.host,
- clientAppType: "browser",
- clientAppVersion: "26.1.97",
- clientSdkVersion: "10.3.7",
- httpEnvironment: "prod",
- sdkInstallId: ""
- }
- },
- dev: {
- os: { name: osName, ver: "22631.2715", platform: "desktop" },
- hw: { make: "Microsoft", model: "unknown", sdktype: "web" },
- browser: { browserName: "chrome", browserVersion: "130.0" },
- displayInfo: {
- dimensions: { widthInPixels: 1920, heightInPixels: 1080 },
- pixelDensity: { dpiX: 1, dpiY: 1 }
- }
- }
- };
+return {
+appInfo: {
+env: {
+clientAppId: window.location.host,
+clientAppType: "browser",
+clientAppVersion: "26.1.97",
+clientSdkVersion: "10.3.7",
+httpEnvironment: "prod",
+sdkInstallId: ""
+}
+},
+dev: {
+os: { name: osName, ver: "22631.2715", platform: "desktop" },
+hw: { make: "Microsoft", model: "unknown", sdktype: "web" },
+browser: { browserName: "chrome", browserVersion: "130.0" },
+displayInfo: {
+dimensions: { widthInPixels: 1920, heightInPixels: 1080 },
+pixelDensity: { dpiX: 1, dpiY: 1 }
+}
+}
+};
}
function getOsNameFromResolution(resolution) {
- let osName;
- switch (resolution) {
- case "1080p-hq":
- osName = "tizen";
- break;
- case "1080p":
- osName = "windows";
- break;
- default:
- osName = "android";
- break;
- }
- return osName;
+let osName;
+switch (resolution) {
+case "1080p-hq":
+osName = "tizen";
+break;
+case "1080p":
+osName = "windows";
+break;
+default:
+osName = "android";
+break;
+}
+return osName;
}
function addCss() {
- let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#df5656;--bx-danger-button-disabled-rgb:223,86,86;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb))}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;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 .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-contents > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-contents > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.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:6px}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getPref("ui.hideSections"), selectorToHide = [];
- if (selectorToHide.push("div[class*=SupportedInputsBadge] svg:first-of-type"), selectorToHide.push("div[class*=SupportedInputsBadge]:not(:has(:nth-child(2)))"), PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]");
- if (getPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]");
- if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]");
- if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');
- if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');
- if (getPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]");
- if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }";
- if (getPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}";
- if (getPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}";
- if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}";
- else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}";
- if (getPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}";
- let $style = CE("style", !1, 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-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = [];
+if (selectorToHide.push("div[class*=SupportedInputsBadge] svg:first-of-type"), selectorToHide.push("div[class*=SupportedInputsBadge]:not(:has(:nth-child(2)))"), PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]");
+if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]");
+if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]");
+if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');
+if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');
+if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]");
+if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }";
+if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}";
+if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}";
+if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}";
+else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}";
+if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}";
+let $style = CE("style", !1, css);
+document.documentElement.appendChild($style);
}
function preloadFonts() {
- let $link = CE("link", {
- rel: "preload",
- href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",
- as: "font",
- type: "font/otf",
- crossorigin: ""
- });
- document.querySelector("head")?.appendChild($link);
+let $link = CE("link", {
+rel: "preload",
+href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",
+as: "font",
+type: "font/otf",
+crossorigin: ""
+});
+document.querySelector("head")?.appendChild($link);
}
function patchHistoryMethod(type) {
- let orig = window.history[type];
- return function(...args) {
- return BxEvent.dispatch(window, BxEvent.POPSTATE, {
- arguments: args
- }), orig.apply(this, arguments);
- };
+let orig = window.history[type];
+return function(...args) {
+return BxEvent.dispatch(window, BxEvent.POPSTATE, {
+arguments: args
+}), orig.apply(this, arguments);
+};
}
function onHistoryChanged(e) {
- if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;
- NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});
+if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;
+NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});
}
function setCodecPreferences(sdp, preferredCodec) {
- let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];
- for (let match of matches) {
- let id = match[1];
- if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);
- }
- if (!preferredCodecIds.length) return sdp;
- let lines = sdp.split(`\r
+let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];
+for (let match of matches) {
+let id = match[1];
+if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);
+}
+if (!preferredCodecIds.length) return sdp;
+let lines = sdp.split(`\r
`);
- for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
- let line = lines[lineIndex];
- if (!line.startsWith("m=video")) continue;
- let tmp = line.trim().split(" "), ids = tmp.slice(3);
- ids = ids.filter((item) => !preferredCodecIds.includes(item)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");
- break;
- }
- return lines.join(`\r
+for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
+let line = lines[lineIndex];
+if (!line.startsWith("m=video")) continue;
+let tmp = line.trim().split(" "), ids = tmp.slice(3);
+ids = ids.filter((item) => !preferredCodecIds.includes(item)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");
+break;
+}
+return lines.join(`\r
`);
}
function patchSdpBitrate(sdp, video, audio) {
- let lines = sdp.split(`\r
+let lines = sdp.split(`\r
`), mediaSet = new Set;
- !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");
- let bitrate = {
- video,
- audio
- };
- for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {
- let media = "", line = lines[lineNumber];
- if (!line.startsWith("m=")) continue;
- for (let m of mediaSet)
- if (line.startsWith(`m=${m}`)) {
- media = m, mediaSet.delete(media);
- break;
- }
- if (!media) continue;
- let bLine = `b=AS:${bitrate[media]}`;
- while (lineNumber++, lineNumber < lines.length) {
- if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue;
- if (line.startsWith("b=AS:")) {
- lines[lineNumber] = bLine;
- break;
- }
- if (line.startsWith("m=")) {
- lines.splice(lineNumber, 0, bLine);
- break;
- }
- }
- }
- return lines.join(`\r
+!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");
+let bitrate = {
+video,
+audio
+};
+for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {
+let media = "", line = lines[lineNumber];
+if (!line.startsWith("m=")) continue;
+for (let m of mediaSet)
+if (line.startsWith(`m=${m}`)) {
+media = m, mediaSet.delete(media);
+break;
+}
+if (!media) continue;
+let bLine = `b=AS:${bitrate[media]}`;
+while (lineNumber++, lineNumber < lines.length) {
+if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue;
+if (line.startsWith("b=AS:")) {
+lines[lineNumber] = bLine;
+break;
+}
+if (line.startsWith("m=")) {
+lines.splice(lineNumber, 0, bLine);
+break;
+}
+}
+}
+return lines.join(`\r
`);
}
var clarity_boost_default = `#version 300 es
@@ -5909,568 +6276,558 @@ color = brightness * color;
fragColor = vec4(color, 1.0);
}`;
class WebGL2Player {
- LOG_TAG = "WebGL2Player";
- $video;
- $canvas;
- gl = null;
- resources = [];
- program = null;
- stopped = !1;
- options = {
- filterId: 1,
- sharpenFactor: 0,
- brightness: 0,
- contrast: 0,
- saturation: 0
- };
- targetFps = 60;
- frameInterval = 0;
- lastFrameTime = 0;
- animFrameId = null;
- constructor($video) {
- BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;
- let $canvas = document.createElement("canvas");
- $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
- }
- setFilter(filterId, update = !0) {
- this.options.filterId = filterId, update && this.updateCanvas();
- }
- setSharpness(sharpness, update = !0) {
- this.options.sharpenFactor = sharpness, update && this.updateCanvas();
- }
- setBrightness(brightness, update = !0) {
- this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
- }
- setContrast(contrast, update = !0) {
- this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
- }
- setSaturation(saturation, update = !0) {
- this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
- }
- setTargetFps(target) {
- this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
- }
- getCanvas() {
- return this.$canvas;
- }
- updateCanvas() {
- let gl = this.gl, program = this.program;
- gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
- }
- forceDrawFrame() {
- let gl = this.gl;
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
- }
- setupRendering() {
- let frameCallback;
- if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
- let $video = this.$video;
- frameCallback = $video.requestVideoFrameCallback.bind($video);
- } else frameCallback = requestAnimationFrame;
- let animate = () => {
- if (this.stopped) return;
- this.animFrameId = frameCallback(animate);
- let draw = !0;
- if (this.targetFps === 0) draw = !1;
- else if (this.targetFps < 60) {
- let currentTime = performance.now();
- if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
- else this.lastFrameTime = currentTime;
- }
- if (draw) {
- let gl = this.gl;
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
- }
- };
- this.animFrameId = frameCallback(animate);
- }
- setupShaders() {
- BxLogger.info(this.LOG_TAG, "Setting up", getPref("video.player.powerPreference"));
- let gl = this.$canvas.getContext("webgl2", {
- isBx: !0,
- antialias: !0,
- alpha: !1,
- powerPreference: getPref("video.player.powerPreference")
- });
- this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
- let vShader = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
- let fShader = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
- let program = gl.createProgram();
- if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
- this.updateCanvas();
- let buffer = gl.createBuffer();
- this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
- let texture = gl.createTexture();
- this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
- }
- resume() {
- this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
- }
- stop() {
- if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
- if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
- else cancelAnimationFrame(this.animFrameId);
- this.animFrameId = null;
- }
- }
- destroy() {
- BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
- let gl = this.gl;
- if (gl) {
- gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
- for (let resource of this.resources)
- if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
- else if (resource instanceof WebGLShader) gl.deleteShader(resource);
- else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
- else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
- this.gl = null;
- }
- if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);
- this.$canvas.width = 1, this.$canvas.height = 1;
- }
+LOG_TAG = "WebGL2Player";
+$video;
+$canvas;
+gl = null;
+resources = [];
+program = null;
+stopped = !1;
+options = {
+filterId: 1,
+sharpenFactor: 0,
+brightness: 0,
+contrast: 0,
+saturation: 0
+};
+targetFps = 60;
+frameInterval = 0;
+lastFrameTime = 0;
+animFrameId = null;
+constructor($video) {
+BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;
+let $canvas = document.createElement("canvas");
+$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
+}
+setFilter(filterId, update = !0) {
+this.options.filterId = filterId, update && this.updateCanvas();
+}
+setSharpness(sharpness, update = !0) {
+this.options.sharpenFactor = sharpness, update && this.updateCanvas();
+}
+setBrightness(brightness, update = !0) {
+this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
+}
+setContrast(contrast, update = !0) {
+this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
+}
+setSaturation(saturation, update = !0) {
+this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
+}
+setTargetFps(target) {
+this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
+}
+getCanvas() {
+return this.$canvas;
+}
+updateCanvas() {
+let gl = this.gl, program = this.program;
+gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
+}
+forceDrawFrame() {
+let gl = this.gl;
+gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
+}
+setupRendering() {
+let frameCallback;
+if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
+let $video = this.$video;
+frameCallback = $video.requestVideoFrameCallback.bind($video);
+} else frameCallback = requestAnimationFrame;
+let animate = () => {
+if (this.stopped) return;
+this.animFrameId = frameCallback(animate);
+let draw = !0;
+if (this.targetFps === 0) draw = !1;
+else if (this.targetFps < 60) {
+let currentTime = performance.now();
+if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
+else this.lastFrameTime = currentTime;
+}
+if (draw) {
+let gl = this.gl;
+gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
+}
+};
+this.animFrameId = frameCallback(animate);
+}
+setupShaders() {
+BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));
+let gl = this.$canvas.getContext("webgl2", {
+isBx: !0,
+antialias: !0,
+alpha: !1,
+powerPreference: getStreamPref("video.player.powerPreference")
+});
+this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
+let vShader = gl.createShader(gl.VERTEX_SHADER);
+gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
+let fShader = gl.createShader(gl.FRAGMENT_SHADER);
+gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
+let program = gl.createProgram();
+if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
+this.updateCanvas();
+let buffer = gl.createBuffer();
+this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
+let texture = gl.createTexture();
+this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
+}
+resume() {
+this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
+}
+stop() {
+if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
+if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
+else cancelAnimationFrame(this.animFrameId);
+this.animFrameId = null;
+}
+}
+destroy() {
+BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
+let gl = this.gl;
+if (gl) {
+gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
+for (let resource of this.resources)
+if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
+else if (resource instanceof WebGLShader) gl.deleteShader(resource);
+else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
+else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
+this.gl = null;
+}
+if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);
+this.$canvas.width = 1, this.$canvas.height = 1;
+}
}
class StreamPlayer {
- $video;
- playerType = "default";
- options = {};
- webGL2Player = null;
- $videoCss = null;
- $usmMatrix = null;
- constructor($video, type, options) {
- this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
- }
- setupVideoElements() {
- if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;
- let $fragment = document.createDocumentFragment();
- this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
- let $svg = CE("svg", {
- id: "bx-video-filters",
- xmlns: "http://www.w3.org/2000/svg",
- class: "bx-gone"
- }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {
- id: "bx-filter-usm",
- xmlns: "http://www.w3.org/2000/svg"
- }, this.$usmMatrix = CE("feConvolveMatrix", {
- id: "bx-filter-usm-matrix",
- order: "3",
- xmlns: "http://www.w3.org/2000/svg"
- }))));
- $fragment.appendChild($svg), document.documentElement.appendChild($fragment);
- }
- getVideoPlayerFilterStyle() {
- let filters = [], sharpness = this.options.sharpness || 0;
- if (this.options.processing === "usm" && sharpness != 0) {
- let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
- this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");
- }
- let saturation = this.options.saturation || 100;
- if (saturation != 100) filters.push(`saturate(${saturation}%)`);
- let contrast = this.options.contrast || 100;
- if (contrast != 100) filters.push(`contrast(${contrast}%)`);
- let brightness = this.options.brightness || 100;
- if (brightness != 100) filters.push(`brightness(${brightness}%)`);
- return filters.join(" ");
- }
- resizePlayer() {
- let PREF_RATIO = getPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;
- if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
- let targetWidth, targetHeight, targetObjectFit;
- if (PREF_RATIO.includes(":")) {
- let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
- if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
- else width = parentRect.width, height = width / videoRatio;
- width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();
- let $parent = $video.parentElement, position = getPref("video.position");
- if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {
- let padding = Math.floor((window.innerHeight - height) / 4);
- if (padding > 0) {
- if (position === "bottom-half") padding *= 3;
- $parent.style.paddingTop = padding + "px";
- }
- }
- targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
- } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
- if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));
- if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
- }
- setPlayerType(type, refreshPlayer = !1) {
- if (this.playerType !== type) {
- let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
- if (type === "webgl2") {
- if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
- else this.webGL2Player.resume();
- this.$videoCss.textContent = "", this.$video.classList.add(videoClass);
- } else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);
- }
- this.playerType = type, refreshPlayer && this.refreshPlayer();
- }
- setOptions(options, refreshPlayer = !1) {
- this.options = options, refreshPlayer && this.refreshPlayer();
- }
- updateOptions(options, refreshPlayer = !1) {
- this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
- }
- getPlayerElement(playerType) {
- if (typeof playerType === "undefined") playerType = this.playerType;
- if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
- return this.$video;
- }
- getWebGL2Player() {
- return this.webGL2Player;
- }
- refreshPlayer() {
- if (this.playerType === "webgl2") {
- let options = this.options, webGL2Player = this.webGL2Player;
- if (options.processing === "usm") webGL2Player.setFilter(1);
- else webGL2Player.setFilter(2);
- 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();
- }
+$video;
+playerType = "default";
+options = {};
+webGL2Player = null;
+$videoCss = null;
+$usmMatrix = null;
+constructor($video, type, options) {
+this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
+}
+setupVideoElements() {
+if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;
+let $fragment = document.createDocumentFragment();
+this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
+let $svg = CE("svg", {
+id: "bx-video-filters",
+xmlns: "http://www.w3.org/2000/svg",
+class: "bx-gone"
+}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {
+id: "bx-filter-usm",
+xmlns: "http://www.w3.org/2000/svg"
+}, this.$usmMatrix = CE("feConvolveMatrix", {
+id: "bx-filter-usm-matrix",
+order: "3",
+xmlns: "http://www.w3.org/2000/svg"
+}))));
+$fragment.appendChild($svg), document.documentElement.appendChild($fragment);
+}
+getVideoPlayerFilterStyle() {
+let filters = [], sharpness = this.options.sharpness || 0;
+if (this.options.processing === "usm" && sharpness != 0) {
+let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
+this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");
+}
+let saturation = this.options.saturation || 100;
+if (saturation != 100) filters.push(`saturate(${saturation}%)`);
+let contrast = this.options.contrast || 100;
+if (contrast != 100) filters.push(`contrast(${contrast}%)`);
+let brightness = this.options.brightness || 100;
+if (brightness != 100) filters.push(`brightness(${brightness}%)`);
+return filters.join(" ");
+}
+resizePlayer() {
+let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;
+if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
+let targetWidth, targetHeight, targetObjectFit;
+if (PREF_RATIO.includes(":")) {
+let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
+if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
+else width = parentRect.width, height = width / videoRatio;
+width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();
+let $parent = $video.parentElement, position = getStreamPref("video.position");
+if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {
+let padding = Math.floor((window.innerHeight - height) / 4);
+if (padding > 0) {
+if (position === "bottom-half") padding *= 3;
+$parent.style.paddingTop = padding + "px";
+}
+}
+targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
+} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
+if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));
+if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
+}
+setPlayerType(type, refreshPlayer = !1) {
+if (this.playerType !== type) {
+let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
+if (type === "webgl2") {
+if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
+else this.webGL2Player.resume();
+this.$videoCss.textContent = "", this.$video.classList.add(videoClass);
+} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);
+}
+this.playerType = type, refreshPlayer && this.refreshPlayer();
+}
+setOptions(options, refreshPlayer = !1) {
+this.options = options, refreshPlayer && this.refreshPlayer();
+}
+updateOptions(options, refreshPlayer = !1) {
+this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
+}
+getPlayerElement(playerType) {
+if (typeof playerType === "undefined") playerType = this.playerType;
+if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
+return this.$video;
+}
+getWebGL2Player() {
+return this.webGL2Player;
+}
+refreshPlayer() {
+if (this.playerType === "webgl2") {
+let options = this.options, webGL2Player = this.webGL2Player;
+if (options.processing === "usm") webGL2Player.setFilter(1);
+else webGL2Player.setFilter(2);
+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() {
- let PREF_SKIP_SPLASH_VIDEO = getPref("ui.splashVideo.skip"), showFunc = function() {
- if (this.style.visibility = "visible", !this.videoWidth) return;
- let playerOptions = {
- processing: getPref("video.processing"),
- sharpness: getPref("video.processing.sharpness"),
- saturation: getPref("video.saturation"),
- contrast: getPref("video.contrast"),
- brightness: getPref("video.brightness")
- };
- STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {
- $video: this
- });
- }, nativePlay = HTMLMediaElement.prototype.play;
- HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {
- if (this.className && this.className.startsWith("XboxSplashVideo")) {
- if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});
- return nativePlay.apply(this);
- }
- let $parent = this.parentElement;
- if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });
- return nativePlay.apply(this);
- };
+let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {
+if (this.style.visibility = "visible", !this.videoWidth) return;
+let playerOptions = {
+processing: getStreamPref("video.processing"),
+sharpness: getStreamPref("video.processing.sharpness"),
+saturation: getStreamPref("video.saturation"),
+contrast: getStreamPref("video.contrast"),
+brightness: getStreamPref("video.brightness")
+};
+STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {
+$video: this
+});
+}, nativePlay = HTMLMediaElement.prototype.play;
+HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {
+if (this.className && this.className.startsWith("XboxSplashVideo")) {
+if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});
+return nativePlay.apply(this);
+}
+let $parent = this.parentElement;
+if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });
+return nativePlay.apply(this);
+};
}
function patchRtcCodecs() {
- if (getPref("stream.video.codecProfile") === "default") return;
- if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;
+if (getGlobalPref("stream.video.codecProfile") === "default") return;
+if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;
}
function patchRtcPeerConnection() {
- let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
- RTCPeerConnection.prototype.createDataChannel = function() {
- let dataChannel = nativeCreateDataChannel.apply(this, arguments);
- return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;
- };
- let maxVideoBitrateDef = getPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getPref("stream.video.maxBitrate"), codec = getPref("stream.video.codecProfile");
- if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {
- let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
- RTCPeerConnection.prototype.setLocalDescription = function(description) {
- if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);
- try {
- if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
- } catch (e) {
- BxLogger.error("setLocalDescription", e);
- }
- return nativeSetLocalDescription.apply(this, arguments);
- };
- }
- let OrgRTCPeerConnection = window.RTCPeerConnection;
- window.RTCPeerConnection = function() {
- let conn = new OrgRTCPeerConnection;
- return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {
- BxLogger.info("connectionstatechange", conn.connectionState);
- }), conn;
- };
+let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
+RTCPeerConnection.prototype.createDataChannel = function() {
+let dataChannel = nativeCreateDataChannel.apply(this, arguments);
+return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;
+};
+let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile");
+if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {
+let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
+RTCPeerConnection.prototype.setLocalDescription = function(description) {
+if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);
+try {
+if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
+} catch (e) {
+BxLogger.error("setLocalDescription", e);
+}
+return nativeSetLocalDescription.apply(this, arguments);
+};
+}
+let OrgRTCPeerConnection = window.RTCPeerConnection;
+window.RTCPeerConnection = function() {
+let conn = new OrgRTCPeerConnection;
+return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {
+BxLogger.info("connectionstatechange", conn.connectionState);
+}), conn;
+};
}
function patchAudioContext() {
- let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;
- window.AudioContext = function(options) {
- if (options && options.latencyHint) options.latencyHint = 0;
- let ctx = new OrgAudioContext(options);
- return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {
- let gainNode = nativeCreateGain.apply(this);
- return gainNode.gain.value = getPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;
- }, STATES.currentStream.audioContext = ctx, ctx;
- };
+let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;
+window.AudioContext = function(options) {
+if (options && options.latencyHint) options.latencyHint = 0;
+let ctx = new OrgAudioContext(options);
+return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {
+let gainNode = nativeCreateGain.apply(this);
+return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;
+}, STATES.currentStream.audioContext = ctx, ctx;
+};
}
function patchMeControl() {
- let overrideConfigs = {
- enableAADTelemetry: !1,
- enableTelemetry: !1,
- telEvs: "",
- oneDSUrl: ""
- }, MSA = {
- MeControl: {
- API: {
- setDisplayMode: () => {},
- setMobileState: () => {},
- addEventListener: () => {},
- removeEventListener: () => {}
- }
- }
- }, MeControl = {}, MsaHandler = {
- get(target, prop, receiver) {
- return target[prop];
- },
- set(obj, prop, value) {
- if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs);
- return obj[prop] = value, !0;
- }
- }, MeControlHandler = {
- get(target, prop, receiver) {
- return target[prop];
- },
- set(obj, prop, value) {
- if (prop === "Config") value = Object.assign(value, overrideConfigs);
- return obj[prop] = value, !0;
- }
- };
- window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);
+let overrideConfigs = {
+enableAADTelemetry: !1,
+enableTelemetry: !1,
+telEvs: "",
+oneDSUrl: ""
+}, MSA = {
+MeControl: {
+API: {
+setDisplayMode: () => {},
+setMobileState: () => {},
+addEventListener: () => {},
+removeEventListener: () => {}
+}
+}
+}, MeControl = {}, MsaHandler = {
+get(target, prop, receiver) {
+return target[prop];
+},
+set(obj, prop, value) {
+if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs);
+return obj[prop] = value, !0;
+}
+}, MeControlHandler = {
+get(target, prop, receiver) {
+return target[prop];
+},
+set(obj, prop, value) {
+if (prop === "Config") value = Object.assign(value, overrideConfigs);
+return obj[prop] = value, !0;
+}
+};
+window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);
}
function disableAdobeAudienceManager() {
- Object.defineProperty(window, "adobe", {
- get() {
- return Object.freeze({});
- }
- });
+Object.defineProperty(window, "adobe", {
+get() {
+return Object.freeze({});
+}
+});
}
function patchCanvasContext() {
- let nativeGetContext = HTMLCanvasElement.prototype.getContext;
- HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
- if (contextType.includes("webgl")) {
- if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {
- if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power";
- }
- }
- return nativeGetContext.apply(this, [contextType, contextAttributes]);
- };
+let nativeGetContext = HTMLCanvasElement.prototype.getContext;
+HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
+if (contextType.includes("webgl")) {
+if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {
+if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power";
+}
+}
+return nativeGetContext.apply(this, [contextType, contextAttributes]);
+};
}
class StreamUiHandler {
- static $btnStreamSettings;
- static $btnStreamStats;
- static $btnRefresh;
- static $btnHome;
- static observer;
- static cloneStreamHudButton($btnOrg, label, svgIcon) {
- if (!$btnOrg) return null;
- let $container = $btnOrg.cloneNode(!0), timeout;
- if (STATES.browser.capabilities.touch) {
- let onTransitionStart = (e) => {
- if (e.propertyName !== "opacity") return;
- timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none";
- }, onTransitionEnd = (e) => {
- if (e.propertyName !== "opacity") return;
- let $streamHud = e.target.closest("#StreamHud");
- if (!$streamHud) return;
- if ($streamHud.style.left === "0px") {
- let $target = e.target;
- timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {
- $target.style.pointerEvents = "auto";
- }, 100);
- }
- };
- $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);
- }
- let $button = $container.querySelector("button");
- if (!$button) return null;
- $button.setAttribute("title", label);
- let $orgSvg = $button.querySelector("svg");
- if (!$orgSvg) return null;
- let $svg = createSvgIcon(svgIcon);
- return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;
- }
- static cloneCloseButton($btnOrg, icon, className, onChange) {
- if (!$btnOrg) return null;
- let $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);
- return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;
- }
- static async handleStreamMenu() {
- let $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
- if (!$btnCloseHud) return;
- let { $btnRefresh, $btnHome } = StreamUiHandler;
- if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {
- confirm(t("confirm-reload-stream")) && window.location.reload();
- });
- if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {
- confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
- });
- if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome);
- document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render());
- }
- static handleSystemMenu($streamHud) {
- let $orgButton = $streamHud.querySelector("div[class^=HUDButton]");
- if (!$orgButton) return;
- let hideGripHandle = () => {
- let $gripHandle = document.querySelector("#StreamHud button[class^=GripHandle]");
- if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();
- }, $btnStreamSettings = StreamUiHandler.$btnStreamSettings;
- if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => {
- hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show();
- }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings;
- let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats;
- if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => {
- hideGripHandle(), e.preventDefault(), await streamStats.toggle();
- let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
- $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);
- }), StreamUiHandler.$btnStreamStats = $btnStreamStats;
- let $btnParent = $orgButton.parentElement;
- if ($btnStreamSettings && $btnStreamStats) {
- let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
- $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);
- }
- let $dotsButton = $btnParent.lastElementChild;
- $dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);
- }
- static reset() {
- StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0;
- }
- static observe() {
- StreamUiHandler.reset();
- let $screen = document.querySelector("#PageContent section[class*=PureScreens]");
- if (!$screen) return;
- let observer = new MutationObserver((mutationList) => {
- let item;
- for (item of mutationList) {
- if (item.type !== "childList") continue;
- item.addedNodes.forEach(async ($node) => {
- if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return;
- let $elm = $node;
- if (!($elm instanceof HTMLElement)) return;
- let className = $elm.className || "";
- if (className.includes("PureErrorPage")) {
- BxEventBus.Stream.emit("state.error", {});
- return;
- }
- if (className.startsWith("StreamMenu-module__container")) {
- StreamUiHandler.handleStreamMenu();
- return;
- }
- if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud");
- if (!$elm || ($elm.id || "") !== "StreamHud") return;
- StreamUiHandler.handleSystemMenu($elm);
- });
- }
- });
- observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer;
- }
+static $btnStreamSettings;
+static $btnStreamStats;
+static $btnRefresh;
+static $btnHome;
+static observer;
+static cloneStreamHudButton($btnOrg, label, svgIcon) {
+if (!$btnOrg) return null;
+let $container = $btnOrg.cloneNode(!0), timeout;
+if (STATES.browser.capabilities.touch) {
+let onTransitionStart = (e) => {
+if (e.propertyName !== "opacity") return;
+timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none";
+}, onTransitionEnd = (e) => {
+if (e.propertyName !== "opacity") return;
+let $streamHud = e.target.closest("#StreamHud");
+if (!$streamHud) return;
+if ($streamHud.style.left === "0px") {
+let $target = e.target;
+timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {
+$target.style.pointerEvents = "auto";
+}, 100);
+}
+};
+$container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);
+}
+let $button = $container.querySelector("button");
+if (!$button) return null;
+$button.setAttribute("title", label);
+let $orgSvg = $button.querySelector("svg");
+if (!$orgSvg) return null;
+let $svg = createSvgIcon(svgIcon);
+return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;
+}
+static cloneCloseButton($btnOrg, icon, className, onChange) {
+if (!$btnOrg) return null;
+let $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);
+return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;
+}
+static async handleStreamMenu() {
+let $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
+if (!$btnCloseHud) return;
+let { $btnRefresh, $btnHome } = StreamUiHandler;
+if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {
+confirm(t("confirm-reload-stream")) && window.location.reload();
+});
+if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {
+confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
+});
+if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome);
+document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render());
+}
+static handleSystemMenu($streamHud) {
+let $orgButton = $streamHud.querySelector("div[class^=HUDButton]");
+if (!$orgButton) return;
+let hideGripHandle = () => {
+let $gripHandle = document.querySelector("#StreamHud button[class^=GripHandle]");
+if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();
+}, $btnStreamSettings = StreamUiHandler.$btnStreamSettings;
+if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => {
+hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show();
+}), StreamUiHandler.$btnStreamSettings = $btnStreamSettings;
+let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats;
+if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => {
+hideGripHandle(), e.preventDefault(), await streamStats.toggle();
+let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
+$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);
+}), StreamUiHandler.$btnStreamStats = $btnStreamStats;
+let $btnParent = $orgButton.parentElement;
+if ($btnStreamSettings && $btnStreamStats) {
+let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
+$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);
+}
+let $dotsButton = $btnParent.lastElementChild;
+$dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);
+}
+static reset() {
+StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0;
+}
+static observe() {
+StreamUiHandler.reset();
+let $screen = document.querySelector("#PageContent section[class*=PureScreens]");
+if (!$screen) return;
+let observer = new MutationObserver((mutationList) => {
+let item;
+for (item of mutationList) {
+if (item.type !== "childList") continue;
+item.addedNodes.forEach(async ($node) => {
+if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return;
+let $elm = $node;
+if (!($elm instanceof HTMLElement)) return;
+let className = $elm.className || "";
+if (className.includes("PureErrorPage")) {
+BxEventBus.Stream.emit("state.error", {});
+return;
+}
+if (className.startsWith("StreamMenu-module__container")) {
+StreamUiHandler.handleStreamMenu();
+return;
+}
+if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud");
+if (!$elm || ($elm.id || "") !== "StreamHud") return;
+StreamUiHandler.handleSystemMenu($elm);
+});
+}
+});
+observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer;
}
-class XboxApi {
- static CACHED_TITLES = {};
- static async getProductTitle(xboxTitleId) {
- if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];
- try {
- let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;
- return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle;
- } catch (e) {}
- return;
- }
}
class RootDialogObserver {
- static $btnShortcut = AppInterface && createButton({
- icon: BxIcon.CREATE_SHORTCUT,
- label: t("create-shortcut"),
- style: 64 | 8 | 128 | 4096 | 8192,
- onClick: (e) => {
- window.BX_EXPOSED.dialogRoutes?.closeAll();
- let $btn = e.target.closest("button");
- AppInterface.createShortcut($btn?.dataset.path);
- }
- });
- static $btnWallpaper = AppInterface && createButton({
- icon: BxIcon.DOWNLOAD,
- label: t("wallpaper"),
- style: 64 | 8 | 128 | 4096 | 8192,
- onClick: (e) => {
- window.BX_EXPOSED.dialogRoutes?.closeAll();
- let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);
- details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
- }
- });
- static handleGameCardMenu($root) {
- let $detail = $root.querySelector('a[href^="/play/"]');
- if (!$detail) return;
- let path = $detail.getAttribute("href");
- RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
- }
- static handleAddedElement($root, $addedElm) {
- if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {
- let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");
- if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;
- } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;
- return !1;
- }
- static observe($root) {
- let beingShown = !1;
- new MutationObserver((mutationList) => {
- for (let mutation of mutationList) {
- if (mutation.type !== "childList") continue;
- if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
- let $addedElm = mutation.addedNodes[0];
- if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
- }
- let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
- if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});
- }
- }).observe($root, { subtree: !0, childList: !0 });
- }
- static waitForRootDialog() {
- let observer = new MutationObserver((mutationList) => {
- for (let mutation of mutationList) {
- if (mutation.type !== "childList") continue;
- let $target = mutation.target;
- if ($target.id && $target.id === "gamepass-dialog-root") {
- observer.disconnect(), RootDialogObserver.observe($target);
- break;
- }
- }
- });
- observer.observe(document.documentElement, { subtree: !0, childList: !0 });
- }
+static $btnShortcut = AppInterface && createButton({
+icon: BxIcon.CREATE_SHORTCUT,
+label: t("create-shortcut"),
+style: 64 | 8 | 128 | 4096 | 8192,
+onClick: (e) => {
+window.BX_EXPOSED.dialogRoutes?.closeAll();
+let $btn = e.target.closest("button");
+AppInterface.createShortcut($btn?.dataset.path);
}
+});
+static $btnWallpaper = AppInterface && createButton({
+icon: BxIcon.DOWNLOAD,
+label: t("wallpaper"),
+style: 64 | 8 | 128 | 4096 | 8192,
+onClick: (e) => {
+window.BX_EXPOSED.dialogRoutes?.closeAll();
+let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);
+details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
+}
+});
+static handleGameCardMenu($root) {
+let $detail = $root.querySelector('a[href^="/play/"]');
+if (!$detail) return;
+let path = $detail.getAttribute("href");
+RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
+}
+static handleAddedElement($root, $addedElm) {
+if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {
+let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");
+if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;
+} else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;
+return !1;
+}
+static observe($root) {
+let beingShown = !1;
+new MutationObserver((mutationList) => {
+for (let mutation of mutationList) {
+if (mutation.type !== "childList") continue;
+if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
+let $addedElm = mutation.addedNodes[0];
+if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
+}
+let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
+if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});
+}
+}).observe($root, { subtree: !0, childList: !0 });
+}
+static waitForRootDialog() {
+let observer = new MutationObserver((mutationList) => {
+for (let mutation of mutationList) {
+if (mutation.type !== "childList") continue;
+let $target = mutation.target;
+if ($target.id && $target.id === "gamepass-dialog-root") {
+observer.disconnect(), RootDialogObserver.observe($target);
+break;
+}
+}
+});
+observer.observe(document.documentElement, { subtree: !0, childList: !0 });
+}
+}
+SettingsManager.getInstance();
if (window.location.pathname.includes("/auth/msa")) {
- let nativePushState = window.history.pushState;
- throw window.history.pushState = function(...args) {
- let url = args[2];
- if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) {
- console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url;
- return;
- }
- return nativePushState.apply(this, arguments);
- }, new Error("[Better xCloud] Refreshing the page after logging in");
+let nativePushState = window.history.pushState;
+throw window.history.pushState = function(...args) {
+let url = args[2];
+if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) {
+console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url;
+return;
+}
+return nativePushState.apply(this, arguments);
+}, new Error("[Better xCloud] Refreshing the page after logging in");
}
BxLogger.info("readyState", document.readyState);
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) ;
- else window.setTimeout(HeaderSection.watchHeader, 2000);
- if (getPref("ui.hideSections").includes("friends") || getPref("block.features").includes("friends")) {
- let $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) ;
+else window.setTimeout(HeaderSection.watchHeader, 2000);
+if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {
+let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");
+$parent && ($parent.style.display = "none");
+}
+preloadFonts();
});
window.BX_EXPOSED = BxExposed;
window.addEventListener(BxEvent.POPSTATE, onHistoryChanged);
@@ -6478,50 +6835,52 @@ window.addEventListener("popstate", onHistoryChanged);
window.history.pushState = patchHistoryMethod("pushState");
window.history.replaceState = patchHistoryMethod("replaceState");
BxEventBus.Script.once("xcloud.server.unavailable", () => {
- if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();
+if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();
});
BxEventBus.Script.on("xcloud.server.ready", () => {
- STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);
+STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);
});
BxEventBus.Stream.on("state.loading", () => {
- if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
- else STATES.currentStream.titleSlug = "remote-play";
+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("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.ready", LoadingScreen.setup);
+getGlobalPref("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.ready", LoadingScreen.setup);
BxEventBus.Stream.on("state.starting", () => {
- LoadingScreen.hide();
+LoadingScreen.hide();
});
BxEventBus.Stream.on("state.playing", (payload) => {
- STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer();
+STATES.isPlaying = !0, StreamUiHandler.observe(), updateVideoPlayer();
});
BxEventBus.Stream.on("state.error", () => {
- BxEventBus.Stream.emit("state.stopped", {});
+BxEventBus.Stream.emit("state.stopped", {});
});
BxEventBus.Stream.on("dataChannelCreated", (payload) => {
- let { dataChannel } = payload;
- if (dataChannel?.label !== "message") return;
- dataChannel.addEventListener("message", async (msg) => {
- if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
- if (!msg.data.includes("/titleinfo")) return;
- let 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) {
- let productTitle = await XboxApi.getProductTitle(xboxTitleId);
- if (productTitle) STATES.currentStream.titleSlug = productTitleToSlug(productTitle);
- }
- }
- });
+let { dataChannel } = payload;
+if (dataChannel?.label !== "message") return;
+dataChannel.addEventListener("message", async (msg) => {
+if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
+if (!msg.data.includes("/titleinfo")) return;
+let currentStream = STATES.currentStream, json = JSON.parse(JSON.parse(msg.data).content), currentId = currentStream.xboxTitleId ?? null, newId = parseInt(json.titleid, 16);
+if (STATES.remotePlay.isPlaying) if (currentStream.titleSlug = "remote-play", json.focused) {
+let productTitle = await XboxApi.getProductTitle(newId);
+if (productTitle) currentStream.titleSlug = productTitleToSlug(productTitle);
+else newId = -1;
+} else newId = 0;
+if (currentId !== newId) currentStream.xboxTitleId = newId, BxEventBus.Stream.emit("xboxTitleId.changed", {
+id: newId
+});
+});
});
function unload() {
- if (!STATES.isPlaying) return;
- STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy();
+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().destroy(), StreamBadges.getInstance().destroy();
}
BxEventBus.Stream.on("state.stopped", unload);
window.addEventListener("pagehide", (e) => {
- BxEventBus.Stream.emit("state.stopped", {});
+BxEventBus.Stream.emit("state.stopped", {});
});
function main() {
- if (GhPagesUtils.fetchLatestCommit(), StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getPref("audio.volume.booster.enabled") && patchAudioContext(), getPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
- if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
+if (GhPagesUtils.fetchLatestCommit(), StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
+if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
}
main();
diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js
index 6d57f5b..70403e2 100755
--- a/dist/better-xcloud.user.js
+++ b/dist/better-xcloud.user.js
@@ -14,4502 +14,4810 @@
// ==/UserScript==
"use strict";
class BxLogger {
- static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args);
- static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args);
- static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);
- static log(color, tag, ...args) {
- console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
- }
+static info = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#008746", tag, ...args);
+static warning = (tag, ...args) => BX_FLAGS.Debug && BxLogger.log("#c1a404", tag, ...args);
+static error = (tag, ...args) => BxLogger.log("#c10404", tag, ...args);
+static log(color, tag, ...args) {
+console.log("%c[BxC]", `color:${color};font-weight:bold;`, tag, "//", ...args);
+}
}
window.BxLogger = BxLogger;
/* ADDITIONAL CODE */
var DEFAULT_FLAGS = {
- Debug: !1,
- CheckForUpdate: !0,
- EnableXcloudLogging: !1,
- SafariWorkaround: !0,
- ForceNativeMkbTitles: [],
- FeatureGates: null,
- DeviceInfo: {
- deviceType: "unknown"
- }
+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 ALL_PREFS = {
- global: [
- "audio.mic.onPlaying",
- "audio.volume.booster.enabled",
- "block.features",
- "block.tracking",
- "gameBar.position",
- "game.fortnite.forceConsole",
- "loadingScreen.gameArt.show",
- "loadingScreen.rocket",
- "loadingScreen.waitTime.show",
- "mkb.enabled",
- "mkb.cursor.hideIdle",
- "nativeMkb.forcedGames",
- "nativeMkb.mode",
- "xhome.enabled",
- "xhome.video.resolution",
- "screenshot.applyFilters",
- "server.bypassRestriction",
- "server.ipv6.prefer",
- "server.region",
- "stream.video.codecProfile",
- "stream.video.combineAudio",
- "stream.video.maxBitrate",
- "stream.locale",
- "stream.video.resolution",
- "touchController.autoOff",
- "touchController.opacity.default",
- "touchController.mode",
- "touchController.style.custom",
- "touchController.style.standard",
- "ui.controllerFriendly",
- "ui.controllerStatus.show",
- "ui.feedbackDialog.disabled",
- "ui.gameCard.waitTime.show",
- "ui.hideSections",
- "ui.systemMenu.hideHandle",
- "ui.imageQuality",
- "ui.layout",
- "ui.reduceAnimations",
- "ui.hideScrollbar",
- "ui.streamMenu.simplify",
- "ui.splashVideo.skip",
- "version.current",
- "version.lastCheck",
- "version.latest",
- "bx.locale",
- "userAgent.profile"
- ],
- stream: [
- "audio.volume",
- "controller.pollingRate",
- "controller.settings",
- "deviceVibration.intensity",
- "deviceVibration.mode",
- "keyboardShortcuts.preset.inGameId",
- "localCoOp.enabled",
- "mkb.p1.preset.mappingId",
- "mkb.p1.slot",
- "mkb.p2.preset.mappingId",
- "mkb.p2.slot",
- "nativeMkb.scroll.sensitivityX",
- "nativeMkb.scroll.sensitivityY",
- "stats.colors",
- "stats.items",
- "stats.opacity.all",
- "stats.opacity.background",
- "stats.position",
- "stats.quickGlance.enabled",
- "stats.showWhenPlaying",
- "stats.textSize",
- "video.brightness",
- "video.contrast",
- "video.maxFps",
- "video.player.type",
- "video.position",
- "video.player.powerPreference",
- "video.processing",
- "video.ratio",
- "video.saturation",
- "video.processing.sharpness"
- ]
+global: [
+"audio.mic.onPlaying",
+"audio.volume.booster.enabled",
+"block.features",
+"block.tracking",
+"gameBar.position",
+"game.fortnite.forceConsole",
+"loadingScreen.gameArt.show",
+"loadingScreen.rocket",
+"loadingScreen.waitTime.show",
+"mkb.enabled",
+"mkb.cursor.hideIdle",
+"nativeMkb.forcedGames",
+"nativeMkb.mode",
+"xhome.enabled",
+"xhome.video.resolution",
+"screenshot.applyFilters",
+"server.bypassRestriction",
+"server.ipv6.prefer",
+"server.region",
+"stream.video.codecProfile",
+"stream.video.combineAudio",
+"stream.video.maxBitrate",
+"stream.locale",
+"stream.video.resolution",
+"touchController.autoOff",
+"touchController.opacity.default",
+"touchController.mode",
+"touchController.style.custom",
+"touchController.style.standard",
+"ui.controllerFriendly",
+"ui.controllerStatus.show",
+"ui.feedbackDialog.disabled",
+"ui.gameCard.waitTime.show",
+"ui.hideSections",
+"ui.systemMenu.hideHandle",
+"ui.imageQuality",
+"ui.layout",
+"ui.reduceAnimations",
+"ui.hideScrollbar",
+"ui.streamMenu.simplify",
+"ui.splashVideo.skip",
+"version.current",
+"version.lastCheck",
+"version.latest",
+"bx.locale",
+"userAgent.profile"
+],
+stream: [
+"audio.volume",
+"controller.pollingRate",
+"controller.settings",
+"deviceVibration.intensity",
+"deviceVibration.mode",
+"keyboardShortcuts.preset.inGameId",
+"localCoOp.enabled",
+"mkb.p1.preset.mappingId",
+"mkb.p1.slot",
+"mkb.p2.preset.mappingId",
+"mkb.p2.slot",
+"nativeMkb.scroll.sensitivityX",
+"nativeMkb.scroll.sensitivityY",
+"stats.colors",
+"stats.items",
+"stats.opacity.all",
+"stats.opacity.background",
+"stats.position",
+"stats.quickGlance.enabled",
+"stats.showWhenPlaying",
+"stats.textSize",
+"video.brightness",
+"video.contrast",
+"video.maxFps",
+"video.player.type",
+"video.position",
+"video.player.powerPreference",
+"video.processing",
+"video.ratio",
+"video.saturation",
+"video.processing.sharpness"
+]
};
var SMART_TV_UNIQUE_ID = "FC4A1DA2-711C-4E9C-BC7F-047AF8A672EA", CHROMIUM_VERSION = "125.0.0.0";
if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {
- let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
- if (match) CHROMIUM_VERSION = match[1];
+let match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
+if (match) CHROMIUM_VERSION = match[1];
}
class UserAgent {
- static STORAGE_KEY = "BetterXcloud.UserAgent";
- static #config;
- static #isMobile = null;
- static #isSafari = null;
- static #isSafariMobile = null;
- static #USER_AGENTS = {
- "windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
- "macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
- "smarttv-generic": `${window.navigator.userAgent} Smart-TV`,
- "smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
- "vr-oculus": window.navigator.userAgent + " OculusBrowser VR"
- };
- static init() {
- if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";
- if (!UserAgent.#config.custom) UserAgent.#config.custom = "";
- UserAgent.spoof();
- }
- static updateStorage(profile, custom) {
- let config = UserAgent.#config;
- if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;
- window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));
- }
- static getDefault() {
- return window.navigator.orgUserAgent || window.navigator.userAgent;
- }
- static get(profile) {
- let defaultUserAgent = window.navigator.userAgent;
- switch (profile) {
- case "default":
- return defaultUserAgent;
- case "custom":
- return UserAgent.#config.custom || defaultUserAgent;
- default:
- return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
- }
- }
- static isSafari() {
- if (this.#isSafari !== null) return this.#isSafari;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");
- return this.#isSafari = result, result;
- }
- static isSafariMobile() {
- if (this.#isSafariMobile !== null) return this.#isSafariMobile;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");
- return this.#isSafariMobile = result, result;
- }
- static isMobile() {
- if (this.#isMobile !== null) return this.#isMobile;
- let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);
- return this.#isMobile = result, result;
- }
- static spoof() {
- let profile = UserAgent.#config.profile;
- if (profile === "default") return;
- let newUserAgent = UserAgent.get(profile);
- if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});
- window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {
- value: newUserAgent
- });
- }
+static STORAGE_KEY = "BetterXcloud.UserAgent";
+static #config;
+static #isMobile = null;
+static #isSafari = null;
+static #isSafariMobile = null;
+static #USER_AGENTS = {
+"windows-edge": `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
+"macos-safari": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
+"smarttv-generic": `${window.navigator.userAgent} Smart-TV`,
+"smarttv-tizen": `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
+"vr-oculus": window.navigator.userAgent + " OculusBrowser VR"
+};
+static init() {
+if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile) UserAgent.#config.profile = BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default";
+if (!UserAgent.#config.custom) UserAgent.#config.custom = "";
+UserAgent.spoof();
+}
+static updateStorage(profile, custom) {
+let config = UserAgent.#config;
+if (config.profile = profile, profile === "custom" && typeof custom !== "undefined") config.custom = custom;
+window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(config));
+}
+static getDefault() {
+return window.navigator.orgUserAgent || window.navigator.userAgent;
+}
+static get(profile) {
+let defaultUserAgent = window.navigator.userAgent;
+switch (profile) {
+case "default":
+return defaultUserAgent;
+case "custom":
+return UserAgent.#config.custom || defaultUserAgent;
+default:
+return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
+}
+}
+static isSafari() {
+if (this.#isSafari !== null) return this.#isSafari;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = userAgent.includes("safari") && !userAgent.includes("chrom");
+return this.#isSafari = result, result;
+}
+static isSafariMobile() {
+if (this.#isSafariMobile !== null) return this.#isSafariMobile;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");
+return this.#isSafariMobile = result, result;
+}
+static isMobile() {
+if (this.#isMobile !== null) return this.#isMobile;
+let userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);
+return this.#isMobile = result, result;
+}
+static spoof() {
+let profile = UserAgent.#config.profile;
+if (profile === "default") return;
+let newUserAgent = UserAgent.get(profile);
+if ("userAgentData" in window.navigator) window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {});
+window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {
+value: newUserAgent
+});
+}
}
var SCRIPT_VERSION = "6.3.0-beta-1", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {
- supportedRegion: !0,
- serverRegions: {},
- selectedRegion: {},
- gsToken: "",
- isSignedIn: !1,
- isPlaying: !1,
- browser: {
- capabilities: {
- touch: browserHasTouchSupport,
- batteryApi: "getBattery" in window.navigator,
- deviceVibration: !!window.navigator.vibrate,
- mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),
- emulatedNativeMkb: !!AppInterface
- }
- },
- userAgent: {
- isTv,
- capabilities: {
- touch: userAgentHasTouchSupport,
- mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)
- }
- },
- currentStream: {},
- remotePlay: {},
- pointerServerPort: 9269
+supportedRegion: !0,
+serverRegions: {},
+selectedRegion: {},
+gsToken: "",
+isSignedIn: !1,
+isPlaying: !1,
+browser: {
+capabilities: {
+touch: browserHasTouchSupport,
+batteryApi: "getBattery" in window.navigator,
+deviceVibration: !!window.navigator.vibrate,
+mkb: AppInterface || !UserAgent.getDefault().toLowerCase().match(/(android|iphone|ipad)/),
+emulatedNativeMkb: !!AppInterface
+}
+},
+userAgent: {
+isTv,
+capabilities: {
+touch: userAgentHasTouchSupport,
+mkb: AppInterface || !userAgent.match(/(android|iphone|ipad)/)
+}
+},
+currentStream: {},
+remotePlay: {},
+pointerServerPort: 9269
};
function deepClone(obj) {
- if (!obj) return {};
- if ("structuredClone" in window) return structuredClone(obj);
- return JSON.parse(JSON.stringify(obj));
+if (!obj) return {};
+if ("structuredClone" in window) return structuredClone(obj);
+return JSON.parse(JSON.stringify(obj));
}
var BxEvent;
((BxEvent) => {
- BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready";
- function dispatch(target, eventName, data) {
- if (!target) return;
- if (!eventName) {
- alert("BxEvent.dispatch(): eventName is null");
- return;
- }
- let event = new Event(eventName);
- if (data) for (let key in data)
- event[key] = data[key];
- target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data);
- }
- BxEvent.dispatch = dispatch;
+BxEvent.POPSTATE = "bx-popstate", BxEvent.STREAM_SESSION_READY = "bx-stream-session-ready", BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED = "bx-custom-touch-layouts-loaded", BxEvent.TOUCH_LAYOUT_MANAGER_READY = "bx-touch-layout-manager-ready", BxEvent.REMOTE_PLAY_READY = "bx-remote-play-ready", BxEvent.REMOTE_PLAY_FAILED = "bx-remote-play-failed", BxEvent.CAPTURE_SCREENSHOT = "bx-capture-screenshot", BxEvent.POINTER_LOCK_REQUESTED = "bx-pointer-lock-requested", BxEvent.POINTER_LOCK_EXITED = "bx-pointer-lock-exited", BxEvent.NAVIGATION_FOCUS_CHANGED = "bx-nav-focus-changed", BxEvent.XCLOUD_GUIDE_MENU_SHOWN = "bx-xcloud-guide-menu-shown", BxEvent.XCLOUD_POLLING_MODE_CHANGED = "bx-xcloud-polling-mode-changed", BxEvent.XCLOUD_RENDERING_COMPONENT = "bx-xcloud-rendering-component", BxEvent.XCLOUD_ROUTER_HISTORY_READY = "bx-xcloud-router-history-ready";
+function dispatch(target, eventName, data) {
+if (!target) return;
+if (!eventName) {
+alert("BxEvent.dispatch(): eventName is null");
+return;
+}
+let event = new Event(eventName);
+if (data) for (let key in data)
+event[key] = data[key];
+target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName), BX_FLAGS.Debug && BxLogger.warning("BxEvent", "dispatch", target, eventName, data);
+}
+BxEvent.dispatch = dispatch;
})(BxEvent ||= {});
window.BxEvent = BxEvent;
var GamepadKeyName = {
- 0: ["A", "⇓"],
- 1: ["B", "⇒"],
- 2: ["X", "⇐"],
- 3: ["Y", "⇑"],
- 4: ["LB", "↘"],
- 5: ["RB", "↙"],
- 6: ["LT", "↖"],
- 7: ["RT", "↗"],
- 8: ["Select", "⇺"],
- 9: ["Start", "⇻"],
- 16: ["Home", ""],
- 12: ["D-Pad Up", "≻"],
- 13: ["D-Pad Down", "≽"],
- 14: ["D-Pad Left", "≺"],
- 15: ["D-Pad Right", "≼"],
- 10: ["L3", "↺"],
- 100: ["Left Stick Up", "↾"],
- 101: ["Left Stick Down", "⇂"],
- 102: ["Left Stick Left", "↼"],
- 103: ["Left Stick Right", "⇀"],
- 104: ["Left Stick", "⇱"],
- 11: ["R3", "↻"],
- 200: ["Right Stick Up", "↿"],
- 201: ["Right Stick Down", "⇃"],
- 202: ["Right Stick Left", "↽"],
- 203: ["Right Stick Right", "⇁"],
- 204: ["Right Stick", "⇲"],
- 17: ["Screenshot", "⇧"]
+0: ["A", "⇓"],
+1: ["B", "⇒"],
+2: ["X", "⇐"],
+3: ["Y", "⇑"],
+4: ["LB", "↘"],
+5: ["RB", "↙"],
+6: ["LT", "↖"],
+7: ["RT", "↗"],
+8: ["Select", "⇺"],
+9: ["Start", "⇻"],
+16: ["Home", ""],
+12: ["D-Pad Up", "≻"],
+13: ["D-Pad Down", "≽"],
+14: ["D-Pad Left", "≺"],
+15: ["D-Pad Right", "≼"],
+10: ["L3", "↺"],
+100: ["Left Stick Up", "↾"],
+101: ["Left Stick Down", "⇂"],
+102: ["Left Stick Left", "↼"],
+103: ["Left Stick Right", "⇀"],
+104: ["Left Stick", "⇱"],
+11: ["R3", "↻"],
+200: ["Right Stick Up", "↿"],
+201: ["Right Stick Down", "⇃"],
+202: ["Right Stick Left", "↽"],
+203: ["Right Stick Right", "⇁"],
+204: ["Right Stick", "⇲"],
+17: ["Screenshot", "⇧"]
};
class BxEventBus {
- listeners = new Map;
- group;
- appJsInterfaces;
- static Script = new BxEventBus("script", {
- "dialog.shown": "onDialogShown",
- "dialog.dismissed": "onDialogDismissed"
- });
- static Stream = new BxEventBus("stream", {
- "state.loading": "onStreamPlaying",
- "state.playing": "onStreamPlaying",
- "state.stopped": "onStreamStopped"
- });
- constructor(group, appJsInterfaces) {
- this.group = group, this.appJsInterfaces = appJsInterfaces;
- }
- on(event, callback) {
- if (!this.listeners.has(event)) this.listeners.set(event, new Set);
- this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);
- }
- once(event, callback) {
- let wrapper = (...args) => {
- callback(...args), this.off(event, wrapper);
- };
- this.on(event, wrapper);
- }
- off(event, callback) {
- if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {
- this.listeners.delete(event);
- return;
- }
- let callbacks = this.listeners.get(event);
- if (!callbacks) return;
- if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);
- }
- offAll() {
- this.listeners.clear();
- }
- emit(event, payload) {
- let callbacks = this.listeners.get(event) || [];
- for (let callback of callbacks)
- callback(payload);
- if (AppInterface) try {
- if (event in this.appJsInterfaces) {
- let method = this.appJsInterfaces[event];
- AppInterface[method] && AppInterface[method]();
- } else AppInterface.onEventBus(this.group + "." + event);
- } catch (e) {
- console.log(e);
- }
- BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);
- }
+listeners = new Map;
+group;
+appJsInterfaces;
+static Script = new BxEventBus("script", {
+"dialog.shown": "onDialogShown",
+"dialog.dismissed": "onDialogDismissed"
+});
+static Stream = new BxEventBus("stream", {
+"state.loading": "onStreamPlaying",
+"state.playing": "onStreamPlaying",
+"state.stopped": "onStreamStopped"
+});
+constructor(group, appJsInterfaces) {
+this.group = group, this.appJsInterfaces = appJsInterfaces;
+}
+on(event, callback) {
+if (!this.listeners.has(event)) this.listeners.set(event, new Set);
+this.listeners.get(event).add(callback), BX_FLAGS.Debug && BxLogger.warning("EventBus", "on", event, callback);
+}
+once(event, callback) {
+let wrapper = (...args) => {
+callback(...args), this.off(event, wrapper);
+};
+this.on(event, wrapper);
+}
+off(event, callback) {
+if (BX_FLAGS.Debug && BxLogger.warning("EventBus", "off", event, callback), !callback) {
+this.listeners.delete(event);
+return;
+}
+let callbacks = this.listeners.get(event);
+if (!callbacks) return;
+if (callbacks.delete(callback), callbacks.size === 0) this.listeners.delete(event);
+}
+offAll() {
+this.listeners.clear();
+}
+emit(event, payload) {
+let callbacks = this.listeners.get(event) || [];
+for (let callback of callbacks)
+callback(payload);
+if (AppInterface) try {
+if (event in this.appJsInterfaces) {
+let method = this.appJsInterfaces[event];
+AppInterface[method] && AppInterface[method]();
+} else AppInterface.onEventBus(this.group + "." + event);
+} catch (e) {
+console.log(e);
+}
+BX_FLAGS.Debug && BxLogger.warning("EventBus", "emit", `${this.group}.${event}`, payload);
+}
}
window.BxEventBus = BxEventBus;
class GhPagesUtils {
- static fetchLatestCommit() {
- NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {
- method: "GET",
- headers: {
- Accept: "application/vnd.github.v3+json"
- }
- }).then((response) => response.json()).then((data) => {
- let latestCommitHash = data.commit.sha;
- window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);
- }).catch((error) => {
- BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);
- });
- }
- static getUrl(path) {
- if (path[0] === "/") alert('`path` must not starts with "/"');
- let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");
- if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;
- else return `${prefix}/refs/heads/gh-pages/${path}`;
- }
- static getNativeMkbCustomList(update = !1) {
- let key = "BetterXcloud.GhPages.ForceNativeMkb";
- update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {
- if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {
- data: json
- });
- else window.localStorage.removeItem(key);
- });
- let info = JSON.parse(window.localStorage.getItem(key) || "{}");
- if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};
- return info.data;
- }
- static getTouchControlCustomList() {
- let key = "BetterXcloud.GhPages.CustomTouchLayouts";
- return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {
- if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));
- }), JSON.parse(window.localStorage.getItem(key) || "[]");
- }
- static getLocalCoOpList() {
- let key = "BetterXcloud.GhPages.LocalCoOp";
- NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {
- if (json.$schemaVersion === 1) {
- window.localStorage.setItem(key, JSON.stringify(json));
- let ids = new Set(Object.keys(json.data));
- BxEventBus.Script.emit("list.localCoOp.updated", { ids });
- } else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });
- });
- let info = JSON.parse(window.localStorage.getItem(key) || "{}");
- if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;
- return new Set(Object.keys(info.data || {}));
- }
+static fetchLatestCommit() {
+NATIVE_FETCH("https://api.github.com/repos/redphx/better-xcloud/branches/gh-pages", {
+method: "GET",
+headers: {
+Accept: "application/vnd.github.v3+json"
+}
+}).then((response) => response.json()).then((data) => {
+let latestCommitHash = data.commit.sha;
+window.localStorage.setItem("BetterXcloud.GhPages.CommitHash", latestCommitHash);
+}).catch((error) => {
+BxLogger.error("GhPagesUtils", "Error fetching the latest commit:", error);
+});
+}
+static getUrl(path) {
+if (path[0] === "/") alert('`path` must not starts with "/"');
+let prefix = "https://raw.githubusercontent.com/redphx/better-xcloud", latestCommitHash = window.localStorage.getItem("BetterXcloud.GhPages.CommitHash");
+if (latestCommitHash) return `${prefix}/${latestCommitHash}/${path}`;
+else return `${prefix}/refs/heads/gh-pages/${path}`;
+}
+static getNativeMkbCustomList(update = !1) {
+let key = "BetterXcloud.GhPages.ForceNativeMkb";
+update && NATIVE_FETCH(GhPagesUtils.getUrl("native-mkb/ids.json")).then((response) => response.json()).then((json) => {
+if (json.$schemaVersion === 1) window.localStorage.setItem(key, JSON.stringify(json)), BxEventBus.Script.emit("list.forcedNativeMkb.updated", {
+data: json
+});
+else window.localStorage.removeItem(key);
+});
+let info = JSON.parse(window.localStorage.getItem(key) || "{}");
+if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), {};
+return info.data;
+}
+static getTouchControlCustomList() {
+let key = "BetterXcloud.GhPages.CustomTouchLayouts";
+return NATIVE_FETCH(GhPagesUtils.getUrl("touch-layouts/ids.json")).then((response) => response.json()).then((json) => {
+if (Array.isArray(json)) window.localStorage.setItem(key, JSON.stringify(json));
+}), JSON.parse(window.localStorage.getItem(key) || "[]");
+}
+static getLocalCoOpList() {
+let key = "BetterXcloud.GhPages.LocalCoOp";
+NATIVE_FETCH(GhPagesUtils.getUrl("local-co-op/ids.json")).then((response) => response.json()).then((json) => {
+if (json.$schemaVersion === 1) {
+window.localStorage.setItem(key, JSON.stringify(json));
+let ids = new Set(Object.keys(json.data));
+BxEventBus.Script.emit("list.localCoOp.updated", { ids });
+} else window.localStorage.removeItem(key), BxEventBus.Script.emit("list.localCoOp.updated", { ids: new Set });
+});
+let info = JSON.parse(window.localStorage.getItem(key) || "{}");
+if (info.$schemaVersion !== 1) return window.localStorage.removeItem(key), new Set;
+return new Set(Object.keys(info.data || {}));
+}
}
var SUPPORTED_LANGUAGES = {
- "en-US": "English (US)",
- "ca-CA": "Català",
- "da-DK": "dansk",
- "de-DE": "Deutsch",
- "en-ID": "Bahasa Indonesia",
- "es-ES": "español (España)",
- "fr-FR": "français",
- "it-IT": "italiano",
- "ja-JP": "日本語",
- "ko-KR": "한국어",
- "pl-PL": "polski",
- "pt-BR": "português (Brasil)",
- "ru-RU": "русский",
- "th-TH": "ภาษาไทย",
- "tr-TR": "Türkçe",
- "uk-UA": "українська",
- "vi-VN": "Tiếng Việt",
- "zh-CN": "中文(简体)",
- "zh-TW": "中文(繁體)"
+"en-US": "English (US)",
+"ca-CA": "Català",
+"da-DK": "dansk",
+"de-DE": "Deutsch",
+"en-ID": "Bahasa Indonesia",
+"es-ES": "español (España)",
+"fr-FR": "français",
+"it-IT": "italiano",
+"ja-JP": "日本語",
+"ko-KR": "한국어",
+"pl-PL": "polski",
+"pt-BR": "português (Brasil)",
+"ru-RU": "русский",
+"th-TH": "ภาษาไทย",
+"tr-TR": "Türkçe",
+"uk-UA": "українська",
+"vi-VN": "Tiếng Việt",
+"zh-CN": "中文(简体)",
+"zh-TW": "中文(繁體)"
}, Texts = {
- achievements: "Achievements",
- activate: "Activate",
- activated: "Activated",
- active: "Active",
- advanced: "Advanced",
- "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)?",
- "background-opacity": "Background opacity",
- battery: "Battery",
- "battery-saving": "Battery saving",
- "better-xcloud": "Better xCloud",
- "bitrate-audio-maximum": "Maximum audio bitrate",
- "bitrate-video-maximum": "Maximum video bitrate",
- bottom: "Bottom",
- "bottom-half": "Bottom half",
- "bottom-left": "Bottom-left",
- "bottom-right": "Bottom-right",
- brazil: "Brazil",
- brightness: "Brightness",
- "browser-unsupported-feature": "Your browser doesn't support this feature",
- "button-xbox": "Xbox button",
- "bypass-region-restriction": "Bypass region restriction",
- "can-stream-xbox-360-games": "Can stream Xbox 360 games",
- cancel: "Cancel",
- "cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
- center: "Center",
- chat: "Chat",
- "clarity-boost": "Clarity boost",
- "clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
- clear: "Clear",
- "clear-data": "Clear data",
- "clear-data-confirm": "Do you want to clear all Better xCloud settings and data?",
- "clear-data-success": "Data cleared! Refresh the page to apply the changes.",
- clock: "Clock",
- close: "Close",
- "close-app": "Close app",
- "combine-audio-video-streams": "Combine audio & video streams",
- "combine-audio-video-streams-summary": "May fix the laggy audio problem",
- "conditional-formatting": "Conditional formatting text color",
- "confirm-delete-preset": "Do you want to delete this preset?",
- "confirm-reload-stream": "Do you want to refresh the stream?",
- connected: "Connected",
- "console-connect": "Connect",
- "continent-asia": "Asia",
- "continent-australia": "Australia",
- "continent-europe": "Europe",
- "continent-north-america": "North America",
- "continent-south-america": "South America",
- contrast: "Contrast",
- controller: "Controller",
- "controller-customization": "Controller customization",
- "controller-customization-input-latency-note": "May slightly increase input latency",
- "controller-friendly-ui": "Controller-friendly UI",
- "controller-shortcuts": "Controller shortcuts",
- "controller-shortcuts-connect-note": "Connect a controller to use this feature",
- "controller-shortcuts-xbox-note": "Button to open the Guide menu",
- "controller-vibration": "Controller vibration",
- copy: "Copy",
- "create-shortcut": "Shortcut",
- custom: "Custom",
- "deadzone-counterweight": "Deadzone counterweight",
- decrease: "Decrease",
- default: "Default",
- "default-preset-note": "You can't modify default presets. Create a new one to customize it.",
- delete: "Delete",
- "detect-controller-button": "Detect controller button",
- device: "Device",
- "device-unsupported-touch": "Your device doesn't have touch support",
- "device-vibration": "Device vibration",
- "device-vibration-not-using-gamepad": "On when not using gamepad",
- disable: "Disable",
- "disable-features": "Disable features",
- "disable-home-context-menu": "Disable context menu in Home page",
- "disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
- "disable-social-features": "Disable social features",
- "disable-xcloud-analytics": "Disable xCloud analytics",
- disabled: "Disabled",
- disconnected: "Disconnected",
- download: "Download",
- downloaded: "Downloaded",
- edit: "Edit",
- "enable-controller-shortcuts": "Enable controller shortcuts",
- "enable-local-co-op-support": "Enable local co-op support",
- "enable-local-co-op-support-note": "Only works with some games",
- "enable-mic-on-startup": "Enable microphone on game launch",
- "enable-mkb": "Emulate controller with Mouse & Keyboard",
- "enable-quick-glance-mode": 'Enable "Quick Glance" mode',
- "enable-remote-play-feature": 'Enable the "Remote Play" feature',
- "enable-volume-control": "Enable volume control feature",
- enabled: "Enabled",
- experimental: "Experimental",
- export: "Export",
- fast: "Fast",
- "force-native-mkb-games": "Force native Mouse & Keyboard for these games",
- "fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',
- "fortnite-force-console-version": "Fortnite: force console version",
- "friends-followers": "Friends and followers",
- "game-bar": "Game Bar",
- "getting-consoles-list": "Getting the list of consoles...",
- guide: "Guide",
- help: "Help",
- hide: "Hide",
- "hide-idle-cursor": "Hide mouse cursor on idle",
- "hide-scrollbar": "Hide web page's scrollbar",
- "hide-sections": "Hide sections",
- "hide-system-menu-icon": "Hide System menu's icon",
- "hide-touch-controller": "Hide touch controller",
- "high-performance": "High performance",
- "highest-quality": "Highest quality",
- "highest-quality-note": "Your device may not be powerful enough to use these settings",
- "horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
- "horizontal-sensitivity": "Horizontal sensitivity",
- "how-to-fix": "How to fix",
- "how-to-improve-app-performance": "How to improve app's performance",
- ignore: "Ignore",
- "image-quality": "Website's image quality",
- import: "Import",
- "in-game-controller-customization": "In-game controller customization",
- "in-game-controller-shortcuts": "In-game controller shortcuts",
- "in-game-keyboard-shortcuts": "In-game keyboard shortcuts",
- "in-game-shortcuts": "In-game shortcuts",
- increase: "Increase",
- "install-android": "Better xCloud app for Android",
- invites: "Invites",
- japan: "Japan",
- jitter: "Jitter",
- "keyboard-key": "Keyboard key",
- "keyboard-shortcuts": "Keyboard shortcuts",
- korea: "Korea",
- language: "Language",
- large: "Large",
- layout: "Layout",
- "left-stick": "Left stick",
- "left-stick-deadzone": "Left stick deadzone",
- "left-trigger-range": "Left trigger range",
- "limit-fps": "Limit FPS",
- "load-failed-message": "Failed to run Better xCloud",
- "loading-screen": "Loading screen",
- "local-co-op": "Local co-op",
- "lowest-quality": "Lowest quality",
- manage: "Manage",
- "map-mouse-to": "Map mouse to",
- "may-not-work-properly": "May not work properly!",
- menu: "Menu",
- microphone: "Microphone",
- "mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
- "mkb-click-to-activate": "Click to activate",
- "mkb-disclaimer": "This could be viewed as cheating when playing online",
- "modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.",
- "mouse-and-keyboard": "Mouse & Keyboard",
- "mouse-click": "Mouse click",
- "mouse-wheel": "Mouse wheel",
- muted: "Muted",
- name: "Name",
- "native-mkb": "Native Mouse & Keyboard",
- new: "New",
- "new-version-available": [
- e => `Version ${e.version} available`,
- e => `Versió ${e.version} disponible`,
- ,
- e => `Version ${e.version} verfügbar`,
- e => `Versi ${e.version} tersedia`,
- e => `Versión ${e.version} disponible`,
- e => `Version ${e.version} disponible`,
- e => `Disponibile la versione ${e.version}`,
- e => `Ver ${e.version} が利用可能です`,
- e => `${e.version} 버전 사용가능`,
- e => `Dostępna jest nowa wersja ${e.version}`,
- e => `Versão ${e.version} disponível`,
- e => `Версия ${e.version} доступна`,
- e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
- e => `${e.version} sayılı yeni sürüm mevcut`,
- e => `Доступна версія ${e.version}`,
- e => `Đã có phiên bản ${e.version}`,
- e => `版本 ${e.version} 可供更新`,
- e => `已可更新為 ${e.version} 版`
- ],
- "no-consoles-found": "No consoles found",
- "no-controllers-connected": "No controllers connected",
- normal: "Normal",
- notifications: "Notifications",
- off: "Off",
- official: "Official",
- on: "On",
- "only-supports-some-games": "Only supports some games",
- opacity: "Opacity",
- other: "Other",
- playing: "Playing",
- playtime: "Playtime",
- poland: "Poland",
- "polling-rate": "Polling rate",
- position: "Position",
- "powered-off": "Powered off",
- "powered-on": "Powered on",
- "prefer-ipv6-server": "Prefer IPv6 server",
- "preferred-game-language": "Preferred game's language",
- preset: "Preset",
- press: "Press",
- "press-any-button": "Press any button...",
- "press-esc-to-cancel": "Press Esc to cancel",
- "press-key-to-toggle-mkb": [
- e => `Press ${e.key} to toggle this feature`,
- e => `Premeu ${e.key} per alternar aquesta funció`,
- e => `Tryk på ${e.key} for at slå denne funktion til`,
- e => `${e.key}: Funktion an-/ausschalten`,
- e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,
- e => `Pulsa ${e.key} para alternar esta función`,
- e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
- e => `Premi ${e.key} per attivare questa funzionalità`,
- e => `${e.key} でこの機能を切替`,
- e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
- e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
- e => `Pressione ${e.key} para alternar este recurso`,
- e => `Нажмите ${e.key} для переключения этой функции`,
- e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,
- e => `Etkinleştirmek için ${e.key} tuşuna basın`,
- e => `Натисніть ${e.key} щоб перемкнути цю функцію`,
- e => `Nhấn ${e.key} để bật/tắt tính năng này`,
- e => `按下 ${e.key} 来切换此功能`,
- e => `按下 ${e.key} 來啟用此功能`
- ],
- "press-to-bind": "Press a key or do a mouse click to bind...",
- "prompt-preset-name": "Preset's name:",
- recommended: "Recommended",
- "recommended-settings-for-device": [
- e => `Recommended settings for ${e.device}`,
- e => `Configuració recomanada per a ${e.device}`,
- ,
- e => `Empfohlene Einstellungen für ${e.device}`,
- e => `Rekomendasi pengaturan untuk ${e.device}`,
- e => `Ajustes recomendados para ${e.device}`,
- e => `Paramètres recommandés pour ${e.device}`,
- e => `Configurazioni consigliate per ${e.device}`,
- e => `${e.device} の推奨設定`,
- e => `다음 기기에서 권장되는 설정: ${e.device}`,
- e => `Zalecane ustawienia dla ${e.device}`,
- e => `Configurações recomendadas para ${e.device}`,
- e => `Рекомендуемые настройки для ${e.device}`,
- e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,
- e => `${e.device} için önerilen ayarlar`,
- e => `Рекомендовані налаштування для ${e.device}`,
- e => `Cấu hình được đề xuất cho ${e.device}`,
- e => `${e.device} 的推荐设置`,
- e => `${e.device} 推薦的設定`
- ],
- "reduce-animations": "Reduce UI animations",
- region: "Region",
- "reload-page": "Reload page",
- "remote-play": "Remote Play",
- rename: "Rename",
- renderer: "Renderer",
- "renderer-configuration": "Renderer configuration",
- "right-click-to-unbind": "Right-click on a key to unbind it",
- "right-stick": "Right stick",
- "right-stick-deadzone": "Right stick deadzone",
- "right-trigger-range": "Right trigger range",
- "rocket-always-hide": "Always hide",
- "rocket-always-show": "Always show",
- "rocket-animation": "Rocket animation",
- "rocket-hide-queue": "Hide when queuing",
- saturation: "Saturation",
- save: "Save",
- screen: "Screen",
- "screenshot-apply-filters": "Apply video filters to screenshots",
- "section-all-games": "All games",
- "section-most-popular": "Most popular",
- "section-native-mkb": "Play with mouse & keyboard",
- "section-news": "News",
- "section-play-with-friends": "Play with friends",
- "section-touch": "Play with touch",
- "separate-touch-controller": "Separate Touch controller & Controller #1",
- "separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
- server: "Server",
- "server-locations": "Server locations",
- settings: "Settings",
- "settings-reload": "Reload page to reflect changes",
- "settings-reload-note": "Settings in this tab only go into effect on the next page load",
- "settings-reloading": "Reloading...",
- sharpness: "Sharpness",
- "shortcut-keys": "Shortcut keys",
- show: "Show",
- "show-controller-connection-status": "Show controller connection status",
- "show-game-art": "Show game art",
- "show-hide": "Show/hide",
- "show-stats-on-startup": "Show stats when starting the game",
- "show-touch-controller": "Show touch controller",
- "show-wait-time": "Show the estimated wait time",
- "show-wait-time-in-game-card": "Show wait time in game card",
- "simplify-stream-menu": "Simplify Stream's menu",
- "skip-splash-video": "Skip Xbox splash video",
- slow: "Slow",
- small: "Small",
- "smart-tv": "Smart TV",
- sound: "Sound",
- standard: "Standard",
- standby: "Standby",
- "stat-bitrate": "Bitrate",
- "stat-decode-time": "Decode time",
- "stat-fps": "FPS",
- "stat-frames-lost": "Frames lost",
- "stat-packets-lost": "Packets lost",
- "stat-ping": "Ping",
- stats: "Stats",
- "stick-decay-minimum": "Stick decay minimum",
- "stick-decay-strength": "Stick decay strength",
- stream: "Stream",
- "stream-settings": "Stream settings",
- "stream-stats": "Stream stats",
- "stream-your-own-game": "Stream your own game",
- stretch: "Stretch",
- "suggest-settings": "Suggest settings",
- "suggest-settings-link": "Suggest recommended settings for this device",
- "support-better-xcloud": "Support Better xCloud",
- "swap-buttons": "Swap buttons",
- "take-screenshot": "Take screenshot",
- "target-resolution": "Target resolution",
- "tc-all-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: "Top",
- "top-center": "Top-center",
- "top-half": "Top half",
- "top-left": "Top-left",
- "top-right": "Top-right",
- "touch-control-layout": "Touch control layout",
- "touch-control-layout-by": [
- e => `Touch control layout by ${e.name}`,
- e => `Format del control tàctil per ${e.name}`,
- e => `Touch-kontrol layout af ${e.name}`,
- e => `Touch-Steuerungslayout von ${e.name}`,
- e => `Tata letak Sentuhan layar oleh ${e.name}`,
- e => `Disposición del control táctil por ${e.nombre}`,
- e => `Disposition du contrôleur tactile par ${e.name}`,
- e => `Configurazione dei comandi su schermo creata da ${e.name}`,
- e => `タッチ操作レイアウト作成者: ${e.name}`,
- e => `${e.name} 제작, 터치 컨트롤 레이아웃`,
- e => `Układ sterowania dotykowego stworzony przez ${e.name}`,
- e => `Disposição de controle por toque feito por ${e.name}`,
- e => `Сенсорная раскладка по ${e.name}`,
- e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,
- e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
- e => `Розташування сенсорного керування від ${e.name}`,
- e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
- e => `由 ${e.name} 提供的虚拟按键样式`,
- e => `觸控遊玩佈局由 ${e.name} 提供`
- ],
- "touch-controller": "Touch controller",
- "true-achievements": "TrueAchievements",
- ui: "UI",
- "unexpected-behavior": "May cause unexpected behavior",
- "united-states": "United States",
- unknown: "Unknown",
- unlimited: "Unlimited",
- unmuted: "Unmuted",
- unofficial: "Unofficial",
- "unofficial-game-list": "Unofficial game list",
- "unsharp-masking": "Unsharp masking",
- upload: "Upload",
- uploaded: "Uploaded",
- "use-mouse-absolute-position": "Use mouse's absolute position",
- "use-this-at-your-own-risk": "Use this at your own risk",
- "user-agent-profile": "User-Agent profile",
- "vertical-scroll-sensitivity": "Vertical scroll sensitivity",
- "vertical-sensitivity": "Vertical sensitivity",
- "vibration-intensity": "Vibration intensity",
- "vibration-status": "Vibration",
- video: "Video",
- "virtual-controller": "Virtual controller",
- "virtual-controller-slot": "Virtual controller slot",
- "visual-quality": "Visual quality",
- "visual-quality-high": "High",
- "visual-quality-low": "Low",
- "visual-quality-normal": "Normal",
- volume: "Volume",
- "wait-time-countdown": "Countdown",
- "wait-time-estimated": "Estimated finish time",
- "waiting-for-input": "Waiting for input...",
- wallpaper: "Wallpaper",
- webgl2: "WebGL2"
+achievements: "Achievements",
+activate: "Activate",
+activated: "Activated",
+active: "Active",
+advanced: "Advanced",
+"all-games": "All games",
+"always-off": "Always off",
+"always-on": "Always on",
+"amd-fidelity-cas": "AMD FidelityFX CAS",
+"app-settings": "App settings",
+apply: "Apply",
+"aspect-ratio": "Aspect ratio",
+"aspect-ratio-note": "Don't use with native touch games",
+audio: "Audio",
+auto: "Auto",
+availability: "Availability",
+"back-to-home": "Back to home",
+"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?",
+"background-opacity": "Background opacity",
+battery: "Battery",
+"battery-saving": "Battery saving",
+"better-xcloud": "Better xCloud",
+"bitrate-audio-maximum": "Maximum audio bitrate",
+"bitrate-video-maximum": "Maximum video bitrate",
+bottom: "Bottom",
+"bottom-half": "Bottom half",
+"bottom-left": "Bottom-left",
+"bottom-right": "Bottom-right",
+brazil: "Brazil",
+brightness: "Brightness",
+"browser-unsupported-feature": "Your browser doesn't support this feature",
+"button-xbox": "Xbox button",
+"bypass-region-restriction": "Bypass region restriction",
+"can-stream-xbox-360-games": "Can stream Xbox 360 games",
+cancel: "Cancel",
+"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
+center: "Center",
+chat: "Chat",
+"clarity-boost": "Clarity boost",
+"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
+clear: "Clear",
+"clear-data": "Clear data",
+"clear-data-confirm": "Do you want to clear all Better xCloud settings and data?",
+"clear-data-success": "Data cleared! Refresh the page to apply the changes.",
+clock: "Clock",
+close: "Close",
+"close-app": "Close app",
+"combine-audio-video-streams": "Combine audio & video streams",
+"combine-audio-video-streams-summary": "May fix the laggy audio problem",
+"conditional-formatting": "Conditional formatting text color",
+"confirm-delete-preset": "Do you want to delete this preset?",
+"confirm-reload-stream": "Do you want to refresh the stream?",
+connected: "Connected",
+"console-connect": "Connect",
+"continent-asia": "Asia",
+"continent-australia": "Australia",
+"continent-europe": "Europe",
+"continent-north-america": "North America",
+"continent-south-america": "South America",
+contrast: "Contrast",
+controller: "Controller",
+"controller-customization": "Controller customization",
+"controller-customization-input-latency-note": "May slightly increase input latency",
+"controller-friendly-ui": "Controller-friendly UI",
+"controller-shortcuts": "Controller shortcuts",
+"controller-shortcuts-connect-note": "Connect a controller to use this feature",
+"controller-shortcuts-xbox-note": "Button to open the Guide menu",
+"controller-vibration": "Controller vibration",
+copy: "Copy",
+"create-shortcut": "Shortcut",
+custom: "Custom",
+"deadzone-counterweight": "Deadzone counterweight",
+decrease: "Decrease",
+default: "Default",
+"default-opacity": "Default opacity",
+"default-preset-note": "You can't modify default presets. Create a new one to customize it.",
+delete: "Delete",
+"detect-controller-button": "Detect controller button",
+device: "Device",
+"device-unsupported-touch": "Your device doesn't have touch support",
+"device-vibration": "Device vibration",
+"device-vibration-not-using-gamepad": "On when not using gamepad",
+disable: "Disable",
+"disable-features": "Disable features",
+"disable-home-context-menu": "Disable context menu in Home page",
+"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
+"disable-social-features": "Disable social features",
+"disable-xcloud-analytics": "Disable xCloud analytics",
+disabled: "Disabled",
+disconnected: "Disconnected",
+download: "Download",
+downloaded: "Downloaded",
+edit: "Edit",
+"enable-controller-shortcuts": "Enable controller shortcuts",
+"enable-local-co-op-support": "Enable local co-op support",
+"enable-local-co-op-support-note": "Only works with some games",
+"enable-mic-on-startup": "Enable microphone on game launch",
+"enable-mkb": "Emulate controller with Mouse & Keyboard",
+"enable-quick-glance-mode": 'Enable "Quick Glance" mode',
+"enable-remote-play-feature": 'Enable the "Remote Play" feature',
+"enable-volume-control": "Enable volume control feature",
+enabled: "Enabled",
+experimental: "Experimental",
+export: "Export",
+fast: "Fast",
+"force-native-mkb-games": "Force native Mouse & Keyboard for these games",
+"fortnite-allow-stw-mode": 'Allows playing "Save the World" mode on mobile',
+"fortnite-force-console-version": "Fortnite: force console version",
+"friends-followers": "Friends and followers",
+"game-bar": "Game Bar",
+"getting-consoles-list": "Getting the list of consoles...",
+guide: "Guide",
+help: "Help",
+hide: "Hide",
+"hide-idle-cursor": "Hide mouse cursor on idle",
+"hide-scrollbar": "Hide web page's scrollbar",
+"hide-sections": "Hide sections",
+"hide-system-menu-icon": "Hide System menu's icon",
+"hide-touch-controller": "Hide touch controller",
+"high-performance": "High performance",
+"highest-quality": "Highest quality",
+"highest-quality-note": "Your device may not be powerful enough to use these settings",
+"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
+"horizontal-sensitivity": "Horizontal sensitivity",
+"how-to-fix": "How to fix",
+"how-to-improve-app-performance": "How to improve app's performance",
+ignore: "Ignore",
+"image-quality": "Website's image quality",
+import: "Import",
+"in-game-controller-customization": "In-game controller customization",
+"in-game-controller-shortcuts": "In-game controller shortcuts",
+"in-game-keyboard-shortcuts": "In-game keyboard shortcuts",
+"in-game-shortcuts": "In-game shortcuts",
+increase: "Increase",
+"install-android": "Better xCloud app for Android",
+invites: "Invites",
+japan: "Japan",
+jitter: "Jitter",
+"keyboard-key": "Keyboard key",
+"keyboard-shortcuts": "Keyboard shortcuts",
+korea: "Korea",
+language: "Language",
+large: "Large",
+layout: "Layout",
+"left-stick": "Left stick",
+"left-stick-deadzone": "Left stick deadzone",
+"left-trigger-range": "Left trigger range",
+"limit-fps": "Limit FPS",
+"load-failed-message": "Failed to run Better xCloud",
+"loading-screen": "Loading screen",
+"local-co-op": "Local co-op",
+"lowest-quality": "Lowest quality",
+manage: "Manage",
+"map-mouse-to": "Map mouse to",
+"may-not-work-properly": "May not work properly!",
+menu: "Menu",
+microphone: "Microphone",
+"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
+"mkb-click-to-activate": "Click to activate",
+"mkb-disclaimer": "This could be viewed as cheating when playing online",
+"modifiers-note": "To use more than one key, include Ctrl, Alt or Shift in your shortcut. Command key is not allowed.",
+"mouse-and-keyboard": "Mouse & Keyboard",
+"mouse-click": "Mouse click",
+"mouse-wheel": "Mouse wheel",
+muted: "Muted",
+name: "Name",
+"native-mkb": "Native Mouse & Keyboard",
+new: "New",
+"new-version-available": [
+e => `Version ${e.version} available`,
+e => `Versió ${e.version} disponible`,
+,
+e => `Version ${e.version} verfügbar`,
+e => `Versi ${e.version} tersedia`,
+e => `Versión ${e.version} disponible`,
+e => `Version ${e.version} disponible`,
+e => `Disponibile la versione ${e.version}`,
+e => `Ver ${e.version} が利用可能です`,
+e => `${e.version} 버전 사용가능`,
+e => `Dostępna jest nowa wersja ${e.version}`,
+e => `Versão ${e.version} disponível`,
+e => `Версия ${e.version} доступна`,
+e => `เวอร์ชัน ${e.version} พร้อมใช้งานแล้ว`,
+e => `${e.version} sayılı yeni sürüm mevcut`,
+e => `Доступна версія ${e.version}`,
+e => `Đã có phiên bản ${e.version}`,
+e => `版本 ${e.version} 可供更新`,
+e => `已可更新為 ${e.version} 版`
+],
+"no-consoles-found": "No consoles found",
+"no-controllers-connected": "No controllers connected",
+normal: "Normal",
+notifications: "Notifications",
+off: "Off",
+official: "Official",
+on: "On",
+"only-supports-some-games": "Only supports some games",
+opacity: "Opacity",
+other: "Other",
+playing: "Playing",
+playtime: "Playtime",
+poland: "Poland",
+"polling-rate": "Polling rate",
+position: "Position",
+"powered-off": "Powered off",
+"powered-on": "Powered on",
+"prefer-ipv6-server": "Prefer IPv6 server",
+"preferred-game-language": "Preferred game's language",
+preset: "Preset",
+press: "Press",
+"press-any-button": "Press any button...",
+"press-esc-to-cancel": "Press Esc to cancel",
+"press-key-to-toggle-mkb": [
+e => `Press ${e.key} to toggle this feature`,
+e => `Premeu ${e.key} per alternar aquesta funció`,
+e => `Tryk på ${e.key} for at slå denne funktion til`,
+e => `${e.key}: Funktion an-/ausschalten`,
+e => `Tekan ${e.key} untuk mengaktifkan fitur ini`,
+e => `Pulsa ${e.key} para alternar esta función`,
+e => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
+e => `Premi ${e.key} per attivare questa funzionalità`,
+e => `${e.key} でこの機能を切替`,
+e => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
+e => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
+e => `Pressione ${e.key} para alternar este recurso`,
+e => `Нажмите ${e.key} для переключения этой функции`,
+e => `กด ${e.key} เพื่อสลับคุณสมบัตินี้`,
+e => `Etkinleştirmek için ${e.key} tuşuna basın`,
+e => `Натисніть ${e.key} щоб перемкнути цю функцію`,
+e => `Nhấn ${e.key} để bật/tắt tính năng này`,
+e => `按下 ${e.key} 来切换此功能`,
+e => `按下 ${e.key} 來啟用此功能`
+],
+"press-to-bind": "Press a key or do a mouse click to bind...",
+"prompt-preset-name": "Preset's name:",
+recommended: "Recommended",
+"recommended-settings-for-device": [
+e => `Recommended settings for ${e.device}`,
+e => `Configuració recomanada per a ${e.device}`,
+,
+e => `Empfohlene Einstellungen für ${e.device}`,
+e => `Rekomendasi pengaturan untuk ${e.device}`,
+e => `Ajustes recomendados para ${e.device}`,
+e => `Paramètres recommandés pour ${e.device}`,
+e => `Configurazioni consigliate per ${e.device}`,
+e => `${e.device} の推奨設定`,
+e => `다음 기기에서 권장되는 설정: ${e.device}`,
+e => `Zalecane ustawienia dla ${e.device}`,
+e => `Configurações recomendadas para ${e.device}`,
+e => `Рекомендуемые настройки для ${e.device}`,
+e => `การตั้งค่าที่แนะนำสำหรับ ${e.device}`,
+e => `${e.device} için önerilen ayarlar`,
+e => `Рекомендовані налаштування для ${e.device}`,
+e => `Cấu hình được đề xuất cho ${e.device}`,
+e => `${e.device} 的推荐设置`,
+e => `${e.device} 推薦的設定`
+],
+"reduce-animations": "Reduce UI animations",
+region: "Region",
+"reload-page": "Reload page",
+"remote-play": "Remote Play",
+rename: "Rename",
+renderer: "Renderer",
+"renderer-configuration": "Renderer configuration",
+"reset-highlighted-setting": "Reset highlighted setting",
+"right-click-to-unbind": "Right-click on a key to unbind it",
+"right-stick": "Right stick",
+"right-stick-deadzone": "Right stick deadzone",
+"right-trigger-range": "Right trigger range",
+"rocket-always-hide": "Always hide",
+"rocket-always-show": "Always show",
+"rocket-animation": "Rocket animation",
+"rocket-hide-queue": "Hide when queuing",
+saturation: "Saturation",
+save: "Save",
+screen: "Screen",
+"screenshot-apply-filters": "Apply video filters to screenshots",
+"section-all-games": "All games",
+"section-most-popular": "Most popular",
+"section-native-mkb": "Play with mouse & keyboard",
+"section-news": "News",
+"section-play-with-friends": "Play with friends",
+"section-touch": "Play with touch",
+"separate-touch-controller": "Separate Touch controller & Controller #1",
+"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
+server: "Server",
+"server-locations": "Server locations",
+settings: "Settings",
+"settings-for": "Settings for",
+"settings-reload": "Reload page to reflect changes",
+"settings-reload-note": "Settings in this tab only go into effect on the next page load",
+"settings-reloading": "Reloading...",
+sharpness: "Sharpness",
+"shortcut-keys": "Shortcut keys",
+show: "Show",
+"show-controller-connection-status": "Show controller connection status",
+"show-game-art": "Show game art",
+"show-hide": "Show/hide",
+"show-stats-on-startup": "Show stats when starting the game",
+"show-touch-controller": "Show touch controller",
+"show-wait-time": "Show the estimated wait time",
+"show-wait-time-in-game-card": "Show wait time in game card",
+"simplify-stream-menu": "Simplify Stream's menu",
+"skip-splash-video": "Skip Xbox splash video",
+slow: "Slow",
+small: "Small",
+"smart-tv": "Smart TV",
+sound: "Sound",
+standard: "Standard",
+standby: "Standby",
+"stat-bitrate": "Bitrate",
+"stat-decode-time": "Decode time",
+"stat-fps": "FPS",
+"stat-frames-lost": "Frames lost",
+"stat-packets-lost": "Packets lost",
+"stat-ping": "Ping",
+stats: "Stats",
+"stick-decay-minimum": "Stick decay minimum",
+"stick-decay-strength": "Stick decay strength",
+stream: "Stream",
+"stream-settings": "Stream settings",
+"stream-stats": "Stream stats",
+"stream-your-own-game": "Stream your own game",
+stretch: "Stretch",
+"suggest-settings": "Suggest settings",
+"suggest-settings-link": "Suggest recommended settings for this device",
+"support-better-xcloud": "Support Better xCloud",
+"swap-buttons": "Swap buttons",
+"take-screenshot": "Take screenshot",
+"target-resolution": "Target resolution",
+"tc-all-white": "All white",
+"tc-auto-off": "Off when controller found",
+"tc-custom-layout-style": "Custom layout's button style",
+"tc-muted-colors": "Muted colors",
+"tc-standard-layout-style": "Standard layout's button style",
+"text-size": "Text size",
+toggle: "Toggle",
+top: "Top",
+"top-center": "Top-center",
+"top-half": "Top half",
+"top-left": "Top-left",
+"top-right": "Top-right",
+"touch-control-layout": "Touch control layout",
+"touch-control-layout-by": [
+e => `Touch control layout by ${e.name}`,
+e => `Format del control tàctil per ${e.name}`,
+e => `Touch-kontrol layout af ${e.name}`,
+e => `Touch-Steuerungslayout von ${e.name}`,
+e => `Tata letak Sentuhan layar oleh ${e.name}`,
+e => `Disposición del control táctil por ${e.nombre}`,
+e => `Disposition du contrôleur tactile par ${e.name}`,
+e => `Configurazione dei comandi su schermo creata da ${e.name}`,
+e => `タッチ操作レイアウト作成者: ${e.name}`,
+e => `${e.name} 제작, 터치 컨트롤 레이아웃`,
+e => `Układ sterowania dotykowego stworzony przez ${e.name}`,
+e => `Disposição de controle por toque feito por ${e.name}`,
+e => `Сенсорная раскладка по ${e.name}`,
+e => `รูปแบบการควบคุมแบบสัมผัสโดย ${e.name}`,
+e => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
+e => `Розташування сенсорного керування від ${e.name}`,
+e => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
+e => `由 ${e.name} 提供的虚拟按键样式`,
+e => `觸控遊玩佈局由 ${e.name} 提供`
+],
+"touch-controller": "Touch controller",
+"true-achievements": "TrueAchievements",
+ui: "UI",
+"unexpected-behavior": "May cause unexpected behavior",
+"united-states": "United States",
+unknown: "Unknown",
+unlimited: "Unlimited",
+unmuted: "Unmuted",
+unofficial: "Unofficial",
+"unofficial-game-list": "Unofficial game list",
+"unsharp-masking": "Unsharp masking",
+upload: "Upload",
+uploaded: "Uploaded",
+"use-mouse-absolute-position": "Use mouse's absolute position",
+"use-this-at-your-own-risk": "Use this at your own risk",
+"user-agent-profile": "User-Agent profile",
+"vertical-scroll-sensitivity": "Vertical scroll sensitivity",
+"vertical-sensitivity": "Vertical sensitivity",
+"vibration-intensity": "Vibration intensity",
+"vibration-status": "Vibration",
+video: "Video",
+"virtual-controller": "Virtual controller",
+"virtual-controller-slot": "Virtual controller slot",
+"visual-quality": "Visual quality",
+"visual-quality-high": "High",
+"visual-quality-low": "Low",
+"visual-quality-normal": "Normal",
+volume: "Volume",
+"wait-time-countdown": "Countdown",
+"wait-time-estimated": "Estimated finish time",
+"waiting-for-input": "Waiting for input...",
+wallpaper: "Wallpaper",
+webgl2: "WebGL2"
};
class Translations {
- static EN_US = "en-US";
- static KEY_LOCALE = "BetterXcloud.Locale";
- static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";
- static selectedLocaleIndex = -1;
- static selectedLocale = "en-US";
- static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
- static foreignTranslations = {};
- static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);
- static async init() {
- Translations.refreshLocale(), await Translations.loadTranslations();
- }
- static refreshLocale(newLocale) {
- let locale;
- if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;
- else locale = localStorage.getItem(Translations.KEY_LOCALE);
- let supportedLocales = Translations.supportedLocales;
- if (!locale) {
- if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;
- localStorage.setItem(Translations.KEY_LOCALE, locale);
- }
- Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);
- }
- static get(key, values) {
- let text = null;
- if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];
- if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);
- let translation;
- if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);
- return translation = text, translation;
- }
- static async loadTranslations() {
- if (Translations.selectedLocale === Translations.EN_US) return;
- try {
- Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));
- } catch (e) {}
- if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);
- }
- static async updateTranslations(async = !1) {
- if (Translations.selectedLocale === Translations.EN_US) {
- localStorage.removeItem(Translations.KEY_TRANSLATIONS);
- return;
- }
- if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);
- else await Translations.downloadTranslations(Translations.selectedLocale);
- }
- static async downloadTranslations(locale) {
- try {
- let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();
- if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
- return !0;
- } catch (e) {
- debugger;
- }
- return !1;
- }
- static downloadTranslationsAsync(locale) {
- NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {
- window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
- });
- }
- static switchLocale(locale) {
- localStorage.setItem(Translations.KEY_LOCALE, locale);
- }
+static EN_US = "en-US";
+static KEY_LOCALE = "BetterXcloud.Locale";
+static KEY_TRANSLATIONS = "BetterXcloud.Locale.Translations";
+static selectedLocaleIndex = -1;
+static selectedLocale = "en-US";
+static supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
+static foreignTranslations = {};
+static enUsIndex = Translations.supportedLocales.indexOf(Translations.EN_US);
+static async init() {
+Translations.refreshLocale(), await Translations.loadTranslations();
}
-var t = Translations.get, ut = (text) => {
- return BxLogger.warning("Untranslated text", text), text;
-};
+static refreshLocale(newLocale) {
+let locale;
+if (newLocale) localStorage.setItem(Translations.KEY_LOCALE, newLocale), locale = newLocale;
+else locale = localStorage.getItem(Translations.KEY_LOCALE);
+let supportedLocales = Translations.supportedLocales;
+if (!locale) {
+if (locale = window.navigator.language || Translations.EN_US, supportedLocales.indexOf(locale) === -1) locale = Translations.EN_US;
+localStorage.setItem(Translations.KEY_LOCALE, locale);
+}
+Translations.selectedLocale = locale, Translations.selectedLocaleIndex = supportedLocales.indexOf(locale);
+}
+static get(key, values) {
+let text = null;
+if (Translations.foreignTranslations && Translations.selectedLocale !== Translations.EN_US) text = Translations.foreignTranslations[key];
+if (!text) text = Texts[key] || alert(`Missing translation key: ${key}`);
+let translation;
+if (Array.isArray(text)) return translation = text[Translations.selectedLocaleIndex] || text[Translations.enUsIndex], translation(values);
+return translation = text, translation;
+}
+static async loadTranslations() {
+if (Translations.selectedLocale === Translations.EN_US) return;
+try {
+Translations.foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.KEY_TRANSLATIONS));
+} catch (e) {}
+if (!Translations.foreignTranslations) await this.downloadTranslations(Translations.selectedLocale);
+}
+static async updateTranslations(async = !1) {
+if (Translations.selectedLocale === Translations.EN_US) {
+localStorage.removeItem(Translations.KEY_TRANSLATIONS);
+return;
+}
+if (async) Translations.downloadTranslationsAsync(Translations.selectedLocale);
+else await Translations.downloadTranslations(Translations.selectedLocale);
+}
+static async downloadTranslations(locale) {
+try {
+let translations = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`))).json();
+if (localStorage.getItem(Translations.KEY_LOCALE) === locale) window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
+return !0;
+} catch (e) {
+debugger;
+}
+return !1;
+}
+static downloadTranslationsAsync(locale) {
+NATIVE_FETCH(GhPagesUtils.getUrl(`translations/${locale}.json`)).then((resp) => resp.json()).then((translations) => {
+window.localStorage.setItem(Translations.KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.foreignTranslations = translations;
+});
+}
+static switchLocale(locale) {
+localStorage.setItem(Translations.KEY_LOCALE, locale);
+}
+}
+var t = Translations.get;
Translations.init();
class NavigationUtils {
- static setNearby($elm, nearby) {
- $elm.nearby = $elm.nearby || {};
- let key;
- for (key in nearby)
- $elm.nearby[key] = nearby[key];
- }
+static setNearby($elm, nearby) {
+$elm.nearby = $elm.nearby || {};
+let key;
+for (key in nearby)
+$elm.nearby[key] = nearby[key];
+}
}
var setNearby = NavigationUtils.setNearby;
var ButtonStyleClass = {
- 1: "bx-primary",
- 2: "bx-warning",
- 4: "bx-danger",
- 8: "bx-ghost",
- 16: "bx-frosted",
- 32: "bx-drop-shadow",
- 64: "bx-focusable",
- 128: "bx-full-width",
- 256: "bx-full-height",
- 512: "bx-auto-height",
- 1024: "bx-tall",
- 2048: "bx-circular",
- 4096: "bx-normal-case",
- 8192: "bx-normal-link"
+1: "bx-primary",
+2: "bx-warning",
+4: "bx-danger",
+8: "bx-ghost",
+16: "bx-frosted",
+32: "bx-drop-shadow",
+64: "bx-focusable",
+128: "bx-full-width",
+256: "bx-full-height",
+512: "bx-auto-height",
+1024: "bx-tall",
+2048: "bx-circular",
+4096: "bx-normal-case",
+8192: "bx-normal-link"
};
function createElement(elmName, props, ..._) {
- let $elm, hasNs = props && "xmlns" in props;
- if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;
- else $elm = document.createElement(elmName);
- if (props) {
- if (props._nearby) setNearby($elm, props._nearby), delete props._nearby;
- if (props._on) {
- for (let name in props._on)
- $elm.addEventListener(name, props._on[name]);
- delete props._on;
- }
- if (props._dataset) {
- for (let name in props._dataset)
- $elm.dataset[name] = props._dataset[name];
- delete props._dataset;
- }
- for (let key in props) {
- if ($elm.hasOwnProperty(key)) continue;
- let value = props[key];
- if (hasNs) $elm.setAttributeNS(null, key, value);
- else $elm.setAttribute(key, value);
- }
- }
- for (let i = 2, size = arguments.length;i < size; i++) {
- let arg = arguments[i];
- if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg);
- }
- return $elm;
+let $elm, hasNs = props && "xmlns" in props;
+if (hasNs) $elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;
+else $elm = document.createElement(elmName);
+if (props) {
+if (props._nearby) setNearby($elm, props._nearby), delete props._nearby;
+if (props._on) {
+for (let name in props._on)
+$elm.addEventListener(name, props._on[name]);
+delete props._on;
+}
+if (props._dataset) {
+for (let name in props._dataset)
+$elm.dataset[name] = props._dataset[name];
+delete props._dataset;
+}
+for (let key in props) {
+if ($elm.hasOwnProperty(key)) continue;
+let value = props[key];
+if (hasNs) $elm.setAttributeNS(null, key, value);
+else $elm.setAttribute(key, value);
+}
+}
+for (let i = 2, size = arguments.length;i < size; i++) {
+let arg = arguments[i];
+if (arg !== null && arg !== !1 && typeof arg !== "undefined") $elm.append(arg);
+}
+return $elm;
}
var domParser = new DOMParser;
function createSvgIcon(icon) {
- return domParser.parseFromString(icon, "image/svg+xml").documentElement;
+return domParser.parseFromString(icon, "image/svg+xml").documentElement;
}
var ButtonStyleIndices = Object.keys(ButtonStyleClass).map((i) => parseInt(i));
function createButton(options) {
- let $btn;
- if (options.url) $btn = CE("a", {
- class: "bx-button",
- href: options.url,
- target: "_blank"
- });
- else $btn = CE("button", {
- class: "bx-button",
- type: "button"
- }), options.disabled && ($btn.disabled = !0);
- let style = options.style || 0;
- if (style) {
- let index;
- for (index of ButtonStyleIndices)
- style & index && $btn.classList.add(ButtonStyleClass[index]);
- }
- if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText));
- for (let key in options.attributes)
- if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]);
- return $btn;
+let $btn;
+if (options.url) $btn = CE("a", {
+class: "bx-button",
+href: options.url,
+target: "_blank"
+});
+else $btn = CE("button", {
+class: "bx-button",
+type: "button"
+}), options.disabled && ($btn.disabled = !0);
+let style = options.style || 0;
+if (style) {
+let index;
+for (index of ButtonStyleIndices)
+style & index && $btn.classList.add(ButtonStyleClass[index]);
+}
+if (options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", !1, options.label)), options.title && $btn.setAttribute("title", options.title), options.onClick && $btn.addEventListener("click", options.onClick), $btn.tabIndex = typeof options.tabIndex === "number" ? options.tabIndex : 0, options.secondaryText) $btn.classList.add("bx-button-multi-lines"), $btn.appendChild(CE("span", !1, options.secondaryText));
+for (let key in options.attributes)
+if (!$btn.hasOwnProperty(key)) $btn.setAttribute(key, options.attributes[key]);
+return $btn;
}
function createSettingRow(label, $control, options = {}) {
- let $label, $row = CE("label", {
- class: "bx-settings-row"
- }, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control);
- if (options.pref) $row.prefKey = options.pref;
- if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu);
- let $link = $label.querySelector("a");
- if ($link) $link.classList.add("bx-focusable"), setNearby($label, {
- focus: $link
- });
- if (setNearby($row, {
- orientation: options.multiLines ? "vertical" : "horizontal"
- }), options.multiLines)
- $row.dataset.multiLines = "true";
- if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id;
- return $row;
+let $label, $row = CE("label", {
+class: "bx-settings-row"
+}, $label = CE("span", { class: "bx-settings-label" }, options.icon && createSvgIcon(options.icon), label, options.$note), $control);
+if (options.pref) $row.prefKey = options.pref;
+if (options.onContextMenu) $row.addEventListener("contextmenu", options.onContextMenu);
+let $link = $label.querySelector("a");
+if ($link) $link.classList.add("bx-focusable"), setNearby($label, {
+focus: $link
+});
+if (setNearby($row, {
+orientation: options.multiLines ? "vertical" : "horizontal"
+}), options.multiLines)
+$row.dataset.multiLines = "true";
+if ($control instanceof HTMLElement && $control.id) $row.htmlFor = $control.id;
+return $row;
}
function getReactProps($elm) {
- for (let key in $elm)
- if (key.startsWith("__reactProps")) return $elm[key];
- return null;
+for (let key in $elm)
+if (key.startsWith("__reactProps")) return $elm[key];
+return null;
}
function escapeHtml(html) {
- let text = document.createTextNode(html), $span = document.createElement("span");
- return $span.appendChild(text), $span.innerHTML;
+let text = document.createTextNode(html), $span = document.createElement("span");
+return $span.appendChild(text), $span.innerHTML;
}
function isElementVisible($elm) {
- let rect = $elm.getBoundingClientRect();
- return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;
+let rect = $elm.getBoundingClientRect();
+return (rect.x >= 0 || rect.y >= 0) && !!rect.width && !!rect.height;
}
function removeChildElements($parent) {
- if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select");
- while ($parent.firstElementChild)
- $parent.firstElementChild.remove();
+if ($parent instanceof HTMLDivElement && $parent.classList.contains("bx-select")) $parent = $parent.querySelector("select");
+while ($parent.firstElementChild)
+$parent.firstElementChild.remove();
}
function clearDataSet($elm) {
- Object.keys($elm.dataset).forEach((key) => {
- delete $elm.dataset[key];
- });
+Object.keys($elm.dataset).forEach((key) => {
+delete $elm.dataset[key];
+});
}
function renderPresetsList($select, allPresets, selectedValue, options = {}) {
- if (removeChildElements($select), options.addOffValue) {
- let $option = CE("option", { value: 0 }, t("off"));
- $option.selected = selectedValue === 0, $select.appendChild($option);
- }
- let groups = {
- default: t("default") + " 🔒",
- custom: t("custom")
- }, key;
- for (key in groups) {
- let $optGroup = CE("optgroup", { label: groups[key] });
- for (let id of allPresets[key]) {
- let record = allPresets.data[id], selected = selectedValue === record.id, name = options.selectedIndicator && selected ? "✅ " + record.name : record.name, $option = CE("option", { value: record.id }, name);
- if (selected) $option.selected = !0;
- $optGroup.appendChild($option);
- }
- if ($optGroup.hasChildNodes()) $select.appendChild($optGroup);
- }
+if (removeChildElements($select), options.addOffValue) {
+let $option = CE("option", { value: 0 }, t("off"));
+$option.selected = selectedValue === 0, $select.appendChild($option);
+}
+let groups = {
+default: t("default") + " 🔒",
+custom: t("custom")
+}, key;
+for (key in groups) {
+let $optGroup = CE("optgroup", { label: groups[key] });
+for (let id of allPresets[key]) {
+let record = allPresets.data[id], selected = selectedValue === record.id, name = options.selectedIndicator && selected ? "✅ " + record.name : record.name, $option = CE("option", { value: record.id }, name);
+if (selected) $option.selected = !0;
+$optGroup.appendChild($option);
+}
+if ($optGroup.hasChildNodes()) $select.appendChild($optGroup);
+}
}
function calculateSelectBoxes($root) {
- let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select"));
- for (let $select of selects) {
- let $parent = $select.parentElement;
- if ($parent.classList.contains("bx-full-width")) {
- $parent.dataset.calculated = "true";
- continue;
- }
- let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width);
- if (!width) continue;
- if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) {
- if ($select.multiple) width += 20;
- if ($select.querySelector("optgroup")) width -= 15;
- } else width += 10;
- $select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
- }
+let selects = Array.from($root.querySelectorAll("div.bx-select:not([data-calculated]) select"));
+for (let $select of selects) {
+let $parent = $select.parentElement;
+if ($parent.classList.contains("bx-full-width")) {
+$parent.dataset.calculated = "true";
+continue;
+}
+let rect = $select.getBoundingClientRect(), $label, width = Math.ceil(rect.width);
+if (!width) continue;
+if ($label = $parent.querySelector($select.multiple ? ".bx-select-value" : "div"), $parent.isControllerFriendly) {
+if ($select.multiple) width += 20;
+if ($select.querySelector("optgroup")) width -= 15;
+} else width += 10;
+$select.style.left = "0", $label.style.minWidth = width + "px", $parent.dataset.calculated = "true";
+}
}
var FILE_SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"];
function humanFileSize(size) {
- let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
- return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i];
+let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
+return (size / Math.pow(1024, i)).toFixed(1) + " " + FILE_SIZE_UNITS[i];
}
function secondsToHm(seconds) {
- let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1;
- if (m === 60) h += 1, m = 0;
- let output = [];
- return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" ");
+let h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1;
+if (m === 60) h += 1, m = 0;
+let output = [];
+return h > 0 && output.push(`${h}h`), m > 0 && output.push(`${m}m`), output.join(" ");
}
function escapeCssSelector(name) {
- return name.replaceAll(".", "-");
+return name.replaceAll(".", "-");
}
var CE = createElement;
window.BX_CE = createElement;
class Toast {
- static instance;
- static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);
- LOG_TAG = "Toast";
- $wrapper;
- $msg;
- $status;
- stack = [];
- isShowing = !1;
- timeoutId;
- DURATION = 3000;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {
- let classList = this.$wrapper.classList;
- if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();
- }), document.documentElement.appendChild(this.$wrapper);
- }
- show(msg, status, options = {}) {
- options = options || {};
- let args = Array.from(arguments);
- if (options.instant) this.stack = [args], this.showNext();
- else this.stack.push(args), !this.isShowing && this.showNext();
- }
- showNext() {
- if (!this.stack.length) {
- this.isShowing = !1;
- return;
- }
- this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION);
- let [msg, status, options] = this.stack.shift();
- if (options && options.html) this.$msg.innerHTML = msg;
- else this.$msg.textContent = msg;
- if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;
- else this.$status.classList.add("bx-gone");
- let classList = this.$wrapper.classList;
- classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
- }
- hide = () => {
- this.timeoutId = null;
- let classList = this.$wrapper.classList;
- classList.remove("bx-show"), classList.add("bx-hide");
- };
- static show(msg, status, options = {}) {
- Toast.getInstance().show(msg, status, options);
- }
- static showNext() {
- Toast.getInstance().showNext();
- }
+static instance;
+static getInstance = () => Toast.instance ?? (Toast.instance = new Toast);
+LOG_TAG = "Toast";
+$wrapper;
+$msg;
+$status;
+stack = [];
+isShowing = !1;
+timeoutId;
+DURATION = 3000;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, this.$msg = CE("span", { class: "bx-toast-msg" }), this.$status = CE("span", { class: "bx-toast-status" })), this.$wrapper.addEventListener("transitionend", (e) => {
+let classList = this.$wrapper.classList;
+if (classList.contains("bx-hide")) classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), this.showNext();
+}), document.documentElement.appendChild(this.$wrapper);
+}
+show(msg, status, options = {}) {
+options = options || {};
+let args = Array.from(arguments);
+if (options.instant) this.stack = [args], this.showNext();
+else this.stack.push(args), !this.isShowing && this.showNext();
+}
+showNext() {
+if (!this.stack.length) {
+this.isShowing = !1;
+return;
+}
+this.isShowing = !0, this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, this.DURATION);
+let [msg, status, options] = this.stack.shift();
+if (options && options.html) this.$msg.innerHTML = msg;
+else this.$msg.textContent = msg;
+if (status) this.$status.classList.remove("bx-gone"), this.$status.textContent = status;
+else this.$status.classList.add("bx-gone");
+let classList = this.$wrapper.classList;
+classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
+}
+hide = () => {
+this.timeoutId = null;
+let classList = this.$wrapper.classList;
+classList.remove("bx-show"), classList.add("bx-hide");
+};
+static show(msg, status, options = {}) {
+Toast.getInstance().show(msg, status, options);
+}
+static showNext() {
+Toast.getInstance().showNext();
+}
}
class MicrophoneShortcut {
- static toggle(showToast = !0) {
- if (!window.BX_EXPOSED.streamSession) return !1;
- let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0;
- try {
- return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic;
- } catch (e) {
- console.log(e);
- }
- return !1;
- }
+static toggle(showToast = !0) {
+if (!window.BX_EXPOSED.streamSession) return !1;
+let enableMic = window.BX_EXPOSED.streamSession._microphoneState === "Enabled" ? !1 : !0;
+try {
+return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic;
+} catch (e) {
+console.log(e);
+}
+return !1;
+}
}
class LocalDb {
- static instance;
- static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb);
- static DB_NAME = "BetterXcloud";
- static DB_VERSION = 4;
- static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers";
- static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts";
- static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations";
- static TABLE_CONTROLLER_SETTINGS = "controller_settings";
- static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts";
- db;
- open() {
- return new Promise((resolve, reject) => {
- if (this.db) {
- resolve(this.db);
- return;
- }
- let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
- request.onupgradeneeded = (e) => {
- let db = e.target.result;
- if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
- if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {
- keyPath: "id"
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, {
- keyPath: "id",
- autoIncrement: !0
- });
- if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {
- keyPath: "id",
- autoIncrement: !0
- });
- }, request.onerror = (e) => {
- console.log(e), alert(e.target.error.message), reject && reject();
- }, request.onsuccess = (e) => {
- this.db = e.target.result, resolve(this.db);
- };
- });
- }
+static instance;
+static getInstance = () => LocalDb.instance ?? (LocalDb.instance = new LocalDb);
+static DB_NAME = "BetterXcloud";
+static DB_VERSION = 4;
+static TABLE_VIRTUAL_CONTROLLERS = "virtual_controllers";
+static TABLE_CONTROLLER_SHORTCUTS = "controller_shortcuts";
+static TABLE_CONTROLLER_CUSTOMIZATIONS = "controller_customizations";
+static TABLE_CONTROLLER_SETTINGS = "controller_settings";
+static TABLE_KEYBOARD_SHORTCUTS = "keyboard_shortcuts";
+db;
+open() {
+return new Promise((resolve, reject) => {
+if (this.db) {
+resolve(this.db);
+return;
+}
+let request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
+request.onupgradeneeded = (e) => {
+let db = e.target.result;
+if (db.objectStoreNames.contains("undefined")) db.deleteObjectStore("undefined");
+if (!db.objectStoreNames.contains(LocalDb.TABLE_VIRTUAL_CONTROLLERS)) db.createObjectStore(LocalDb.TABLE_VIRTUAL_CONTROLLERS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SHORTCUTS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_SETTINGS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_SETTINGS, {
+keyPath: "id"
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS)) db.createObjectStore(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS, {
+keyPath: "id",
+autoIncrement: !0
+});
+if (!db.objectStoreNames.contains(LocalDb.TABLE_KEYBOARD_SHORTCUTS)) db.createObjectStore(LocalDb.TABLE_KEYBOARD_SHORTCUTS, {
+keyPath: "id",
+autoIncrement: !0
+});
+}, request.onerror = (e) => {
+console.log(e), alert(e.target.error.message), reject && reject();
+}, request.onsuccess = (e) => {
+this.db = e.target.result, resolve(this.db);
+};
+});
+}
}
var BypassServers = {
- br: t("brazil"),
- jp: t("japan"),
- kr: t("korea"),
- pl: t("poland"),
- us: t("united-states")
+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 BaseSettingsStorage {
- storage;
- storageKey;
- _settings;
- definitions;
- constructor(storageKey, definitions) {
- this.storage = window.localStorage, this.storageKey = storageKey;
- for (let [_, setting] of Object.entries(definitions)) {
- if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants];
- setting.ready && setting.ready.call(this, setting);
- }
- this.definitions = definitions, this._settings = null;
- }
- get settings() {
- if (this._settings) return this._settings;
- let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");
- for (let key in settings)
- settings[key] = this.validateValue("get", key, settings[key]);
- return this._settings = settings, settings;
- }
- getDefinition(key) {
- if (!this.definitions[key]) return alert("Request invalid definition: " + key), {};
- return this.definitions[key];
- }
- hasSetting(key) {
- return key in this.settings;
- }
- getSetting(key, checkUnsupported = !0) {
- let definition = this.definitions[key];
- if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
- if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;
- else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
- if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);
- return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key];
- }
- setSetting(key, value, origin) {
- if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", {
- storageKey: this.storageKey,
- settingKey: key
- });
- else BxEventBus.Script.emit("setting.changed", {
- storageKey: this.storageKey,
- settingKey: key
- });
- return value;
- }
- saveSettings() {
- this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
- }
- validateValue(action, key, value) {
- let def = this.definitions[key];
- if (!def) return value;
- if (typeof value === "undefined" || value === null) value = def.default;
- if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);
- if ("min" in def) value = Math.max(def.min, value);
- if ("max" in def) value = Math.min(def.max, value);
- if ("options" in def) {
- if (!(value in def.options)) value = def.default;
- } else if ("multipleOptions" in def) {
- if (value.length) {
- let validOptions = Object.keys(def.multipleOptions);
- value.forEach((item2, idx) => {
- validOptions.indexOf(item2) === -1 && value.splice(idx, 1);
- });
- }
- if (!value.length) value = def.default;
- }
- if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);
- return value;
- }
- getLabel(key) {
- return this.definitions[key].label || key;
- }
- getValueText(key, value) {
- let definition = this.definitions[key];
- if ("min" in definition) {
- let params = definition.params;
- if (params.customTextValue) {
- if (definition.transformValue) value = definition.transformValue.get.call(definition, value);
- let text = params.customTextValue(value, definition.min, definition.max);
- if (text) return text;
- }
- return value.toString();
- } else if ("options" in definition) {
- let options = definition.options;
- if (value in options) return options[value];
- } else if (typeof value === "boolean") return value ? t("on") : t("off");
- return value.toString();
- }
+storage;
+storageKey;
+_settings;
+definitions;
+constructor(storageKey, definitions) {
+this.storage = window.localStorage, this.storageKey = storageKey;
+for (let [_, setting] of Object.entries(definitions)) {
+if (typeof setting.requiredVariants === "string") setting.requiredVariants = [setting.requiredVariants];
+setting.ready && setting.ready.call(this, setting);
+}
+this.definitions = definitions, this._settings = null;
+}
+get settings() {
+if (this._settings) return this._settings;
+let settings = JSON.parse(this.storage.getItem(this.storageKey) || "{}");
+for (let key in settings)
+settings[key] = this.validateValue("get", key, settings[key]);
+return this._settings = settings, settings;
+}
+getDefinition(key) {
+if (!this.definitions[key]) return alert("Request invalid definition: " + key), {};
+return this.definitions[key];
+}
+hasSetting(key) {
+return key in this.settings;
+}
+getSetting(key, checkUnsupported = !0) {
+let definition = this.definitions[key];
+if (definition.requiredVariants && !definition.requiredVariants.includes(SCRIPT_VARIANT)) return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
+if (checkUnsupported && definition.unsupported) if ("unsupportedValue" in definition) return definition.unsupportedValue;
+else return isPlainObject(definition.default) ? deepClone(definition.default) : definition.default;
+if (!(key in this.settings)) this.settings[key] = this.validateValue("get", key, null);
+return isPlainObject(this.settings[key]) ? deepClone(this.settings[key]) : this.settings[key];
+}
+setSetting(key, value, origin) {
+if (value = this.validateValue("set", key, value), this.settings[key] = this.validateValue("get", key, value), this.saveSettings(), origin === "ui") if (isStreamPref(key)) BxEventBus.Stream.emit("setting.changed", {
+storageKey: this.storageKey,
+settingKey: key
+});
+else BxEventBus.Script.emit("setting.changed", {
+storageKey: this.storageKey,
+settingKey: key
+});
+return value;
+}
+saveSettings() {
+this.storage.setItem(this.storageKey, JSON.stringify(this.settings));
+}
+validateValue(action, key, value) {
+let def = this.definitions[key];
+if (!def) return value;
+if (typeof value === "undefined" || value === null) value = def.default;
+if (def.transformValue && action === "get") value = def.transformValue.get.call(def, value);
+if ("min" in def) value = Math.max(def.min, value);
+if ("max" in def) value = Math.min(def.max, value);
+if ("options" in def) {
+if (!(value in def.options)) value = def.default;
+} else if ("multipleOptions" in def) {
+if (value.length) {
+let validOptions = Object.keys(def.multipleOptions);
+value.forEach((item2, idx) => {
+validOptions.indexOf(item2) === -1 && value.splice(idx, 1);
+});
+}
+if (!value.length) value = def.default;
+}
+if (def.transformValue && action === "set") value = def.transformValue.set.call(def, value);
+return value;
+}
+getLabel(key) {
+return this.definitions[key].label || key;
+}
+getValueText(key, value) {
+let definition = this.definitions[key];
+if ("min" in definition) {
+let params = definition.params;
+if (params.customTextValue) {
+if (definition.transformValue) value = definition.transformValue.get.call(definition, value);
+let text = params.customTextValue(value, definition.min, definition.max);
+if (text) return text;
+}
+return value.toString();
+} else if ("options" in definition) {
+let options = definition.options;
+if (value in options) return options[value];
+} else if (typeof value === "boolean") return value ? t("on") : t("off");
+return value.toString();
+}
}
var BxIcon = {
- BETTER_XCLOUD: "",
- TRUE_ACHIEVEMENTS: "",
- STREAM_SETTINGS: "",
- STREAM_STATS: "",
- CLOSE: "",
- CONTROLLER: "",
- CREATE_SHORTCUT: "",
- DISPLAY: "",
- EYE: "",
- EYE_SLASH: "",
- HOME: "",
- LOCAL_CO_OP: "",
- NATIVE_MKB: "",
- NEW: "",
- MANAGE: "",
- COPY: "",
- TRASH: "",
- CURSOR_TEXT: "",
- POWER: "",
- QUESTION: "",
- REFRESH: "",
- REMOTE_PLAY: "",
- CARET_LEFT: "",
- CARET_RIGHT: "",
- SCREENSHOT: "",
- SPEAKER_MUTED: "",
- TOUCH_CONTROL_ENABLE: "",
- TOUCH_CONTROL_DISABLE: "",
- MICROPHONE: "",
- MICROPHONE_MUTED: "",
- BATTERY: "",
- PLAYTIME: "",
- SERVER: "",
- DOWNLOAD: "",
- UPLOAD: "",
- AUDIO: ""
+BETTER_XCLOUD: "",
+TRUE_ACHIEVEMENTS: "",
+STREAM_SETTINGS: "",
+STREAM_STATS: "",
+CLOSE: "",
+CONTROLLER: "",
+CREATE_SHORTCUT: "",
+DISPLAY: "",
+EYE: "",
+EYE_SLASH: "",
+HOME: "",
+LOCAL_CO_OP: "",
+NATIVE_MKB: "",
+NEW: "",
+MANAGE: "",
+COPY: "",
+TRASH: "",
+CURSOR_TEXT: "",
+POWER: "",
+QUESTION: "",
+REFRESH: "",
+REMOTE_PLAY: "",
+CARET_LEFT: "",
+CARET_RIGHT: "",
+SCREENSHOT: "",
+SPEAKER_MUTED: "",
+TOUCH_CONTROL_ENABLE: "",
+TOUCH_CONTROL_DISABLE: "",
+MICROPHONE: "",
+MICROPHONE_MUTED: "",
+BATTERY: "",
+PLAYTIME: "",
+SERVER: "",
+DOWNLOAD: "",
+UPLOAD: "",
+AUDIO: ""
};
function getSupportedCodecProfiles() {
- let options = {
- default: t("default")
- };
- if (!("getCapabilities" in RTCRtpReceiver)) return options;
- let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;
- for (let codec of codecs) {
- if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;
- let fmtp = codec.sdpFmtpLine.toLowerCase();
- if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;
- else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;
- else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;
- }
- if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`;
- else options["low"] = t("visual-quality-low");
- if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`;
- else options["normal"] = t("visual-quality-normal");
- if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`;
- else options["high"] = t("visual-quality-high");
- return options;
+let options = {
+default: t("default")
+};
+if (!("getCapabilities" in RTCRtpReceiver)) return options;
+let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1, codecs = RTCRtpReceiver.getCapabilities("video").codecs;
+for (let codec of codecs) {
+if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine) continue;
+let fmtp = codec.sdpFmtpLine.toLowerCase();
+if (fmtp.includes("profile-level-id=4d")) hasHighCodec = !0;
+else if (fmtp.includes("profile-level-id=42e")) hasNormalCodec = !0;
+else if (fmtp.includes("profile-level-id=420")) hasLowCodec = !0;
+}
+if (hasLowCodec) if (!hasNormalCodec && !hasHighCodec) options["default"] = `${t("visual-quality-low")} (${t("default")})`;
+else options["low"] = t("visual-quality-low");
+if (hasNormalCodec) if (!hasLowCodec && !hasHighCodec) options["default"] = `${t("visual-quality-normal")} (${t("default")})`;
+else options["normal"] = t("visual-quality-normal");
+if (hasHighCodec) if (!hasLowCodec && !hasNormalCodec) options["default"] = `${t("visual-quality-high")} (${t("default")})`;
+else options["high"] = t("visual-quality-high");
+return options;
}
class GlobalSettingsStorage extends BaseSettingsStorage {
- static DEFINITIONS = {
- "version.lastCheck": {
- default: 0
- },
- "version.latest": {
- default: ""
- },
- "version.current": {
- default: ""
- },
- "bx.locale": {
- label: t("language"),
- default: localStorage.getItem("BetterXcloud.Locale") || "en-US",
- options: SUPPORTED_LANGUAGES
- },
- "server.region": {
- label: t("region"),
- note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
- default: "default"
- },
- "server.bypassRestriction": {
- label: t("bypass-region-restriction"),
- note: "⚠️ " + t("use-this-at-your-own-risk"),
- default: "off",
- optionsGroup: t("region"),
- options: Object.assign({
- off: t("off")
- }, BypassServers)
- },
- "stream.locale": {
- label: t("preferred-game-language"),
- default: "default",
- options: {
- default: t("default"),
- "ar-SA": "العربية",
- "bg-BG": "Български",
- "cs-CZ": "čeština",
- "da-DK": "dansk",
- "de-DE": "Deutsch",
- "el-GR": "Ελληνικά",
- "en-GB": "English (UK)",
- "en-US": "English (US)",
- "es-ES": "español (España)",
- "es-MX": "español (Latinoamérica)",
- "fi-FI": "suomi",
- "fr-FR": "français",
- "he-IL": "עברית",
- "hu-HU": "magyar",
- "it-IT": "italiano",
- "ja-JP": "日本語",
- "ko-KR": "한국어",
- "nb-NO": "norsk bokmål",
- "nl-NL": "Nederlands",
- "pl-PL": "polski",
- "pt-BR": "português (Brasil)",
- "pt-PT": "português (Portugal)",
- "ro-RO": "Română",
- "ru-RU": "русский",
- "sk-SK": "slovenčina",
- "sv-SE": "svenska",
- "th-TH": "ไทย",
- "tr-TR": "Türkçe",
- "zh-CN": "中文(简体)",
- "zh-TW": "中文 (繁體)"
- }
- },
- "stream.video.resolution": {
- label: t("target-resolution"),
- default: "auto",
- options: {
- auto: t("default"),
- "720p": "720p",
- "1080p": "1080p",
- "1080p-hq": "1080p (HQ)"
- },
- suggest: {
- lowest: "720p",
- highest: "1080p-hq"
- }
- },
- "stream.video.codecProfile": {
- label: t("visual-quality"),
- default: "default",
- options: getSupportedCodecProfiles(),
- ready: (setting) => {
- let options = setting.options, keys = Object.keys(options);
- if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");
- setting.suggest = {
- lowest: keys.length === 1 ? keys[0] : keys[1],
- highest: keys[keys.length - 1]
- };
- }
- },
- "server.ipv6.prefer": {
- label: t("prefer-ipv6-server"),
- default: !1
- },
- "screenshot.applyFilters": {
- requiredVariants: "full",
- label: t("screenshot-apply-filters"),
- default: !1
- },
- "ui.splashVideo.skip": {
- label: t("skip-splash-video"),
- default: !1
- },
- "ui.systemMenu.hideHandle": {
- label: "⣿ " + t("hide-system-menu-icon"),
- default: !1
- },
- "ui.imageQuality": {
- requiredVariants: "full",
- label: t("image-quality"),
- default: 90,
- min: 10,
- max: 90,
- params: {
- steps: 10,
- exactTicks: 20,
- hideSlider: !0,
- customTextValue(value, min, max) {
- if (value === 90) return t("default");
- return value + "%";
- }
- }
- },
- "stream.video.combineAudio": {
- requiredVariants: "full",
- label: t("combine-audio-video-streams"),
- default: !1,
- experimental: !0,
- note: t("combine-audio-video-streams-summary")
- },
- "touchController.mode": {
- requiredVariants: "full",
- label: t("tc-availability"),
- default: "all",
- options: {
- default: t("default"),
- off: t("off"),
- all: t("tc-all-games")
- },
- unsupported: !STATES.userAgent.capabilities.touch,
- unsupportedValue: "default"
- },
- "touchController.autoOff": {
- requiredVariants: "full",
- label: t("tc-auto-off"),
- default: !1,
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.opacity.default": {
- requiredVariants: "full",
- label: t("tc-default-opacity"),
- default: 100,
- min: 10,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10,
- hideSlider: !0
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.style.standard": {
- requiredVariants: "full",
- label: t("tc-standard-layout-style"),
- default: "default",
- options: {
- default: t("default"),
- white: t("tc-all-white"),
- muted: t("tc-muted-colors")
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "touchController.style.custom": {
- requiredVariants: "full",
- label: t("tc-custom-layout-style"),
- default: "default",
- options: {
- default: t("default"),
- muted: t("tc-muted-colors")
- },
- unsupported: !STATES.userAgent.capabilities.touch
- },
- "ui.streamMenu.simplify": {
- label: t("simplify-stream-menu"),
- default: !1
- },
- "mkb.cursor.hideIdle": {
- requiredVariants: "full",
- label: t("hide-idle-cursor"),
- default: !1
- },
- "ui.feedbackDialog.disabled": {
- requiredVariants: "full",
- label: t("disable-post-stream-feedback-dialog"),
- default: !1
- },
- "stream.video.maxBitrate": {
- requiredVariants: "full",
- label: t("bitrate-video-maximum"),
- note: "⚠️ " + t("unexpected-behavior"),
- default: 0,
- min: 102400,
- max: 15360000,
- transformValue: {
- get(value) {
- return value === 0 ? this.max : value;
- },
- set(value) {
- return value === this.max ? 0 : value;
- }
- },
- params: {
- steps: 102400,
- exactTicks: 5120000,
- customTextValue: (value, min, max) => {
- if (value = parseInt(value), value === max) return t("unlimited");
- else return (value / 1024000).toFixed(1) + " Mb/s";
- }
- },
- suggest: {
- highest: 0
- }
- },
- "gameBar.position": {
- requiredVariants: "full",
- label: t("position"),
- default: "bottom-left",
- options: {
- off: t("off"),
- "bottom-left": t("bottom-left"),
- "bottom-right": t("bottom-right")
- }
- },
- "ui.controllerStatus.show": {
- label: t("show-controller-connection-status"),
- default: !0
- },
- "mkb.enabled": {
- requiredVariants: "full",
- label: t("enable-mkb"),
- default: !1,
- unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,
- ready: (setting) => {
- let note, url;
- if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
- else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
- setting.unsupportedNote = () => CE("a", {
- href: url,
- target: "_blank"
- }, "⚠️ " + note);
- }
- },
- "nativeMkb.mode": {
- requiredVariants: "full",
- label: t("native-mkb"),
- default: "default",
- options: {
- default: t("default"),
- off: t("off"),
- on: t("on")
- },
- ready: (setting) => {
- if (STATES.browser.capabilities.emulatedNativeMkb) ;
- else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];
- else delete setting.options["on"];
- }
- },
- "nativeMkb.forcedGames": {
- label: t("force-native-mkb-games"),
- default: [],
- unsupported: !AppInterface && UserAgent.isMobile(),
- ready: (setting) => {
- if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {
- setting.multipleOptions = payload.data.data;
- });
- },
- params: {
- size: 6
- }
- },
- "ui.reduceAnimations": {
- label: t("reduce-animations"),
- default: !1
- },
- "loadingScreen.gameArt.show": {
- requiredVariants: "full",
- label: t("show-game-art"),
- default: !0
- },
- "loadingScreen.waitTime.show": {
- label: t("show-wait-time"),
- default: !0
- },
- "loadingScreen.rocket": {
- label: t("rocket-animation"),
- default: "show",
- options: {
- show: t("rocket-always-show"),
- "hide-queue": t("rocket-hide-queue"),
- hide: t("rocket-always-hide")
- }
- },
- "ui.controllerFriendly": {
- label: t("controller-friendly-ui"),
- default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"
- },
- "ui.layout": {
- requiredVariants: "full",
- label: t("layout"),
- default: "default",
- options: {
- default: t("default"),
- normal: t("normal"),
- tv: t("smart-tv")
- }
- },
- "ui.hideScrollbar": {
- label: t("hide-scrollbar"),
- default: !1
- },
- "ui.hideSections": {
- requiredVariants: "full",
- label: t("hide-sections"),
- default: [],
- multipleOptions: {
- news: t("section-news"),
- friends: t("section-play-with-friends"),
- "native-mkb": t("section-native-mkb"),
- touch: t("section-touch"),
- "most-popular": t("section-most-popular"),
- "all-games": t("section-all-games")
- },
- params: {
- size: 0
- }
- },
- "ui.gameCard.waitTime.show": {
- requiredVariants: "full",
- label: t("show-wait-time-in-game-card"),
- default: !0
- },
- "block.tracking": {
- label: t("disable-xcloud-analytics"),
- default: !1
- },
- "block.features": {
- requiredVariants: "full",
- label: t("disable-features"),
- default: [],
- multipleOptions: {
- chat: t("chat"),
- friends: t("friends-followers"),
- byog: t("stream-your-own-game"),
- "notifications-invites": t("notifications") + ": " + t("invites"),
- "notifications-achievements": t("notifications") + ": " + t("achievements")
- }
- },
- "userAgent.profile": {
- label: t("user-agent-profile"),
- note: "⚠️ " + t("unexpected-behavior"),
- default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",
- options: {
- default: t("default"),
- "windows-edge": "Edge + Windows",
- "macos-safari": "Safari + macOS",
- "vr-oculus": "Android TV",
- "smarttv-generic": "Smart TV",
- "smarttv-tizen": "Samsung Smart TV",
- custom: t("custom")
- }
- },
- "audio.mic.onPlaying": {
- label: t("enable-mic-on-startup"),
- default: !1
- },
- "audio.volume.booster.enabled": {
- requiredVariants: "full",
- label: t("enable-volume-control"),
- default: !1
- },
- "xhome.enabled": {
- requiredVariants: "full",
- label: t("enable-remote-play-feature"),
- labelIcon: BxIcon.REMOTE_PLAY,
- default: !1
- },
- "xhome.video.resolution": {
- requiredVariants: "full",
- default: "1080p",
- options: {
- "720p": "720p",
- "1080p": "1080p",
- "1080p-hq": "1080p (HQ)"
- }
- },
- "game.fortnite.forceConsole": {
- requiredVariants: "full",
- label: "🎮 " + t("fortnite-force-console-version"),
- default: !1,
- note: t("fortnite-allow-stw-mode")
- }
- };
- constructor() {
- super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);
- }
+static DEFINITIONS = {
+"version.lastCheck": {
+default: 0
+},
+"version.latest": {
+default: ""
+},
+"version.current": {
+default: ""
+},
+"bx.locale": {
+label: t("language"),
+default: localStorage.getItem("BetterXcloud.Locale") || "en-US",
+options: SUPPORTED_LANGUAGES
+},
+"server.region": {
+label: t("region"),
+note: CE("a", { target: "_blank", href: "https://umap.openstreetmap.fr/en/map/xbox-cloud-gaming-servers_1135022" }, t("server-locations")),
+default: "default"
+},
+"server.bypassRestriction": {
+label: t("bypass-region-restriction"),
+note: "⚠️ " + t("use-this-at-your-own-risk"),
+default: "off",
+optionsGroup: t("region"),
+options: Object.assign({
+off: t("off")
+}, BypassServers)
+},
+"stream.locale": {
+label: t("preferred-game-language"),
+default: "default",
+options: {
+default: t("default"),
+"ar-SA": "العربية",
+"bg-BG": "Български",
+"cs-CZ": "čeština",
+"da-DK": "dansk",
+"de-DE": "Deutsch",
+"el-GR": "Ελληνικά",
+"en-GB": "English (UK)",
+"en-US": "English (US)",
+"es-ES": "español (España)",
+"es-MX": "español (Latinoamérica)",
+"fi-FI": "suomi",
+"fr-FR": "français",
+"he-IL": "עברית",
+"hu-HU": "magyar",
+"it-IT": "italiano",
+"ja-JP": "日本語",
+"ko-KR": "한국어",
+"nb-NO": "norsk bokmål",
+"nl-NL": "Nederlands",
+"pl-PL": "polski",
+"pt-BR": "português (Brasil)",
+"pt-PT": "português (Portugal)",
+"ro-RO": "Română",
+"ru-RU": "русский",
+"sk-SK": "slovenčina",
+"sv-SE": "svenska",
+"th-TH": "ไทย",
+"tr-TR": "Türkçe",
+"zh-CN": "中文(简体)",
+"zh-TW": "中文 (繁體)"
+}
+},
+"stream.video.resolution": {
+label: t("target-resolution"),
+default: "auto",
+options: {
+auto: t("default"),
+"720p": "720p",
+"1080p": "1080p",
+"1080p-hq": "1080p (HQ)"
+},
+suggest: {
+lowest: "720p",
+highest: "1080p-hq"
+}
+},
+"stream.video.codecProfile": {
+label: t("visual-quality"),
+default: "default",
+options: getSupportedCodecProfiles(),
+ready: (setting) => {
+let options = setting.options, keys = Object.keys(options);
+if (keys.length <= 1) setting.unsupported = !0, setting.unsupportedNote = "⚠️ " + t("browser-unsupported-feature");
+setting.suggest = {
+lowest: keys.length === 1 ? keys[0] : keys[1],
+highest: keys[keys.length - 1]
+};
+}
+},
+"server.ipv6.prefer": {
+label: t("prefer-ipv6-server"),
+default: !1
+},
+"screenshot.applyFilters": {
+requiredVariants: "full",
+label: t("screenshot-apply-filters"),
+default: !1
+},
+"ui.splashVideo.skip": {
+label: t("skip-splash-video"),
+default: !1
+},
+"ui.systemMenu.hideHandle": {
+label: "⣿ " + t("hide-system-menu-icon"),
+default: !1
+},
+"ui.imageQuality": {
+requiredVariants: "full",
+label: t("image-quality"),
+default: 90,
+min: 10,
+max: 90,
+params: {
+steps: 10,
+exactTicks: 20,
+hideSlider: !0,
+customTextValue(value, min, max) {
+if (value === 90) return t("default");
+return value + "%";
+}
+}
+},
+"stream.video.combineAudio": {
+requiredVariants: "full",
+label: t("combine-audio-video-streams"),
+default: !1,
+experimental: !0,
+note: t("combine-audio-video-streams-summary")
+},
+"touchController.mode": {
+requiredVariants: "full",
+label: t("availability"),
+default: "all",
+options: {
+default: t("default"),
+off: t("off"),
+all: t("all-games")
+},
+unsupported: !STATES.userAgent.capabilities.touch,
+unsupportedValue: "default"
+},
+"touchController.autoOff": {
+requiredVariants: "full",
+label: t("tc-auto-off"),
+default: !1,
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.opacity.default": {
+requiredVariants: "full",
+label: t("default-opacity"),
+default: 100,
+min: 10,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10,
+hideSlider: !0
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.style.standard": {
+requiredVariants: "full",
+label: t("tc-standard-layout-style"),
+default: "default",
+options: {
+default: t("default"),
+white: t("tc-all-white"),
+muted: t("tc-muted-colors")
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"touchController.style.custom": {
+requiredVariants: "full",
+label: t("tc-custom-layout-style"),
+default: "default",
+options: {
+default: t("default"),
+muted: t("tc-muted-colors")
+},
+unsupported: !STATES.userAgent.capabilities.touch
+},
+"ui.streamMenu.simplify": {
+label: t("simplify-stream-menu"),
+default: !1
+},
+"mkb.cursor.hideIdle": {
+requiredVariants: "full",
+label: t("hide-idle-cursor"),
+default: !1
+},
+"ui.feedbackDialog.disabled": {
+requiredVariants: "full",
+label: t("disable-post-stream-feedback-dialog"),
+default: !1
+},
+"stream.video.maxBitrate": {
+requiredVariants: "full",
+label: t("bitrate-video-maximum"),
+note: "⚠️ " + t("unexpected-behavior"),
+default: 0,
+min: 102400,
+max: 15360000,
+transformValue: {
+get(value) {
+return value === 0 ? this.max : value;
+},
+set(value) {
+return value === this.max ? 0 : value;
+}
+},
+params: {
+steps: 102400,
+exactTicks: 5120000,
+customTextValue: (value, min, max) => {
+if (value = parseInt(value), value === max) return t("unlimited");
+else return (value / 1024000).toFixed(1) + " Mb/s";
+}
+},
+suggest: {
+highest: 0
+}
+},
+"gameBar.position": {
+requiredVariants: "full",
+label: t("position"),
+default: "bottom-left",
+options: {
+off: t("off"),
+"bottom-left": t("bottom-left"),
+"bottom-right": t("bottom-right")
+}
+},
+"ui.controllerStatus.show": {
+label: t("show-controller-connection-status"),
+default: !0
+},
+"mkb.enabled": {
+requiredVariants: "full",
+label: t("enable-mkb"),
+default: !1,
+unsupported: !STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb,
+ready: (setting) => {
+let note, url;
+if (setting.unsupported) note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
+else note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
+setting.unsupportedNote = () => CE("a", {
+href: url,
+target: "_blank"
+}, "⚠️ " + note);
+}
+},
+"nativeMkb.mode": {
+requiredVariants: "full",
+label: t("native-mkb"),
+default: "default",
+options: {
+default: t("default"),
+off: t("off"),
+on: t("on")
+},
+ready: (setting) => {
+if (STATES.browser.capabilities.emulatedNativeMkb) ;
+else if (UserAgent.isMobile()) setting.unsupported = !0, setting.unsupportedValue = "off", delete setting.options["default"], delete setting.options["on"];
+else delete setting.options["on"];
+}
+},
+"nativeMkb.forcedGames": {
+label: t("force-native-mkb-games"),
+default: [],
+unsupported: !AppInterface && UserAgent.isMobile(),
+ready: (setting) => {
+if (!setting.unsupported) setting.multipleOptions = GhPagesUtils.getNativeMkbCustomList(!0), BxEventBus.Script.once("list.forcedNativeMkb.updated", (payload) => {
+setting.multipleOptions = payload.data.data;
+});
+},
+params: {
+size: 6
+}
+},
+"ui.reduceAnimations": {
+label: t("reduce-animations"),
+default: !1
+},
+"loadingScreen.gameArt.show": {
+requiredVariants: "full",
+label: t("show-game-art"),
+default: !0
+},
+"loadingScreen.waitTime.show": {
+label: t("show-wait-time"),
+default: !0
+},
+"loadingScreen.rocket": {
+label: t("rocket-animation"),
+default: "show",
+options: {
+show: t("rocket-always-show"),
+"hide-queue": t("rocket-hide-queue"),
+hide: t("rocket-always-hide")
+}
+},
+"ui.controllerFriendly": {
+label: t("controller-friendly-ui"),
+default: BX_FLAGS.DeviceInfo.deviceType !== "unknown"
+},
+"ui.layout": {
+requiredVariants: "full",
+label: t("layout"),
+default: "default",
+options: {
+default: t("default"),
+normal: t("normal"),
+tv: t("smart-tv")
+}
+},
+"ui.hideScrollbar": {
+label: t("hide-scrollbar"),
+default: !1
+},
+"ui.hideSections": {
+requiredVariants: "full",
+label: t("hide-sections"),
+default: [],
+multipleOptions: {
+news: t("section-news"),
+friends: t("section-play-with-friends"),
+"native-mkb": t("section-native-mkb"),
+touch: t("section-touch"),
+"most-popular": t("section-most-popular"),
+"all-games": t("section-all-games")
+},
+params: {
+size: 0
+}
+},
+"ui.gameCard.waitTime.show": {
+requiredVariants: "full",
+label: t("show-wait-time-in-game-card"),
+default: !0
+},
+"block.tracking": {
+label: t("disable-xcloud-analytics"),
+default: !1
+},
+"block.features": {
+requiredVariants: "full",
+label: t("disable-features"),
+default: [],
+multipleOptions: {
+chat: t("chat"),
+friends: t("friends-followers"),
+byog: t("stream-your-own-game"),
+"notifications-invites": t("notifications") + ": " + t("invites"),
+"notifications-achievements": t("notifications") + ": " + t("achievements")
+}
+},
+"userAgent.profile": {
+label: t("user-agent-profile"),
+note: "⚠️ " + t("unexpected-behavior"),
+default: BX_FLAGS.DeviceInfo.deviceType === "android-tv" || BX_FLAGS.DeviceInfo.deviceType === "webos" ? "vr-oculus" : "default",
+options: {
+default: t("default"),
+"windows-edge": "Edge + Windows",
+"macos-safari": "Safari + macOS",
+"vr-oculus": "Android TV",
+"smarttv-generic": "Smart TV",
+"smarttv-tizen": "Samsung Smart TV",
+custom: t("custom")
+}
+},
+"audio.mic.onPlaying": {
+label: t("enable-mic-on-startup"),
+default: !1
+},
+"audio.volume.booster.enabled": {
+requiredVariants: "full",
+label: t("enable-volume-control"),
+default: !1
+},
+"xhome.enabled": {
+requiredVariants: "full",
+label: t("enable-remote-play-feature"),
+labelIcon: BxIcon.REMOTE_PLAY,
+default: !1
+},
+"xhome.video.resolution": {
+requiredVariants: "full",
+default: "1080p",
+options: {
+"720p": "720p",
+"1080p": "1080p",
+"1080p-hq": "1080p (HQ)"
+}
+},
+"game.fortnite.forceConsole": {
+requiredVariants: "full",
+label: "🎮 " + t("fortnite-force-console-version"),
+default: !1,
+note: t("fortnite-allow-stw-mode")
+}
+};
+constructor() {
+super("BetterXcloud", GlobalSettingsStorage.DEFINITIONS);
+}
}
class BaseLocalTable {
- tableName;
- constructor(tableName) {
- this.tableName = tableName;
- }
- async prepareTable(type = "readonly") {
- return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);
- }
- call(method) {
- return new Promise((resolve) => {
- let request = method.call(null, ...Array.from(arguments).slice(1));
- request.onsuccess = (e) => {
- resolve(e.target.result);
- };
- });
- }
- async count() {
- let table = await this.prepareTable();
- return this.call(table.count.bind(table));
- }
- async add(data) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.add.bind(table), ...arguments);
- }
- async put(data) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.put.bind(table), ...arguments);
- }
- async delete(id) {
- let table = await this.prepareTable("readwrite");
- return this.call(table.delete.bind(table), ...arguments);
- }
- async get(id) {
- let table = await this.prepareTable();
- return this.call(table.get.bind(table), ...arguments);
- }
- async getAll() {
- let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};
- return all.forEach((item2) => {
- results[item2.id] = item2;
- }), results;
- }
+tableName;
+constructor(tableName) {
+this.tableName = tableName;
+}
+async prepareTable(type = "readonly") {
+return (await LocalDb.getInstance().open()).transaction(this.tableName, type).objectStore(this.tableName);
+}
+call(method) {
+return new Promise((resolve) => {
+let request = method.call(null, ...Array.from(arguments).slice(1));
+request.onsuccess = (e) => {
+resolve(e.target.result);
+};
+});
+}
+async count() {
+let table = await this.prepareTable();
+return this.call(table.count.bind(table));
+}
+async add(data) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.add.bind(table), ...arguments);
+}
+async put(data) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.put.bind(table), ...arguments);
+}
+async delete(id) {
+let table = await this.prepareTable("readwrite");
+return this.call(table.delete.bind(table), ...arguments);
+}
+async get(id) {
+let table = await this.prepareTable();
+return this.call(table.get.bind(table), ...arguments);
+}
+async getAll() {
+let table = await this.prepareTable(), all = await this.call(table.getAll.bind(table), ...arguments), results = {};
+return all.forEach((item2) => {
+results[item2.id] = item2;
+}), results;
+}
}
class BasePresetsTable extends BaseLocalTable {
- async newPreset(name, data) {
- let newRecord = { name, data };
- return await this.add(newRecord);
- }
- async updatePreset(preset) {
- return await this.put(preset);
- }
- async deletePreset(id) {
- return this.delete(id);
- }
- async getPreset(id) {
- if (id === 0) return null;
- if (id < 0) return this.DEFAULT_PRESETS[id];
- let preset = await this.get(id);
- if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];
- return preset;
- }
- async getPresets() {
- let all = deepClone(this.DEFAULT_PRESETS), presets = {
- default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)),
- custom: [],
- data: {}
- };
- if (await this.count() > 0) {
- let items = await this.getAll(), id;
- for (id in items) {
- let item2 = items[id];
- presets.custom.push(item2.id), all[item2.id] = item2;
- }
- }
- return presets.data = all, presets;
- }
- async getPresetsData() {
- let presetsData = {};
- for (let id in this.DEFAULT_PRESETS) {
- let preset = this.DEFAULT_PRESETS[id];
- presetsData[id] = deepClone(preset.data);
- }
- if (await this.count() > 0) {
- let items = await this.getAll(), id;
- for (id in items) {
- let item2 = items[id];
- presetsData[item2.id] = item2.data;
- }
- }
- return presetsData;
- }
+async newPreset(name, data) {
+let newRecord = { name, data };
+return await this.add(newRecord);
+}
+async updatePreset(preset) {
+return await this.put(preset);
+}
+async deletePreset(id) {
+return this.delete(id);
+}
+async getPreset(id) {
+if (id === 0) return null;
+if (id < 0) return this.DEFAULT_PRESETS[id];
+let preset = await this.get(id);
+if (!preset) preset = this.DEFAULT_PRESETS[this.DEFAULT_PRESET_ID];
+return preset;
+}
+async getPresets() {
+let all = deepClone(this.DEFAULT_PRESETS), presets = {
+default: Object.keys(this.DEFAULT_PRESETS).map((key) => parseInt(key)),
+custom: [],
+data: {}
+};
+if (await this.count() > 0) {
+let items = await this.getAll(), id;
+for (id in items) {
+let item2 = items[id];
+presets.custom.push(item2.id), all[item2.id] = item2;
+}
+}
+return presets.data = all, presets;
+}
+async getPresetsData() {
+let presetsData = {};
+for (let id in this.DEFAULT_PRESETS) {
+let preset = this.DEFAULT_PRESETS[id];
+presetsData[id] = deepClone(preset.data);
+}
+if (await this.count() > 0) {
+let items = await this.getAll(), id;
+for (id in items) {
+let item2 = items[id];
+presetsData[item2.id] = item2.data;
+}
+}
+return presetsData;
+}
}
class KeyboardShortcutsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);
- LOG_TAG = "KeyboardShortcutsTable";
- TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: t("standard"),
- data: {
- mapping: {
- "mkb.toggle": {
- code: "F8"
- },
- "stream.screenshot.capture": {
- code: "Slash"
- }
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {}
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
+static instance;
+static getInstance = () => KeyboardShortcutsTable.instance ?? (KeyboardShortcutsTable.instance = new KeyboardShortcutsTable);
+LOG_TAG = "KeyboardShortcutsTable";
+TABLE_PRESETS = LocalDb.TABLE_KEYBOARD_SHORTCUTS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: t("standard"),
+data: {
+mapping: {
+"mkb.toggle": {
+code: "F8"
+},
+"stream.screenshot.capture": {
+code: "Slash"
+}
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_KEYBOARD_SHORTCUTS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
}
class MkbMappingPresetsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable);
- LOG_TAG = "MkbMappingPresetsTable";
- TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: t("standard"),
- data: {
- mapping: {
- 16: ["Backquote"],
- 12: ["ArrowUp", "Digit1"],
- 13: ["ArrowDown", "Digit2"],
- 14: ["ArrowLeft", "Digit3"],
- 15: ["ArrowRight", "Digit4"],
- 100: ["KeyW"],
- 101: ["KeyS"],
- 102: ["KeyA"],
- 103: ["KeyD"],
- 200: ["KeyU"],
- 201: ["KeyJ"],
- 202: ["KeyH"],
- 203: ["KeyK"],
- 0: ["Space", "KeyE"],
- 2: ["KeyR"],
- 1: ["KeyC", "Backspace"],
- 3: ["KeyV"],
- 9: ["Enter"],
- 8: ["Tab"],
- 4: ["KeyQ"],
- 5: ["KeyF"],
- 7: ["Mouse0"],
- 6: ["Mouse2"],
- 10: ["KeyX"],
- 11: ["KeyZ"]
- },
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- }
- },
- [-2]: {
- id: -2,
- name: "Shooter",
- data: {
- mapping: {
- 16: ["Backquote"],
- 12: ["ArrowUp"],
- 13: ["ArrowDown"],
- 14: ["ArrowLeft"],
- 15: ["ArrowRight"],
- 100: ["KeyW"],
- 101: ["KeyS"],
- 102: ["KeyA"],
- 103: ["KeyD"],
- 200: ["KeyI"],
- 201: ["KeyK"],
- 202: ["KeyJ"],
- 203: ["KeyL"],
- 0: ["Space", "KeyE"],
- 2: ["KeyR"],
- 1: ["ControlLeft", "Backspace"],
- 3: ["KeyV"],
- 9: ["Enter"],
- 8: ["Tab"],
- 4: ["KeyC", "KeyG"],
- 5: ["KeyQ"],
- 7: ["Mouse0"],
- 6: ["Mouse2"],
- 10: ["ShiftLeft"],
- 11: ["KeyF"]
- },
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {},
- mouse: {
- mapTo: 2,
- sensitivityX: 100,
- sensitivityY: 100,
- deadzoneCounterweight: 20
- }
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
+static instance;
+static getInstance = () => MkbMappingPresetsTable.instance ?? (MkbMappingPresetsTable.instance = new MkbMappingPresetsTable);
+LOG_TAG = "MkbMappingPresetsTable";
+TABLE_PRESETS = LocalDb.TABLE_VIRTUAL_CONTROLLERS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: t("standard"),
+data: {
+mapping: {
+16: ["Backquote"],
+12: ["ArrowUp", "Digit1"],
+13: ["ArrowDown", "Digit2"],
+14: ["ArrowLeft", "Digit3"],
+15: ["ArrowRight", "Digit4"],
+100: ["KeyW"],
+101: ["KeyS"],
+102: ["KeyA"],
+103: ["KeyD"],
+200: ["KeyU"],
+201: ["KeyJ"],
+202: ["KeyH"],
+203: ["KeyK"],
+0: ["Space", "KeyE"],
+2: ["KeyR"],
+1: ["KeyC", "Backspace"],
+3: ["KeyV"],
+9: ["Enter"],
+8: ["Tab"],
+4: ["KeyQ"],
+5: ["KeyF"],
+7: ["Mouse0"],
+6: ["Mouse2"],
+10: ["KeyX"],
+11: ["KeyZ"]
+},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+}
+},
+[-2]: {
+id: -2,
+name: "Shooter",
+data: {
+mapping: {
+16: ["Backquote"],
+12: ["ArrowUp"],
+13: ["ArrowDown"],
+14: ["ArrowLeft"],
+15: ["ArrowRight"],
+100: ["KeyW"],
+101: ["KeyS"],
+102: ["KeyA"],
+103: ["KeyD"],
+200: ["KeyI"],
+201: ["KeyK"],
+202: ["KeyJ"],
+203: ["KeyL"],
+0: ["Space", "KeyE"],
+2: ["KeyR"],
+1: ["ControlLeft", "Backspace"],
+3: ["KeyV"],
+9: ["Enter"],
+8: ["Tab"],
+4: ["KeyC", "KeyG"],
+5: ["KeyQ"],
+7: ["Mouse0"],
+6: ["Mouse2"],
+10: ["ShiftLeft"],
+11: ["KeyF"]
+},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {},
+mouse: {
+mapTo: 2,
+sensitivityX: 100,
+sensitivityY: 100,
+deadzoneCounterweight: 20
+}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_VIRTUAL_CONTROLLERS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
}
class GameSettingsStorage extends BaseSettingsStorage {
- constructor(id) {
- super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS);
- }
- deleteSetting(pref) {
- if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0;
- return !1;
- }
+constructor(id) {
+super(`${"BetterXcloud.Stream"}.${id}`, StreamSettingsStorage.DEFINITIONS);
+}
+deleteSetting(pref) {
+if (this.hasSetting(pref)) return delete this.settings[pref], this.saveSettings(), !0;
+return !1;
+}
}
class ControllerCustomizationsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));
- TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: "ABXY ⇄ BAYX",
- data: {
- mapping: {
- 0: 1,
- 1: 0,
- 2: 3,
- 3: 2
- },
- settings: {
- leftStickDeadzone: [0, 100],
- rightStickDeadzone: [0, 100],
- leftTriggerRange: [0, 100],
- rightTriggerRange: [0, 100],
- vibrationIntensity: 100
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {},
- settings: {
- leftTriggerRange: [0, 100],
- rightTriggerRange: [0, 100],
- leftStickDeadzone: [0, 100],
- rightStickDeadzone: [0, 100],
- vibrationIntensity: 100
- }
- };
- DEFAULT_PRESET_ID = 0;
+static instance;
+static getInstance = () => ControllerCustomizationsTable.instance ?? (ControllerCustomizationsTable.instance = new ControllerCustomizationsTable(LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS));
+TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_CUSTOMIZATIONS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: "ABXY ⇄ BAYX",
+data: {
+mapping: {
+0: 1,
+1: 0,
+2: 3,
+3: 2
+},
+settings: {
+leftStickDeadzone: [0, 100],
+rightStickDeadzone: [0, 100],
+leftTriggerRange: [0, 100],
+rightTriggerRange: [0, 100],
+vibrationIntensity: 100
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {},
+settings: {
+leftTriggerRange: [0, 100],
+rightTriggerRange: [0, 100],
+leftStickDeadzone: [0, 100],
+rightStickDeadzone: [0, 100],
+vibrationIntensity: 100
+}
+};
+DEFAULT_PRESET_ID = 0;
}
class ControllerShortcutsTable extends BasePresetsTable {
- static instance;
- static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);
- LOG_TAG = "ControllerShortcutsTable";
- TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;
- DEFAULT_PRESETS = {
- [-1]: {
- id: -1,
- name: "Type A",
- data: {
- mapping: {
- 3: AppInterface ? "device.volume.inc" : "stream.volume.inc",
- 0: AppInterface ? "device.volume.dec" : "stream.volume.dec",
- 2: "stream.stats.toggle",
- 1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
- 5: "stream.screenshot.capture",
- 9: "stream.menu.show"
- }
- }
- },
- [-2]: {
- id: -2,
- name: "Type B",
- data: {
- mapping: {
- 12: AppInterface ? "device.volume.inc" : "stream.volume.inc",
- 13: AppInterface ? "device.volume.dec" : "stream.volume.dec",
- 15: "stream.stats.toggle",
- 14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
- 4: "stream.screenshot.capture",
- 8: "stream.menu.show"
- }
- }
- }
- };
- BLANK_PRESET_DATA = {
- mapping: {}
- };
- DEFAULT_PRESET_ID = -1;
- constructor() {
- super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
+static instance;
+static getInstance = () => ControllerShortcutsTable.instance ?? (ControllerShortcutsTable.instance = new ControllerShortcutsTable);
+LOG_TAG = "ControllerShortcutsTable";
+TABLE_PRESETS = LocalDb.TABLE_CONTROLLER_SHORTCUTS;
+DEFAULT_PRESETS = {
+[-1]: {
+id: -1,
+name: "Type A",
+data: {
+mapping: {
+3: AppInterface ? "device.volume.inc" : "stream.volume.inc",
+0: AppInterface ? "device.volume.dec" : "stream.volume.dec",
+2: "stream.stats.toggle",
+1: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
+5: "stream.screenshot.capture",
+9: "stream.menu.show"
+}
+}
+},
+[-2]: {
+id: -2,
+name: "Type B",
+data: {
+mapping: {
+12: AppInterface ? "device.volume.inc" : "stream.volume.inc",
+13: AppInterface ? "device.volume.dec" : "stream.volume.dec",
+15: "stream.stats.toggle",
+14: AppInterface ? "device.sound.toggle" : "stream.sound.toggle",
+4: "stream.screenshot.capture",
+8: "stream.menu.show"
+}
+}
+}
+};
+BLANK_PRESET_DATA = {
+mapping: {}
+};
+DEFAULT_PRESET_ID = -1;
+constructor() {
+super(LocalDb.TABLE_CONTROLLER_SHORTCUTS);
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
}
class StreamSettingsStorage extends BaseSettingsStorage {
- static DEFINITIONS = {
- "deviceVibration.mode": {
- requiredVariants: "full",
- label: t("device-vibration"),
- default: "off",
- options: {
- off: t("off"),
- on: t("on"),
- auto: t("device-vibration-not-using-gamepad")
- }
- },
- "deviceVibration.intensity": {
- requiredVariants: "full",
- label: t("vibration-intensity"),
- default: 50,
- min: 10,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- exactTicks: 20
- }
- },
- "controller.pollingRate": {
- requiredVariants: "full",
- label: t("polling-rate"),
- default: 4,
- min: 4,
- max: 60,
- params: {
- steps: 4,
- exactTicks: 20,
- reverse: !0,
- customTextValue(value) {
- value = parseInt(value);
- let text = +(1000 / value).toFixed(2) + " Hz";
- if (value === 4) text = `${text} (${t("default")})`;
- return text;
- }
- }
- },
- "controller.settings": {
- default: {}
- },
- "nativeMkb.scroll.sensitivityX": {
- requiredVariants: "full",
- label: t("horizontal-scroll-sensitivity"),
- default: 0,
- min: 0,
- max: 1e4,
- params: {
- steps: 10,
- exactTicks: 2000,
- customTextValue: (value) => {
- if (!value) return t("default");
- return (value / 100).toFixed(1) + "x";
- }
- }
- },
- "nativeMkb.scroll.sensitivityY": {
- requiredVariants: "full",
- label: t("vertical-scroll-sensitivity"),
- default: 0,
- min: 0,
- max: 1e4,
- params: {
- steps: 10,
- exactTicks: 2000,
- customTextValue: (value) => {
- if (!value) return t("default");
- return (value / 100).toFixed(1) + "x";
- }
- }
- },
- "mkb.p1.preset.mappingId": {
- requiredVariants: "full",
- default: -1
- },
- "mkb.p1.slot": {
- requiredVariants: "full",
- default: 1,
- min: 1,
- max: 4,
- params: {
- hideSlider: !0
- }
- },
- "mkb.p2.preset.mappingId": {
- requiredVariants: "full",
- default: 0
- },
- "mkb.p2.slot": {
- requiredVariants: "full",
- default: 0,
- min: 0,
- max: 4,
- params: {
- hideSlider: !0,
- customTextValue(value) {
- return value = parseInt(value), value === 0 ? t("off") : value.toString();
- }
- }
- },
- "keyboardShortcuts.preset.inGameId": {
- requiredVariants: "full",
- default: -1
- },
- "video.player.type": {
- label: t("renderer"),
- default: "default",
- options: {
- default: t("default"),
- webgl2: t("webgl2")
- },
- suggest: {
- lowest: "default",
- highest: "webgl2"
- }
- },
- "video.processing": {
- label: t("clarity-boost"),
- default: "usm",
- options: {
- usm: t("unsharp-masking"),
- cas: t("amd-fidelity-cas")
- },
- suggest: {
- lowest: "usm",
- highest: "cas"
- }
- },
- "video.player.powerPreference": {
- label: t("renderer-configuration"),
- default: "default",
- options: {
- default: t("default"),
- "low-power": t("battery-saving"),
- "high-performance": t("high-performance")
- },
- suggest: {
- highest: "low-power"
- }
- },
- "video.maxFps": {
- label: t("limit-fps"),
- default: 60,
- min: 10,
- max: 60,
- params: {
- steps: 10,
- exactTicks: 10,
- customTextValue: (value) => {
- return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";
- }
- }
- },
- "video.processing.sharpness": {
- label: t("sharpness"),
- default: 0,
- min: 0,
- max: 10,
- params: {
- exactTicks: 2,
- customTextValue: (value) => {
- return value = parseInt(value), value === 0 ? t("off") : value.toString();
- }
- },
- suggest: {
- lowest: 0,
- highest: 2
- }
- },
- "video.ratio": {
- label: t("aspect-ratio"),
- note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
- default: "16:9",
- options: {
- "16:9": `16:9 (${t("default")})`,
- "18:9": "18:9",
- "21:9": "21:9",
- "16:10": "16:10",
- "4:3": "4:3",
- fill: t("stretch")
- }
- },
- "video.position": {
- label: t("position"),
- note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
- default: "center",
- options: {
- top: t("top"),
- "top-half": t("top-half"),
- center: `${t("center")} (${t("default")})`,
- "bottom-half": t("bottom-half"),
- bottom: t("bottom")
- }
- },
- "video.saturation": {
- label: t("saturation"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "video.contrast": {
- label: t("contrast"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "video.brightness": {
- label: t("brightness"),
- default: 100,
- min: 50,
- max: 150,
- params: {
- suffix: "%",
- ticks: 25
- }
- },
- "audio.volume": {
- label: t("volume"),
- default: 100,
- min: 0,
- max: 600,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 100
- }
- },
- "stats.items": {
- label: t("stats"),
- default: ["ping", "fps", "btr", "dt", "pl", "fl"],
- multipleOptions: {
- time: t("clock"),
- play: t("playtime"),
- batt: t("battery"),
- ping: t("stat-ping"),
- jit: t("jitter"),
- fps: t("stat-fps"),
- btr: t("stat-bitrate"),
- dt: t("stat-decode-time"),
- pl: t("stat-packets-lost"),
- fl: t("stat-frames-lost"),
- dl: t("downloaded"),
- ul: t("uploaded")
- },
- params: {
- size: 0
- },
- ready: (setting) => {
- let multipleOptions = setting.multipleOptions;
- if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];
- for (let key in multipleOptions)
- multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];
- }
- },
- "stats.showWhenPlaying": {
- label: t("show-stats-on-startup"),
- default: !1
- },
- "stats.quickGlance.enabled": {
- label: "👀 " + t("enable-quick-glance-mode"),
- default: !0
- },
- "stats.position": {
- label: t("position"),
- default: "top-right",
- options: {
- "top-left": t("top-left"),
- "top-center": t("top-center"),
- "top-right": t("top-right")
- }
- },
- "stats.textSize": {
- label: t("text-size"),
- default: "0.9rem",
- options: {
- "0.9rem": t("small"),
- "1.0rem": t("normal"),
- "1.1rem": t("large")
- }
- },
- "stats.opacity.all": {
- label: t("opacity"),
- default: 80,
- min: 50,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10
- }
- },
- "stats.opacity.background": {
- label: t("background-opacity"),
- default: 100,
- min: 0,
- max: 100,
- params: {
- steps: 10,
- suffix: "%",
- ticks: 10
- }
- },
- "stats.colors": {
- label: t("conditional-formatting"),
- default: !1
- },
- "localCoOp.enabled": {
- requiredVariants: "full",
- label: t("enable-local-co-op-support"),
- labelIcon: BxIcon.LOCAL_CO_OP,
- default: !1,
- note: () => CE("div", !1, CE("a", {
- href: "https://github.com/redphx/better-xcloud/discussions/275",
- target: "_blank"
- }, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))
- }
- };
- gameSettings = {};
- xboxTitleId = -1;
- constructor() {
- super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);
- }
- setGameId(id) {
- this.xboxTitleId = id;
- }
- getGameSettings(id) {
- if (id > -1) {
- if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id);
- return this.gameSettings[id];
- }
- return null;
- }
- getSetting(key, checkUnsupported) {
- return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported);
- }
- getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) {
- let gameSettings = this.getGameSettings(id);
- if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported);
- if (returnBaseValue) return super.getSetting(key, checkUnsupported);
- return;
- }
- setSetting(key, value, origin) {
- return this.setSettingByGame(this.xboxTitleId, key, value, origin);
- }
- setSettingByGame(id, key, value, origin) {
- let gameSettings = this.getGameSettings(id);
- if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);
- return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);
- }
- hasGameSetting(id, key) {
- let gameSettings = this.getGameSettings(id);
- return !!(gameSettings && gameSettings.hasSetting(key));
- }
- getControllerSetting(gamepadId) {
- let controllerSetting = this.getSetting("controller.settings")[gamepadId];
- if (!controllerSetting) controllerSetting = {};
- if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;
- if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;
- return controllerSetting;
- }
+static DEFINITIONS = {
+"deviceVibration.mode": {
+requiredVariants: "full",
+label: t("device-vibration"),
+default: "off",
+options: {
+off: t("off"),
+on: t("on"),
+auto: t("device-vibration-not-using-gamepad")
+}
+},
+"deviceVibration.intensity": {
+requiredVariants: "full",
+label: t("vibration-intensity"),
+default: 50,
+min: 10,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+exactTicks: 20
+}
+},
+"controller.pollingRate": {
+requiredVariants: "full",
+label: t("polling-rate"),
+default: 4,
+min: 4,
+max: 60,
+params: {
+steps: 4,
+exactTicks: 20,
+reverse: !0,
+customTextValue(value) {
+value = parseInt(value);
+let text = +(1000 / value).toFixed(2) + " Hz";
+if (value === 4) text = `${text} (${t("default")})`;
+return text;
+}
+}
+},
+"controller.settings": {
+default: {}
+},
+"nativeMkb.scroll.sensitivityX": {
+requiredVariants: "full",
+label: t("horizontal-scroll-sensitivity"),
+default: 0,
+min: 0,
+max: 1e4,
+params: {
+steps: 10,
+exactTicks: 2000,
+customTextValue: (value) => {
+if (!value) return t("default");
+return (value / 100).toFixed(1) + "x";
+}
+}
+},
+"nativeMkb.scroll.sensitivityY": {
+requiredVariants: "full",
+label: t("vertical-scroll-sensitivity"),
+default: 0,
+min: 0,
+max: 1e4,
+params: {
+steps: 10,
+exactTicks: 2000,
+customTextValue: (value) => {
+if (!value) return t("default");
+return (value / 100).toFixed(1) + "x";
+}
+}
+},
+"mkb.p1.preset.mappingId": {
+requiredVariants: "full",
+default: -1
+},
+"mkb.p1.slot": {
+requiredVariants: "full",
+default: 1,
+min: 1,
+max: 4,
+params: {
+hideSlider: !0
+}
+},
+"mkb.p2.preset.mappingId": {
+requiredVariants: "full",
+default: 0
+},
+"mkb.p2.slot": {
+requiredVariants: "full",
+default: 0,
+min: 0,
+max: 4,
+params: {
+hideSlider: !0,
+customTextValue(value) {
+return value = parseInt(value), value === 0 ? t("off") : value.toString();
+}
+}
+},
+"keyboardShortcuts.preset.inGameId": {
+requiredVariants: "full",
+default: -1
+},
+"video.player.type": {
+label: t("renderer"),
+default: "default",
+options: {
+default: t("default"),
+webgl2: t("webgl2")
+},
+suggest: {
+lowest: "default",
+highest: "webgl2"
+}
+},
+"video.processing": {
+label: t("clarity-boost"),
+default: "usm",
+options: {
+usm: t("unsharp-masking"),
+cas: t("amd-fidelity-cas")
+},
+suggest: {
+lowest: "usm",
+highest: "cas"
+}
+},
+"video.player.powerPreference": {
+label: t("renderer-configuration"),
+default: "default",
+options: {
+default: t("default"),
+"low-power": t("battery-saving"),
+"high-performance": t("high-performance")
+},
+suggest: {
+highest: "low-power"
+}
+},
+"video.maxFps": {
+label: t("limit-fps"),
+default: 60,
+min: 10,
+max: 60,
+params: {
+steps: 10,
+exactTicks: 10,
+customTextValue: (value) => {
+return value = parseInt(value), value === 60 ? t("unlimited") : value + "fps";
+}
+}
+},
+"video.processing.sharpness": {
+label: t("sharpness"),
+default: 0,
+min: 0,
+max: 10,
+params: {
+exactTicks: 2,
+customTextValue: (value) => {
+return value = parseInt(value), value === 0 ? t("off") : value.toString();
+}
+},
+suggest: {
+lowest: 0,
+highest: 2
+}
+},
+"video.ratio": {
+label: t("aspect-ratio"),
+note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
+default: "16:9",
+options: {
+"16:9": `16:9 (${t("default")})`,
+"18:9": "18:9",
+"21:9": "21:9",
+"16:10": "16:10",
+"4:3": "4:3",
+fill: t("stretch")
+}
+},
+"video.position": {
+label: t("position"),
+note: STATES.browser.capabilities.touch ? t("aspect-ratio-note") : void 0,
+default: "center",
+options: {
+top: t("top"),
+"top-half": t("top-half"),
+center: `${t("center")} (${t("default")})`,
+"bottom-half": t("bottom-half"),
+bottom: t("bottom")
+}
+},
+"video.saturation": {
+label: t("saturation"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"video.contrast": {
+label: t("contrast"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"video.brightness": {
+label: t("brightness"),
+default: 100,
+min: 50,
+max: 150,
+params: {
+suffix: "%",
+ticks: 25
+}
+},
+"audio.volume": {
+label: t("volume"),
+default: 100,
+min: 0,
+max: 600,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 100
+}
+},
+"stats.items": {
+label: t("stats"),
+default: ["ping", "fps", "btr", "dt", "pl", "fl"],
+multipleOptions: {
+time: t("clock"),
+play: t("playtime"),
+batt: t("battery"),
+ping: t("stat-ping"),
+jit: t("jitter"),
+fps: t("stat-fps"),
+btr: t("stat-bitrate"),
+dt: t("stat-decode-time"),
+pl: t("stat-packets-lost"),
+fl: t("stat-frames-lost"),
+dl: t("downloaded"),
+ul: t("uploaded")
+},
+params: {
+size: 0
+},
+ready: (setting) => {
+let multipleOptions = setting.multipleOptions;
+if (!STATES.browser.capabilities.batteryApi) delete multipleOptions["batt"];
+for (let key in multipleOptions)
+multipleOptions[key] = key.toUpperCase() + ": " + multipleOptions[key];
+}
+},
+"stats.showWhenPlaying": {
+label: t("show-stats-on-startup"),
+default: !1
+},
+"stats.quickGlance.enabled": {
+label: "👀 " + t("enable-quick-glance-mode"),
+default: !0
+},
+"stats.position": {
+label: t("position"),
+default: "top-right",
+options: {
+"top-left": t("top-left"),
+"top-center": t("top-center"),
+"top-right": t("top-right")
+}
+},
+"stats.textSize": {
+label: t("text-size"),
+default: "0.9rem",
+options: {
+"0.9rem": t("small"),
+"1.0rem": t("normal"),
+"1.1rem": t("large")
+}
+},
+"stats.opacity.all": {
+label: t("opacity"),
+default: 80,
+min: 50,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10
+}
+},
+"stats.opacity.background": {
+label: t("background-opacity"),
+default: 100,
+min: 0,
+max: 100,
+params: {
+steps: 10,
+suffix: "%",
+ticks: 10
+}
+},
+"stats.colors": {
+label: t("conditional-formatting"),
+default: !1
+},
+"localCoOp.enabled": {
+requiredVariants: "full",
+label: t("enable-local-co-op-support"),
+labelIcon: BxIcon.LOCAL_CO_OP,
+default: !1,
+note: () => CE("div", !1, CE("a", {
+href: "https://github.com/redphx/better-xcloud/discussions/275",
+target: "_blank"
+}, t("enable-local-co-op-support-note")), CE("br"), "⚠️ " + t("unexpected-behavior"))
+}
+};
+gameSettings = {};
+xboxTitleId = -1;
+constructor() {
+super("BetterXcloud.Stream", StreamSettingsStorage.DEFINITIONS);
+}
+setGameId(id) {
+this.xboxTitleId = id;
+}
+getGameSettings(id) {
+if (id > -1) {
+if (!this.gameSettings[id]) this.gameSettings[id] = new GameSettingsStorage(id);
+return this.gameSettings[id];
+}
+return null;
+}
+getSetting(key, checkUnsupported) {
+return this.getSettingByGame(this.xboxTitleId, key, !0, checkUnsupported);
+}
+getSettingByGame(id, key, returnBaseValue = !0, checkUnsupported) {
+let gameSettings = this.getGameSettings(id);
+if (gameSettings?.hasSetting(key)) return gameSettings.getSetting(key, checkUnsupported);
+if (returnBaseValue) return super.getSetting(key, checkUnsupported);
+return;
+}
+setSetting(key, value, origin) {
+return this.setSettingByGame(this.xboxTitleId, key, value, origin);
+}
+setSettingByGame(id, key, value, origin) {
+let gameSettings = this.getGameSettings(id);
+if (gameSettings) return BxLogger.info("setSettingByGame", id, key, value), gameSettings.setSetting(key, value, origin);
+return BxLogger.info("setSettingByGame", id, key, value), super.setSetting(key, value, origin);
+}
+hasGameSetting(id, key) {
+let gameSettings = this.getGameSettings(id);
+return !!(gameSettings && gameSettings.hasSetting(key));
+}
+getControllerSetting(gamepadId) {
+let controllerSetting = this.getSetting("controller.settings")[gamepadId];
+if (!controllerSetting) controllerSetting = {};
+if (!controllerSetting.hasOwnProperty("shortcutPresetId")) controllerSetting.shortcutPresetId = -1;
+if (!controllerSetting.hasOwnProperty("customizationPresetId")) controllerSetting.customizationPresetId = 0;
+return controllerSetting;
+}
}
var STORAGE = {
- Global: new GlobalSettingsStorage,
- Stream: new StreamSettingsStorage
+Global: new GlobalSettingsStorage,
+Stream: new StreamSettingsStorage
}, streamSettingsStorage = STORAGE.Stream, getStreamPrefDefinition = streamSettingsStorage.getDefinition.bind(streamSettingsStorage), getStreamPref = streamSettingsStorage.getSetting.bind(streamSettingsStorage), setStreamPref = streamSettingsStorage.setSetting.bind(streamSettingsStorage), getGamePref = streamSettingsStorage.getSettingByGame.bind(streamSettingsStorage), setGamePref = streamSettingsStorage.setSettingByGame.bind(streamSettingsStorage), setGameIdPref = streamSettingsStorage.setGameId.bind(streamSettingsStorage), hasGamePref = streamSettingsStorage.hasGameSetting.bind(streamSettingsStorage);
STORAGE.Stream = streamSettingsStorage;
var globalSettingsStorage = STORAGE.Global, getGlobalPrefDefinition = globalSettingsStorage.getDefinition.bind(globalSettingsStorage), getGlobalPref = globalSettingsStorage.getSetting.bind(globalSettingsStorage), setGlobalPref = globalSettingsStorage.setSetting.bind(globalSettingsStorage);
function isGlobalPref(prefKey) {
- return ALL_PREFS.global.includes(prefKey);
+return ALL_PREFS.global.includes(prefKey);
}
function isStreamPref(prefKey) {
- return ALL_PREFS.stream.includes(prefKey);
+return ALL_PREFS.stream.includes(prefKey);
}
function getPrefInfo(prefKey) {
- if (isGlobalPref(prefKey)) return {
- storage: STORAGE.Global,
- definition: getGlobalPrefDefinition(prefKey)
- };
- else if (isStreamPref(prefKey)) return {
- storage: STORAGE.Stream,
- definition: getStreamPrefDefinition(prefKey)
- };
- return alert("Missing pref definition: " + prefKey), {};
+if (isGlobalPref(prefKey)) return {
+storage: STORAGE.Global,
+definition: getGlobalPrefDefinition(prefKey)
+};
+else if (isStreamPref(prefKey)) return {
+storage: STORAGE.Stream,
+definition: getStreamPrefDefinition(prefKey)
+};
+return alert("Missing pref definition: " + prefKey), {};
}
function setPref(prefKey, value, origin) {
- if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin);
- else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin);
+if (isGlobalPref(prefKey)) setGlobalPref(prefKey, value, origin);
+else if (isStreamPref(prefKey)) setStreamPref(prefKey, value, origin);
}
function checkForUpdate() {
- if (SCRIPT_VERSION.includes("beta")) return;
- let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getGlobalPref("version.current"), lastCheck = getGlobalPref("version.lastCheck"), now = Math.round(+new Date / 1000);
- if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return;
- setGlobalPref("version.lastCheck", now, "direct"), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => {
- setGlobalPref("version.latest", json.tag_name.substring(1), "direct"), setGlobalPref("version.current", SCRIPT_VERSION, "direct");
- }), Translations.updateTranslations(currentVersion === SCRIPT_VERSION);
+if (SCRIPT_VERSION.includes("beta")) return;
+let CHECK_INTERVAL_SECONDS = 7200, currentVersion = getGlobalPref("version.current"), lastCheck = getGlobalPref("version.lastCheck"), now = Math.round(+new Date / 1000);
+if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) return;
+setGlobalPref("version.lastCheck", now, "direct"), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => {
+setGlobalPref("version.latest", json.tag_name.substring(1), "direct"), setGlobalPref("version.current", SCRIPT_VERSION, "direct");
+}), Translations.updateTranslations(currentVersion === SCRIPT_VERSION);
}
function disablePwa() {
- if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return;
- if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", {
- value: !0
- });
+if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase()) return;
+if (!!AppInterface || UserAgent.isSafariMobile()) Object.defineProperty(window.navigator, "standalone", {
+value: !0
+});
}
function hashCode(str) {
- let hash = 0;
- for (let i = 0, len = str.length;i < len; i++) {
- let chr = str.charCodeAt(i);
- hash = (hash << 5) - hash + chr, hash |= 0;
- }
- return hash;
+let hash = 0;
+for (let i = 0, len = str.length;i < len; i++) {
+let chr = str.charCodeAt(i);
+hash = (hash << 5) - hash + chr, hash |= 0;
+}
+return hash;
}
function renderString(str, obj) {
- return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => {
- let name = p1 || p2;
- return name in obj ? obj[name] : match;
- });
+return str.replace(/\$\{([A-Za-z0-9_$]+)\}|\$([A-Za-z0-9_$]+)\$/g, (match, p1, p2) => {
+let name = p1 || p2;
+return name in obj ? obj[name] : match;
+});
}
function ceilToNearest(value, interval) {
- return Math.ceil(value / interval) * interval;
+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();
}
function parseDetailsPath(path) {
- let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path);
- if (!matches?.groups) return {};
- let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId;
- return { titleSlug, productId };
+let matches = /\/games\/(?[^\/]+)\/(?\w+)/.exec(path);
+if (!matches?.groups) return {};
+let titleSlug = matches.groups.titleSlug.replaceAll("|", "-"), productId = matches.groups.productId;
+return { titleSlug, productId };
}
function clearAllData() {
- for (let i = 0;i < localStorage.length; i++) {
- let key = localStorage.key(i);
- if (!key) continue;
- if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key);
- }
- try {
- indexedDB.deleteDatabase(LocalDb.DB_NAME);
- } catch (e) {}
- alert(t("clear-data-success"));
+for (let i = 0;i < localStorage.length; i++) {
+let key = localStorage.key(i);
+if (!key) continue;
+if (key.startsWith("BetterXcloud") || key.startsWith("better_xcloud")) localStorage.removeItem(key);
+}
+try {
+indexedDB.deleteDatabase(LocalDb.DB_NAME);
+} catch (e) {}
+alert(t("clear-data-success"));
}
function blockAllNotifications() {
- let blockFeatures = getGlobalPref("block.features");
- return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value));
+let blockFeatures = getGlobalPref("block.features");
+return ["friends", "notifications-achievements", "notifications-invites"].every((value) => blockFeatures.includes(value));
}
function blockSomeNotifications() {
- let blockFeatures = getGlobalPref("block.features");
- if (blockAllNotifications()) return !1;
- return ["friends", "notifications-achievements", "notifications-invites"].some((value) => blockFeatures.includes(value));
+let blockFeatures = getGlobalPref("block.features");
+if (blockAllNotifications()) return !1;
+return ["friends", "notifications-achievements", "notifications-invites"].some((value) => blockFeatures.includes(value));
}
function isPlainObject(input) {
- return typeof input === "object" && input !== null && input.constructor === Object;
+return typeof input === "object" && input !== null && input.constructor === Object;
}
class SoundShortcut {
- static adjustGainNodeVolume(amount) {
- if (!getGlobalPref("audio.volume.booster.enabled")) return 0;
- let currentValue = getStreamPref("audio.volume"), nearestValue;
- if (amount > 0) nearestValue = ceilToNearest(currentValue, amount);
- else nearestValue = floorToNearest(currentValue, -1 * amount);
- let newValue;
- if (currentValue !== nearestValue) newValue = nearestValue;
- else newValue = currentValue + amount;
- return newValue = setStreamPref("audio.volume", newValue, "direct"), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue;
- }
- static setGainNodeVolume(value) {
- STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100);
- }
- static muteUnmute() {
- if (getGlobalPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) {
- let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getStreamPref("audio.volume"), targetValue;
- if (settingValue === 0) targetValue = 100, setStreamPref("audio.volume", targetValue, "direct");
- else if (gainValue === 0) targetValue = settingValue;
- else targetValue = 0;
- let status;
- if (targetValue === 0) status = t("muted");
- else status = targetValue + "%";
- SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
- state: targetValue === 0 ? 1 : 0
- });
- return;
- }
- let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");
- if ($media) {
- $media.muted = !$media.muted;
- let status = $media.muted ? t("muted") : t("unmuted");
- Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
- state: $media.muted ? 1 : 0
- });
- }
- }
+static adjustGainNodeVolume(amount) {
+if (!getGlobalPref("audio.volume.booster.enabled")) return 0;
+let currentValue = getStreamPref("audio.volume"), nearestValue;
+if (amount > 0) nearestValue = ceilToNearest(currentValue, amount);
+else nearestValue = floorToNearest(currentValue, -1 * amount);
+let newValue;
+if (currentValue !== nearestValue) newValue = nearestValue;
+else newValue = currentValue + amount;
+return newValue = setStreamPref("audio.volume", newValue, "direct"), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), newValue;
+}
+static setGainNodeVolume(value) {
+STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100);
+}
+static muteUnmute() {
+if (getGlobalPref("audio.volume.booster.enabled") && STATES.currentStream.audioGainNode) {
+let gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getStreamPref("audio.volume"), targetValue;
+if (settingValue === 0) targetValue = 100, setStreamPref("audio.volume", targetValue, "direct");
+else if (gainValue === 0) targetValue = settingValue;
+else targetValue = 0;
+let status;
+if (targetValue === 0) status = t("muted");
+else status = targetValue + "%";
+SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
+state: targetValue === 0 ? 1 : 0
+});
+return;
+}
+let $media = document.querySelector("div[data-testid=media-container] audio") ?? document.querySelector("div[data-testid=media-container] video");
+if ($media) {
+$media.muted = !$media.muted;
+let status = $media.muted ? t("muted") : t("unmuted");
+Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 }), BxEventBus.Stream.emit("speaker.state.changed", {
+state: $media.muted ? 1 : 0
+});
+}
+}
}
class StreamUiShortcut {
- static showHideStreamMenu() {
- window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
- }
+static showHideStreamMenu() {
+window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
+}
}
class StreamStatsCollector {
- static instance;
- static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
- LOG_TAG = "StreamStatsCollector";
- static INTERVAL_BACKGROUND = 60000;
- calculateGrade(value, grades) {
- return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
- }
- currentStats = {
- ping: {
- current: -1,
- grades: [40, 75, 100],
- toString() {
- return this.current === -1 ? "???" : this.current.toString().padStart(3);
- }
- },
- jit: {
- current: 0,
- grades: [30, 40, 60],
- toString() {
- return `${this.current.toFixed(1)}ms`.padStart(6);
- }
- },
- fps: {
- current: 0,
- toString() {
- let maxFps = getStreamPref("video.maxFps");
- return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString();
- }
- },
- btr: {
- current: 0,
- toString() {
- return `${this.current.toFixed(1)} Mbps`.padStart(9);
- }
- },
- fl: {
- received: 0,
- dropped: 0,
- toString() {
- let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
- return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
- }
- },
- pl: {
- received: 0,
- dropped: 0,
- toString() {
- let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
- return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
- }
- },
- dt: {
- current: 0,
- total: 0,
- grades: [6, 9, 12],
- toString() {
- return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6);
- }
- },
- dl: {
- total: 0,
- toString() {
- return humanFileSize(this.total).padStart(8);
- }
- },
- ul: {
- total: 0,
- toString() {
- return humanFileSize(this.total);
- }
- },
- play: {
- seconds: 0,
- startTime: 0,
- toString() {
- return secondsToHm(this.seconds);
- }
- },
- batt: {
- current: 100,
- start: 100,
- isCharging: !1,
- toString() {
- let text = `${this.current}%`;
- if (this.current !== this.start) {
- let diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : "";
- text += ` (${sign}${diffLevel}%)`;
- }
- return text;
- }
- },
- time: {
- toString() {
- return (new Date()).toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- hour12: !1
- });
- }
- }
- };
- lastVideoStat;
- selectedCandidatePairId = null;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- async collect() {
- let stats = await STATES.currentStream.peerConnection?.getStats();
- if (!stats) return;
- if (!this.selectedCandidatePairId) {
- let found = !1;
- stats.forEach((stat) => {
- if (found || stat.type !== "transport") return;
- if (stat = stat, stat.iceState === "connected" && stat.selectedCandidatePairId) this.selectedCandidatePairId = stat.selectedCandidatePairId, found = !0;
- });
- }
- stats.forEach((stat) => {
- if (stat.type === "inbound-rtp" && stat.kind === "video") {
- let fps = this.currentStats["fps"];
- fps.current = stat.framesPerSecond || 0;
- let pl = this.currentStats["pl"];
- pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived;
- let fl = this.currentStats["fl"];
- if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) {
- this.lastVideoStat = stat;
- return;
- }
- let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount;
- if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000;
- let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp;
- btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
- let dt = this.currentStats["dt"];
- dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
- let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
- dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat;
- } else if (this.selectedCandidatePairId && stat.type === "candidate-pair" && stat.id === this.selectedCandidatePairId) {
- let ping = this.currentStats["ping"];
- ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
- let dl = this.currentStats["dl"];
- dl.total = stat.bytesReceived;
- let ul = this.currentStats["ul"];
- ul.total = stat.bytesSent;
- }
- });
- let batteryLevel = 100, isCharging = !1;
- if (STATES.browser.capabilities.batteryApi) try {
- let bm = await navigator.getBattery();
- isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100);
- } catch (e) {}
- let battery = this.currentStats["batt"];
- battery.current = batteryLevel, battery.isCharging = isCharging;
- let playTime = this.currentStats["play"], now = +new Date;
- playTime.seconds = Math.ceil((now - playTime.startTime) / 1000);
- }
- getStat(kind) {
- return this.currentStats[kind];
- }
- reset() {
- let playTime = this.currentStats["play"];
- playTime.seconds = 0, playTime.startTime = +new Date;
- try {
- STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => {
- this.currentStats["batt"].start = Math.round(bm.level * 100);
- });
- } catch (e) {}
- }
- static setupEvents() {
- BxEventBus.Stream.on("state.playing", () => {
- StreamStatsCollector.getInstance().reset();
- });
- }
+static instance;
+static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
+LOG_TAG = "StreamStatsCollector";
+static INTERVAL_BACKGROUND = 60000;
+calculateGrade(value, grades) {
+return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
+}
+currentStats = {
+ping: {
+current: -1,
+grades: [40, 75, 100],
+toString() {
+return this.current === -1 ? "???" : this.current.toString().padStart(3);
+}
+},
+jit: {
+current: 0,
+grades: [30, 40, 60],
+toString() {
+return `${this.current.toFixed(1)}ms`.padStart(6);
+}
+},
+fps: {
+current: 0,
+toString() {
+let maxFps = getStreamPref("video.maxFps");
+return maxFps < 60 ? `${maxFps}/${this.current}`.padStart(5) : this.current.toString();
+}
+},
+btr: {
+current: 0,
+toString() {
+return `${this.current.toFixed(1)} Mbps`.padStart(9);
+}
+},
+fl: {
+received: 0,
+dropped: 0,
+toString() {
+let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
+return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
+}
+},
+pl: {
+received: 0,
+dropped: 0,
+toString() {
+let percentage = (this.dropped * 100 / (this.dropped + this.received || 1)).toFixed(1);
+return percentage.startsWith("0.") ? this.dropped.toString() : `${this.dropped} (${percentage}%)`;
+}
+},
+dt: {
+current: 0,
+total: 0,
+grades: [6, 9, 12],
+toString() {
+return isNaN(this.current) ? "??ms" : `${this.current.toFixed(1)}ms`.padStart(6);
+}
+},
+dl: {
+total: 0,
+toString() {
+return humanFileSize(this.total).padStart(8);
+}
+},
+ul: {
+total: 0,
+toString() {
+return humanFileSize(this.total);
+}
+},
+play: {
+seconds: 0,
+startTime: 0,
+toString() {
+return secondsToHm(this.seconds);
+}
+},
+batt: {
+current: 100,
+start: 100,
+isCharging: !1,
+toString() {
+let text = `${this.current}%`;
+if (this.current !== this.start) {
+let diffLevel = Math.round(this.current - this.start), sign = diffLevel > 0 ? "+" : "";
+text += ` (${sign}${diffLevel}%)`;
+}
+return text;
+}
+},
+time: {
+toString() {
+return (new Date()).toLocaleTimeString([], {
+hour: "2-digit",
+minute: "2-digit",
+hour12: !1
+});
+}
+}
+};
+lastVideoStat;
+selectedCandidatePairId = null;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+async collect() {
+let stats = await STATES.currentStream.peerConnection?.getStats();
+if (!stats) return;
+if (!this.selectedCandidatePairId) {
+let found = !1;
+stats.forEach((stat) => {
+if (found || stat.type !== "transport") return;
+if (stat = stat, stat.iceState === "connected" && stat.selectedCandidatePairId) this.selectedCandidatePairId = stat.selectedCandidatePairId, found = !0;
+});
+}
+stats.forEach((stat) => {
+if (stat.type === "inbound-rtp" && stat.kind === "video") {
+let fps = this.currentStats["fps"];
+fps.current = stat.framesPerSecond || 0;
+let pl = this.currentStats["pl"];
+pl.dropped = Math.max(0, stat.packetsLost), pl.received = stat.packetsReceived;
+let fl = this.currentStats["fl"];
+if (fl.dropped = stat.framesDropped, fl.received = stat.framesReceived, !this.lastVideoStat) {
+this.lastVideoStat = stat;
+return;
+}
+let lastStat = this.lastVideoStat, jit = this.currentStats["jit"], bufferDelayDiff = stat.jitterBufferDelay - lastStat.jitterBufferDelay, emittedCountDiff = stat.jitterBufferEmittedCount - lastStat.jitterBufferEmittedCount;
+if (emittedCountDiff > 0) jit.current = bufferDelayDiff / emittedCountDiff * 1000;
+let btr = this.currentStats["btr"], timeDiff = stat.timestamp - lastStat.timestamp;
+btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
+let dt = this.currentStats["dt"];
+dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime;
+let framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
+dt.current = dt.total / framesDecodedDiff * 1000, this.lastVideoStat = stat;
+} else if (this.selectedCandidatePairId && stat.type === "candidate-pair" && stat.id === this.selectedCandidatePairId) {
+let ping = this.currentStats["ping"];
+ping.current = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
+let dl = this.currentStats["dl"];
+dl.total = stat.bytesReceived;
+let ul = this.currentStats["ul"];
+ul.total = stat.bytesSent;
+}
+});
+let batteryLevel = 100, isCharging = !1;
+if (STATES.browser.capabilities.batteryApi) try {
+let bm = await navigator.getBattery();
+isCharging = bm.charging, batteryLevel = Math.round(bm.level * 100);
+} catch (e) {}
+let battery = this.currentStats["batt"];
+battery.current = batteryLevel, battery.isCharging = isCharging;
+let playTime = this.currentStats["play"], now = +new Date;
+playTime.seconds = Math.ceil((now - playTime.startTime) / 1000);
+}
+getStat(kind) {
+return this.currentStats[kind];
+}
+reset() {
+let playTime = this.currentStats["play"];
+playTime.seconds = 0, playTime.startTime = +new Date;
+try {
+STATES.browser.capabilities.batteryApi && navigator.getBattery().then((bm) => {
+this.currentStats["batt"].start = Math.round(bm.level * 100);
+});
+} catch (e) {}
+}
+static setupEvents() {
+BxEventBus.Stream.on("state.playing", () => {
+StreamStatsCollector.getInstance().reset();
+});
+}
}
class StreamStats {
- static instance;
- static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
- LOG_TAG = "StreamStats";
- intervalId;
- REFRESH_INTERVAL = 1000;
- stats = {
- time: {
- name: t("clock"),
- $element: CE("span")
- },
- play: {
- name: t("playtime"),
- $element: CE("span")
- },
- batt: {
- name: t("battery"),
- $element: CE("span")
- },
- ping: {
- name: t("stat-ping"),
- $element: CE("span")
- },
- jit: {
- name: t("jitter"),
- $element: CE("span")
- },
- fps: {
- name: t("stat-fps"),
- $element: CE("span")
- },
- btr: {
- name: t("stat-bitrate"),
- $element: CE("span")
- },
- dt: {
- name: t("stat-decode-time"),
- $element: CE("span")
- },
- pl: {
- name: t("stat-packets-lost"),
- $element: CE("span")
- },
- fl: {
- name: t("stat-frames-lost"),
- $element: CE("span")
- },
- dl: {
- name: t("downloaded"),
- $element: CE("span")
- },
- ul: {
- name: t("uploaded"),
- $element: CE("span")
- }
- };
- $container;
- quickGlanceObserver;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.render();
- }
- async start(glancing = !1) {
- if (!this.isHidden() || glancing && this.isGlancing()) return;
- this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
- }
- async stop(glancing = !1) {
- if (glancing && !this.isGlancing()) return;
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");
- }
- async toggle() {
- if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");
- else this.isHidden() ? await this.start() : await this.stop();
- }
- destroy() {
- this.stop(), this.quickGlanceStop(), this.hideSettingsUi();
- }
- isHidden = () => this.$container.classList.contains("bx-gone");
- isGlancing = () => this.$container.dataset.display === "glancing";
- quickGlanceSetup() {
- if (!STATES.isPlaying || this.quickGlanceObserver) return;
- let $uiContainer = document.querySelector("div[data-testid=ui-container]");
- if (!$uiContainer) return;
- this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {
- for (let record of mutationList) {
- let $target = record.target;
- if (!$target.className || !$target.className.startsWith("GripHandle")) continue;
- if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0);
- else this.stop(!0);
- }
- }), this.quickGlanceObserver.observe($uiContainer, {
- attributes: !0,
- attributeFilter: ["aria-expanded"],
- subtree: !0
- });
- }
- quickGlanceStop() {
- this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null;
- }
- update = async (forceUpdate = !1) => {
- if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {
- this.destroy();
- return;
- }
- let PREF_STATS_CONDITIONAL_FORMATTING = getStreamPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();
- await statsCollector.collect();
- let statKey;
- for (statKey in this.stats) {
- grade = "";
- let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;
- if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);
- if ($element.dataset.grade !== grade) $element.dataset.grade = grade;
- }
- };
- refreshStyles() {
- let PREF_ITEMS = getStreamPref("stats.items"), PREF_OPACITY_BG = getStreamPref("stats.opacity.background"), $container = this.$container;
- if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getStreamPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";
- else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
- $container.style.opacity = getStreamPref("stats.opacity.all") + "%", $container.style.fontSize = getStreamPref("stats.textSize");
- }
- hideSettingsUi() {
- if (this.isGlancing() && !getStreamPref("stats.quickGlance.enabled")) this.stop();
- }
- async render() {
- this.$container = CE("div", { class: "bx-stats-bar bx-gone" });
- let statKey;
- for (statKey in this.stats) {
- let stat = this.stats[statKey], $div = CE("div", {
- class: `bx-stat-${statKey}`,
- title: stat.name
- }, CE("label", !1, statKey.toUpperCase()), stat.$element);
- this.$container.appendChild($div);
- }
- this.refreshStyles(), document.documentElement.appendChild(this.$container);
- }
- static setupEvents() {
- BxEventBus.Stream.on("state.playing", () => {
- let PREF_STATS_QUICK_GLANCE = getStreamPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getStreamPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();
- if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();
- else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);
- });
- }
- static refreshStyles() {
- StreamStats.getInstance().refreshStyles();
- }
+static instance;
+static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
+LOG_TAG = "StreamStats";
+intervalId;
+REFRESH_INTERVAL = 1000;
+stats = {
+time: {
+name: t("clock"),
+$element: CE("span")
+},
+play: {
+name: t("playtime"),
+$element: CE("span")
+},
+batt: {
+name: t("battery"),
+$element: CE("span")
+},
+ping: {
+name: t("stat-ping"),
+$element: CE("span")
+},
+jit: {
+name: t("jitter"),
+$element: CE("span")
+},
+fps: {
+name: t("stat-fps"),
+$element: CE("span")
+},
+btr: {
+name: t("stat-bitrate"),
+$element: CE("span")
+},
+dt: {
+name: t("stat-decode-time"),
+$element: CE("span")
+},
+pl: {
+name: t("stat-packets-lost"),
+$element: CE("span")
+},
+fl: {
+name: t("stat-frames-lost"),
+$element: CE("span")
+},
+dl: {
+name: t("downloaded"),
+$element: CE("span")
+},
+ul: {
+name: t("uploaded"),
+$element: CE("span")
}
-function onChangeVideoPlayerType() {
- let playerType = getStreamPref("video.player.type"), $videoProcessing = document.getElementById(`bx_setting_${escapeCssSelector("video.processing")}`), $videoSharpness = document.getElementById(`bx_setting_${escapeCssSelector("video.processing.sharpness")}`), $videoPowerPreference = document.getElementById(`bx_setting_${escapeCssSelector("video.player.powerPreference")}`), $videoMaxFps = document.getElementById(`bx_setting_${escapeCssSelector("video.maxFps")}`);
- if (!$videoProcessing) return;
- let isDisabled = !1, $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);
- if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);
- else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
- if ($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"), STATES.isPlaying) updateVideoPlayer();
+};
+$container;
+quickGlanceObserver;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.render();
}
-function limitVideoPlayerFps(targetFps) {
- STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
+async start(glancing = !1) {
+if (!this.isHidden() || glancing && this.isGlancing()) return;
+this.intervalId && clearInterval(this.intervalId), await this.update(!0), this.$container.classList.remove("bx-gone"), this.$container.dataset.display = glancing ? "glancing" : "fixed", this.intervalId = window.setInterval(this.update, this.REFRESH_INTERVAL);
+}
+async stop(glancing = !1) {
+if (glancing && !this.isGlancing()) return;
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null, this.$container.removeAttribute("data-display"), this.$container.classList.add("bx-gone");
+}
+async toggle() {
+if (this.isGlancing()) this.$container && (this.$container.dataset.display = "fixed");
+else this.isHidden() ? await this.start() : await this.stop();
+}
+destroy() {
+this.stop(), this.quickGlanceStop(), this.hideSettingsUi();
+}
+isHidden = () => this.$container.classList.contains("bx-gone");
+isGlancing = () => this.$container.dataset.display === "glancing";
+quickGlanceSetup() {
+if (!STATES.isPlaying || this.quickGlanceObserver) return;
+let $uiContainer = document.querySelector("div[data-testid=ui-container]");
+if (!$uiContainer) return;
+this.quickGlanceObserver = new MutationObserver((mutationList, observer) => {
+for (let record of mutationList) {
+let $target = record.target;
+if (!$target.className || !$target.className.startsWith("GripHandle")) continue;
+if (record.target.ariaExpanded === "true") this.isHidden() && this.start(!0);
+else this.stop(!0);
+}
+}), this.quickGlanceObserver.observe($uiContainer, {
+attributes: !0,
+attributeFilter: ["aria-expanded"],
+subtree: !0
+});
+}
+quickGlanceStop() {
+this.quickGlanceObserver && this.quickGlanceObserver.disconnect(), this.quickGlanceObserver = null;
+}
+update = async (forceUpdate = !1) => {
+if (!forceUpdate && this.isHidden() || !STATES.currentStream.peerConnection) {
+this.destroy();
+return;
+}
+let PREF_STATS_CONDITIONAL_FORMATTING = getStreamPref("stats.colors"), grade = "", statsCollector = StreamStatsCollector.getInstance();
+await statsCollector.collect();
+let statKey;
+for (statKey in this.stats) {
+grade = "";
+let stat = this.stats[statKey], value = statsCollector.getStat(statKey), $element = stat.$element;
+if ($element.textContent = value.toString(), PREF_STATS_CONDITIONAL_FORMATTING && "grades" in value) grade = statsCollector.calculateGrade(value.current, value.grades);
+if ($element.dataset.grade !== grade) $element.dataset.grade = grade;
+}
+};
+refreshStyles() {
+let PREF_ITEMS = getStreamPref("stats.items"), PREF_OPACITY_BG = getStreamPref("stats.opacity.background"), $container = this.$container;
+if ($container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = getStreamPref("stats.position"), PREF_OPACITY_BG === 0) $container.style.removeProperty("background-color"), $container.dataset.shadow = "true";
+else delete $container.dataset.shadow, $container.style.backgroundColor = `rgba(0, 0, 0, ${PREF_OPACITY_BG}%)`;
+$container.style.opacity = getStreamPref("stats.opacity.all") + "%", $container.style.fontSize = getStreamPref("stats.textSize");
+}
+hideSettingsUi() {
+if (this.isGlancing() && !getStreamPref("stats.quickGlance.enabled")) this.stop();
+}
+async render() {
+this.$container = CE("div", { class: "bx-stats-bar bx-gone" });
+let statKey;
+for (statKey in this.stats) {
+let stat = this.stats[statKey], $div = CE("div", {
+class: `bx-stat-${statKey}`,
+title: stat.name
+}, CE("label", !1, statKey.toUpperCase()), stat.$element);
+this.$container.appendChild($div);
+}
+this.refreshStyles(), document.documentElement.appendChild(this.$container);
+}
+static setupEvents() {
+BxEventBus.Stream.on("state.playing", () => {
+let PREF_STATS_QUICK_GLANCE = getStreamPref("stats.quickGlance.enabled"), PREF_STATS_SHOW_WHEN_PLAYING = getStreamPref("stats.showWhenPlaying"), streamStats = StreamStats.getInstance();
+if (PREF_STATS_SHOW_WHEN_PLAYING) streamStats.start();
+else if (PREF_STATS_QUICK_GLANCE) streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);
+});
+}
+static refreshStyles() {
+StreamStats.getInstance().refreshStyles();
}
-function updateVideoPlayer() {
- let streamPlayer = STATES.currentStream.streamPlayer;
- if (!streamPlayer) return;
- limitVideoPlayerFps(getStreamPref("video.maxFps"));
- let options = {
- processing: getStreamPref("video.processing"),
- sharpness: getStreamPref("video.processing.sharpness"),
- saturation: getStreamPref("video.saturation"),
- contrast: getStreamPref("video.contrast"),
- brightness: getStreamPref("video.brightness")
- };
- streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
}
-window.addEventListener("resize", updateVideoPlayer);
class KeyHelper {
- static NON_PRINTABLE_KEYS = {
- Backquote: "`",
- Minus: "-",
- Equal: "=",
- BracketLeft: "[",
- BracketRight: "]",
- Backslash: "\\",
- Semicolon: ";",
- Quote: "'",
- Comma: ",",
- Period: ".",
- Slash: "/",
- NumpadMultiply: "Numpad *",
- NumpadAdd: "Numpad +",
- NumpadSubtract: "Numpad -",
- NumpadDecimal: "Numpad .",
- NumpadDivide: "Numpad /",
- NumpadEqual: "Numpad =",
- Mouse0: "Left Click",
- Mouse2: "Right Click",
- Mouse1: "Middle Click",
- ScrollUp: "Scroll Up",
- ScrollDown: "Scroll Down",
- ScrollLeft: "Scroll Left",
- ScrollRight: "Scroll Right"
- };
- static getKeyFromEvent(e) {
- let code = null, modifiers;
- if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0;
- else if (e instanceof WheelEvent) {
- if (e.deltaY < 0) code = "ScrollUp";
- else if (e.deltaY > 0) code = "ScrollDown";
- else if (e.deltaX < 0) code = "ScrollLeft";
- else if (e.deltaX > 0) code = "ScrollRight";
- } else if (e instanceof MouseEvent) code = "Mouse" + e.button;
- if (code) {
- let results = { code };
- if (modifiers) results.modifiers = modifiers;
- return results;
- }
- return null;
- }
- static getFullKeyCodeFromEvent(e) {
- let key = KeyHelper.getKeyFromEvent(e);
- return key ? `${key.code}:${key.modifiers || 0}` : "";
- }
- static parseFullKeyCode(str) {
- if (!str) return null;
- let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]);
- return {
- code,
- modifiers
- };
- }
- static codeToKeyName(key) {
- let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code];
- if (modifiers && modifiers !== 0) {
- if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) {
- if (modifiers & 2) text.unshift("Shift");
- if (modifiers & 4) text.unshift("Alt");
- if (modifiers & 1) text.unshift("Ctrl");
- }
- }
- return text.join(" + ");
- }
+static NON_PRINTABLE_KEYS = {
+Backquote: "`",
+Minus: "-",
+Equal: "=",
+BracketLeft: "[",
+BracketRight: "]",
+Backslash: "\\",
+Semicolon: ";",
+Quote: "'",
+Comma: ",",
+Period: ".",
+Slash: "/",
+NumpadMultiply: "Numpad *",
+NumpadAdd: "Numpad +",
+NumpadSubtract: "Numpad -",
+NumpadDecimal: "Numpad .",
+NumpadDivide: "Numpad /",
+NumpadEqual: "Numpad =",
+Mouse0: "Left Click",
+Mouse2: "Right Click",
+Mouse1: "Middle Click",
+ScrollUp: "Scroll Up",
+ScrollDown: "Scroll Down",
+ScrollLeft: "Scroll Left",
+ScrollRight: "Scroll Right"
+};
+static getKeyFromEvent(e) {
+let code = null, modifiers;
+if (e instanceof KeyboardEvent) code = e.code || e.key, modifiers = 0, modifiers ^= e.ctrlKey ? 1 : 0, modifiers ^= e.shiftKey ? 2 : 0, modifiers ^= e.altKey ? 4 : 0;
+else if (e instanceof WheelEvent) {
+if (e.deltaY < 0) code = "ScrollUp";
+else if (e.deltaY > 0) code = "ScrollDown";
+else if (e.deltaX < 0) code = "ScrollLeft";
+else if (e.deltaX > 0) code = "ScrollRight";
+} else if (e instanceof MouseEvent) code = "Mouse" + e.button;
+if (code) {
+let results = { code };
+if (modifiers) results.modifiers = modifiers;
+return results;
+}
+return null;
+}
+static getFullKeyCodeFromEvent(e) {
+let key = KeyHelper.getKeyFromEvent(e);
+return key ? `${key.code}:${key.modifiers || 0}` : "";
+}
+static parseFullKeyCode(str) {
+if (!str) return null;
+let tmp = str.split(":"), code = tmp[0], modifiers = parseInt(tmp[1]);
+return {
+code,
+modifiers
+};
+}
+static codeToKeyName(key) {
+let { code, modifiers } = key, text = [KeyHelper.NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code];
+if (modifiers && modifiers !== 0) {
+if (!code.startsWith("Control") && !code.startsWith("Shift") && !code.startsWith("Alt")) {
+if (modifiers & 2) text.unshift("Shift");
+if (modifiers & 4) text.unshift("Alt");
+if (modifiers & 1) text.unshift("Ctrl");
+}
+}
+return text.join(" + ");
+}
}
class PointerClient {
- static instance;
- static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
- LOG_TAG = "PointerClient";
- REQUIRED_PROTOCOL_VERSION = 2;
- socket;
- mkbHandler;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- start(port, mkbHandler) {
- if (!port) throw new Error("PointerServer port is 0");
- this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {
- BxLogger.info(this.LOG_TAG, "connected");
- }), this.socket.addEventListener("error", (event) => {
- BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
- }), this.socket.addEventListener("close", (event) => {
- this.socket = null;
- }), this.socket.addEventListener("message", (event) => {
- let dataView = new DataView(event.data), messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;
- switch (messageType) {
- case 127:
- let protocolVersion = this.onProtocolVersion(dataView, offset);
- if (BxLogger.info(this.LOG_TAG, "Protocol version", protocolVersion), protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) alert("Required MKB protocol: " + protocolVersion), this.stop();
- break;
- case 1:
- this.onMove(dataView, offset);
- break;
- case 2:
- case 3:
- this.onPress(messageType, dataView, offset);
- break;
- case 4:
- this.onScroll(dataView, offset);
- break;
- case 5:
- this.onPointerCaptureChanged(dataView, offset);
- }
- });
- }
- onProtocolVersion(dataView, offset) {
- return dataView.getUint16(offset);
- }
- onMove(dataView, offset) {
- let x = dataView.getInt16(offset);
- offset += Int16Array.BYTES_PER_ELEMENT;
- let y = dataView.getInt16(offset);
- this.mkbHandler?.handleMouseMove({
- movementX: x,
- movementY: y
- });
- }
- onPress(messageType, dataView, offset) {
- let button = dataView.getUint8(offset);
- this.mkbHandler?.handleMouseClick({
- pointerButton: button,
- pressed: messageType === 2
- });
- }
- onScroll(dataView, offset) {
- let vScroll = dataView.getInt16(offset);
- offset += Int16Array.BYTES_PER_ELEMENT;
- let hScroll = dataView.getInt16(offset);
- this.mkbHandler?.handleMouseWheel({
- vertical: vScroll,
- horizontal: hScroll
- });
- }
- onPointerCaptureChanged(dataView, offset) {
- dataView.getInt8(offset) !== 1 && this.mkbHandler?.stop();
- }
- stop() {
- try {
- this.socket?.close();
- } catch (e) {}
- this.socket = null;
- }
+static instance;
+static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
+LOG_TAG = "PointerClient";
+REQUIRED_PROTOCOL_VERSION = 2;
+socket;
+mkbHandler;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+start(port, mkbHandler) {
+if (!port) throw new Error("PointerServer port is 0");
+this.mkbHandler = mkbHandler, this.socket = new WebSocket(`ws://localhost:${port}`), this.socket.binaryType = "arraybuffer", this.socket.addEventListener("open", (event) => {
+BxLogger.info(this.LOG_TAG, "connected");
+}), this.socket.addEventListener("error", (event) => {
+BxLogger.error(this.LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
+}), this.socket.addEventListener("close", (event) => {
+this.socket = null;
+}), this.socket.addEventListener("message", (event) => {
+let dataView = new DataView(event.data), messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;
+switch (messageType) {
+case 127:
+let protocolVersion = this.onProtocolVersion(dataView, offset);
+if (BxLogger.info(this.LOG_TAG, "Protocol version", protocolVersion), protocolVersion !== this.REQUIRED_PROTOCOL_VERSION) alert("Required MKB protocol: " + protocolVersion), this.stop();
+break;
+case 1:
+this.onMove(dataView, offset);
+break;
+case 2:
+case 3:
+this.onPress(messageType, dataView, offset);
+break;
+case 4:
+this.onScroll(dataView, offset);
+break;
+case 5:
+this.onPointerCaptureChanged(dataView, offset);
+}
+});
+}
+onProtocolVersion(dataView, offset) {
+return dataView.getUint16(offset);
+}
+onMove(dataView, offset) {
+let x = dataView.getInt16(offset);
+offset += Int16Array.BYTES_PER_ELEMENT;
+let y = dataView.getInt16(offset);
+this.mkbHandler?.handleMouseMove({
+movementX: x,
+movementY: y
+});
+}
+onPress(messageType, dataView, offset) {
+let button = dataView.getUint8(offset);
+this.mkbHandler?.handleMouseClick({
+pointerButton: button,
+pressed: messageType === 2
+});
+}
+onScroll(dataView, offset) {
+let vScroll = dataView.getInt16(offset);
+offset += Int16Array.BYTES_PER_ELEMENT;
+let hScroll = dataView.getInt16(offset);
+this.mkbHandler?.handleMouseWheel({
+vertical: vScroll,
+horizontal: hScroll
+});
+}
+onPointerCaptureChanged(dataView, offset) {
+dataView.getInt8(offset) !== 1 && this.mkbHandler?.stop();
+}
+stop() {
+try {
+this.socket?.close();
+} catch (e) {}
+this.socket = null;
+}
}
class MouseDataProvider {
- mkbHandler;
- constructor(handler) {
- this.mkbHandler = handler;
- }
- init() {}
- destroy() {}
+mkbHandler;
+constructor(handler) {
+this.mkbHandler = handler;
+}
+init() {}
+destroy() {}
}
class MkbHandler {}
-function showGamepadToast(gamepad) {
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) return;
- if (gamepad._noToast) return;
- BxLogger.info("Gamepad", gamepad);
- let text = "🎮";
- if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`;
- let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");
- text += ` - ${gamepadId}`;
- let status;
- if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");
- else status = t("disconnected");
- Toast.show(text, status, { instant: !1 });
-}
-function simplifyGamepadName(name) {
- return name.replace(/\s+\(.*Vendor: ([0-9a-f]{4}) Product: ([0-9a-f]{4})\)$/, " ($1-$2)");
-}
-function getUniqueGamepadNames() {
- let gamepads = window.navigator.getGamepads(), names = [];
- for (let gamepad of gamepads)
- if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id);
- return names;
-}
-function hasGamepad() {
- let gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads)
- if (gamepad?.connected) return !0;
- return !1;
-}
-function generateVirtualControllerMapping(index, override = {}) {
- return Object.assign({}, {
- GamepadIndex: index,
- A: 0,
- B: 0,
- X: 0,
- Y: 0,
- LeftShoulder: 0,
- RightShoulder: 0,
- LeftTrigger: 0,
- RightTrigger: 0,
- View: 0,
- Menu: 0,
- LeftThumb: 0,
- RightThumb: 0,
- DPadUp: 0,
- DPadDown: 0,
- DPadLeft: 0,
- DPadRight: 0,
- Nexus: 0,
- LeftThumbXAxis: 0,
- LeftThumbYAxis: 0,
- RightThumbXAxis: 0,
- RightThumbYAxis: 0,
- PhysicalPhysicality: 0,
- VirtualPhysicality: 0,
- Dirty: !1,
- Virtual: !1
- }, override);
-}
-function getGamepadPrompt(gamepadKey) {
- return GamepadKeyName[gamepadKey][1];
-}
-var XCLOUD_GAMEPAD_KEY_MAPPING = {
- 0: "A",
- 1: "B",
- 2: "X",
- 3: "Y",
- 12: "DPadUp",
- 15: "DPadRight",
- 13: "DPadDown",
- 14: "DPadLeft",
- 4: "LeftShoulder",
- 5: "RightShoulder",
- 6: "LeftTrigger",
- 7: "RightTrigger",
- 10: "LeftThumb",
- 11: "RightThumb",
- 104: "LeftStickAxes",
- 204: "RightStickAxes",
- 8: "View",
- 9: "Menu",
- 16: "Nexus",
- 17: "Share",
- 102: "LeftThumbXAxis",
- 103: "LeftThumbXAxis",
- 100: "LeftThumbYAxis",
- 101: "LeftThumbYAxis",
- 202: "RightThumbXAxis",
- 203: "RightThumbXAxis",
- 200: "RightThumbYAxis",
- 201: "RightThumbYAxis"
-};
-function toXcloudGamepadKey(gamepadKey) {
- return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
-}
-class StreamSettings {
- static settings = {
- settings: {},
- xCloudPollingMode: "all",
- deviceVibrationIntensity: 0,
- controllerPollingRate: 4,
- controllers: {},
- mkbPreset: null,
- keyboardShortcuts: {}
- };
- static async refreshControllerSettings() {
- let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads) {
- if (!gamepad?.connected) continue;
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
- let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data);
- controllers[gamepad.id] = {
- shortcuts: shortcutsMapping,
- customization: customizationData
- };
- }
- settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration();
- }
- static preCalculateControllerRange(obj, target, values) {
- if (values && Array.isArray(values)) {
- let [from, to] = values;
- if (from > 1 || to < 100) obj[target] = [from / 100, to / 100];
- }
- }
- static convertControllerCustomization(customization) {
- if (!customization) return null;
- let converted = {
- mapping: {},
- ranges: {},
- vibrationIntensity: 1
- }, gamepadKey;
- for (gamepadKey in customization.mapping) {
- let gamepadStr = toXcloudGamepadKey(gamepadKey);
- if (!gamepadStr) continue;
- let mappedKey = customization.mapping[gamepadKey];
- if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey);
- else converted.mapping[gamepadStr] = !1;
- }
- return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted;
- }
- static async refreshDeviceVibration() {
- if (!STATES.browser.capabilities.deviceVibration) return;
- let mode = getStreamPref("deviceVibration.mode"), intensity = 0;
- if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100;
- StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {});
- }
- static async refreshMkbSettings() {
- let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = {
- mapping: {},
- mouse: Object.assign({}, orgPresetData.mouse)
- }, key;
- for (key in orgPresetData.mapping) {
- let buttonIndex = parseInt(key);
- if (!orgPresetData.mapping[buttonIndex]) continue;
- for (let keyName of orgPresetData.mapping[buttonIndex])
- if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex;
- }
- let mouse = converted.mouse;
- mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {});
- }
- static async refreshKeyboardShortcuts() {
- let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId");
- if (presetId === 0) {
- settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
- return;
- }
- let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action;
- for (action in orgPresetData) {
- let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`;
- converted[key] = action;
- }
- settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
- }
- static async refreshAllSettings() {
- window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts();
- }
- static findKeyboardShortcut(targetAction) {
- let shortcuts = StreamSettings.settings.keyboardShortcuts;
- for (let codeStr in shortcuts)
- if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr);
- return null;
- }
- static setup() {
- let listener = () => {
- StreamSettings.refreshControllerSettings();
- };
- window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings();
- }
-}
class MkbPopup {
- static instance;
- static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup);
- popupType;
- $popup;
- $title;
- $btnActivate;
- mkbHandler;
- constructor() {
- this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => {
- let $newButton = this.createActivateButton();
- this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton;
- });
- }
- attachMkbHandler(handler) {
- this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller");
- }
- toggleVisibility(show) {
- this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1);
- }
- moveOffscreen(doMove) {
- this.$popup.classList.toggle("bx-offscreen", doMove);
- }
- createActivateButton() {
- let options = {
- style: 1 | 1024 | 128,
- label: t("activate"),
- onClick: this.onActivate
- }, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) });
- return createButton(options);
- }
- onActivate = (e) => {
- e.preventDefault(), this.mkbHandler.toggle(!0);
- };
- render() {
- this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({
- label: t("ignore"),
- style: 8,
- onClick: (e) => {
- e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1);
- }
- }), createButton({
- label: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64,
- onClick: () => {
- let dialog = SettingsDialog.getInstance();
- dialog.focusTab("mkb"), dialog.show();
- }
- }))), document.documentElement.appendChild(this.$popup);
- }
- reset() {
- this.toggleVisibility(!0), this.moveOffscreen(!1);
- }
+static instance;
+static getInstance = () => MkbPopup.instance ?? (MkbPopup.instance = new MkbPopup);
+popupType;
+$popup;
+$title;
+$btnActivate;
+mkbHandler;
+constructor() {
+this.render(), BxEventBus.Stream.on("keyboardShortcuts.updated", () => {
+let $newButton = this.createActivateButton();
+this.$btnActivate.replaceWith($newButton), this.$btnActivate = $newButton;
+});
+}
+attachMkbHandler(handler) {
+this.mkbHandler = handler, this.popupType = handler instanceof NativeMkbHandler ? "native" : "virtual", this.$popup.dataset.type = this.popupType, this.$title.innerText = t(this.popupType === "native" ? "native-mkb" : "virtual-controller");
+}
+toggleVisibility(show) {
+this.$popup.classList.toggle("bx-gone", !show), show && this.moveOffscreen(!1);
+}
+moveOffscreen(doMove) {
+this.$popup.classList.toggle("bx-offscreen", doMove);
+}
+createActivateButton() {
+let options = {
+style: 1 | 1024 | 128,
+label: t("activate"),
+onClick: this.onActivate
+}, shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) options.secondaryText = t("press-key-to-toggle-mkb", { key: KeyHelper.codeToKeyName(shortcutKey) });
+return createButton(options);
+}
+onActivate = (e) => {
+e.preventDefault(), this.mkbHandler.toggle(!0);
+};
+render() {
+this.$popup = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, this.$title = CE("p"), this.$btnActivate = this.createActivateButton(), CE("div", !1, createButton({
+label: t("ignore"),
+style: 8,
+onClick: (e) => {
+e.preventDefault(), this.mkbHandler.toggle(!1), this.mkbHandler.waitForMouseData(!1);
+}
+}), createButton({
+label: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64,
+onClick: () => {
+let dialog = SettingsDialog.getInstance();
+dialog.focusTab("mkb"), dialog.show();
+}
+}))), document.documentElement.appendChild(this.$popup);
+}
+reset() {
+this.toggleVisibility(!0), this.moveOffscreen(!1);
+}
}
class NativeMkbHandler extends MkbHandler {
- static instance;
- static getInstance() {
- if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler;
- else NativeMkbHandler.instance = null;
- return NativeMkbHandler.instance;
- }
- LOG_TAG = "NativeMkbHandler";
- static isAllowed = () => {
- return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on";
- };
- pointerClient;
- enabled = !1;
- mouseButtonsPressed = 0;
- mouseVerticalMultiply = 0;
- mouseHorizontalMultiply = 0;
- inputChannel;
- popup;
- constructor() {
- super();
- BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
- }
- onKeyboardEvent(e) {
- if (e.type === "keyup" && e.code === "F8") {
- e.preventDefault(), this.toggle();
- return;
- }
- }
- onPointerLockRequested(e) {
- AppInterface.requestPointerCapture(), this.start();
- }
- onPointerLockExited(e) {
- AppInterface.releasePointerCapture(), this.stop();
- }
- onPollingModeChanged = (e) => {
- let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
- this.popup.moveOffscreen(move);
- };
- onDialogShown = () => {
- document.pointerLockElement && document.exitPointerLock();
- };
- handleEvent(event) {
- switch (event.type) {
- case "keyup":
- this.onKeyboardEvent(event);
- break;
- case BxEvent.POINTER_LOCK_REQUESTED:
- this.onPointerLockRequested(event);
- break;
- case BxEvent.POINTER_LOCK_EXITED:
- this.onPointerLockExited(event);
- break;
- case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
- this.onPollingModeChanged(event);
- break;
- }
- }
- init() {
- this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1);
- try {
- this.pointerClient.start(STATES.pointerServerPort, this);
- } catch (e) {
- Toast.show("Cannot enable Mouse & Keyboard feature");
- }
- this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown);
- let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) {
- let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
- Toast.show(msg, t("native-mkb"), { html: !0 });
- }
- this.waitForMouseData(!1);
- }
- toggle(force) {
- let setEnable;
- if (typeof force !== "undefined") setEnable = force;
- else setEnable = !this.enabled;
- if (setEnable) document.documentElement.requestPointerLock();
- else document.exitPointerLock();
- }
- updateInputConfigurationAsync(enabled) {
- window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
- enableKeyboardInput: enabled,
- enableMouseInput: enabled,
- enableAbsoluteMouse: !1,
- enableTouchInput: !1
- });
- }
- start() {
- this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });
- }
- stop() {
- this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0);
- }
- destroy() {
- this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock();
- }
- handleMouseMove(data) {
- this.sendMouseInput({
- X: data.movementX,
- Y: data.movementY,
- Buttons: this.mouseButtonsPressed,
- WheelX: 0,
- WheelY: 0
- });
- }
- handleMouseClick(data) {
- let { pointerButton, pressed } = data;
- if (pressed) this.mouseButtonsPressed |= pointerButton;
- else this.mouseButtonsPressed ^= pointerButton;
- this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: this.mouseButtonsPressed,
- WheelX: 0,
- WheelY: 0
- });
- }
- handleMouseWheel(data) {
- let { vertical, horizontal } = data, mouseWheelX = horizontal;
- if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply;
- let mouseWheelY = vertical;
- if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply;
- return this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: this.mouseButtonsPressed,
- WheelX: mouseWheelX,
- WheelY: mouseWheelY
- }), !0;
- }
- setVerticalScrollMultiplier(vertical) {
- this.mouseVerticalMultiply = vertical;
- }
- setHorizontalScrollMultiplier(horizontal) {
- this.mouseHorizontalMultiply = horizontal;
- }
- waitForMouseData(showPopup) {
- this.popup.toggleVisibility(showPopup);
- }
- isEnabled() {
- return this.enabled;
- }
- sendMouseInput(data) {
- data.Type = 0, this.inputChannel?.queueMouseInput(data);
- }
- resetMouseInput() {
- this.mouseButtonsPressed = 0, this.sendMouseInput({
- X: 0,
- Y: 0,
- Buttons: 0,
- WheelX: 0,
- WheelY: 0
- });
- }
+static instance;
+static getInstance() {
+if (typeof NativeMkbHandler.instance === "undefined") if (NativeMkbHandler.isAllowed()) NativeMkbHandler.instance = new NativeMkbHandler;
+else NativeMkbHandler.instance = null;
+return NativeMkbHandler.instance;
+}
+LOG_TAG = "NativeMkbHandler";
+static isAllowed = () => {
+return STATES.browser.capabilities.emulatedNativeMkb && getGlobalPref("nativeMkb.mode") === "on";
+};
+pointerClient;
+enabled = !1;
+mouseButtonsPressed = 0;
+mouseVerticalMultiply = 0;
+mouseHorizontalMultiply = 0;
+inputChannel;
+popup;
+constructor() {
+super();
+BxLogger.info(this.LOG_TAG, "constructor()"), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
+}
+onKeyboardEvent(e) {
+if (e.type === "keyup" && e.code === "F8") {
+e.preventDefault(), this.toggle();
+return;
+}
+}
+onPointerLockRequested(e) {
+AppInterface.requestPointerCapture(), this.start();
+}
+onPointerLockExited(e) {
+AppInterface.releasePointerCapture(), this.stop();
+}
+onPollingModeChanged = (e) => {
+let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
+this.popup.moveOffscreen(move);
+};
+onDialogShown = () => {
+document.pointerLockElement && document.exitPointerLock();
+};
+handleEvent(event) {
+switch (event.type) {
+case "keyup":
+this.onKeyboardEvent(event);
+break;
+case BxEvent.POINTER_LOCK_REQUESTED:
+this.onPointerLockRequested(event);
+break;
+case BxEvent.POINTER_LOCK_EXITED:
+this.onPointerLockExited(event);
+break;
+case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
+this.onPollingModeChanged(event);
+break;
+}
+}
+init() {
+this.pointerClient = PointerClient.getInstance(), this.inputChannel = window.BX_EXPOSED.inputChannel, this.updateInputConfigurationAsync(!1);
+try {
+this.pointerClient.start(STATES.pointerServerPort, this);
+} catch (e) {
+Toast.show("Cannot enable Mouse & Keyboard feature");
+}
+this.mouseVerticalMultiply = getStreamPref("nativeMkb.scroll.sensitivityY"), this.mouseHorizontalMultiply = getStreamPref("nativeMkb.scroll.sensitivityX"), window.addEventListener("keyup", this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.on("dialog.shown", this.onDialogShown);
+let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) {
+let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
+Toast.show(msg, t("native-mkb"), { html: !0 });
+}
+this.waitForMouseData(!1);
+}
+toggle(force) {
+let setEnable;
+if (typeof force !== "undefined") setEnable = force;
+else setEnable = !this.enabled;
+if (setEnable) document.documentElement.requestPointerLock();
+else document.exitPointerLock();
+}
+updateInputConfigurationAsync(enabled) {
+window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
+enableKeyboardInput: enabled,
+enableMouseInput: enabled,
+enableAbsoluteMouse: !1,
+enableTouchInput: !1
+});
+}
+start() {
+this.resetMouseInput(), this.enabled = !0, this.updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.waitForMouseData(!1), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });
+}
+stop() {
+this.resetMouseInput(), this.enabled = !1, this.updateInputConfigurationAsync(!1), this.waitForMouseData(!0);
+}
+destroy() {
+this.pointerClient?.stop(), this.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.waitForMouseData(!1), document.exitPointerLock();
+}
+handleMouseMove(data) {
+this.sendMouseInput({
+X: data.movementX,
+Y: data.movementY,
+Buttons: this.mouseButtonsPressed,
+WheelX: 0,
+WheelY: 0
+});
+}
+handleMouseClick(data) {
+let { pointerButton, pressed } = data;
+if (pressed) this.mouseButtonsPressed |= pointerButton;
+else this.mouseButtonsPressed ^= pointerButton;
+this.mouseButtonsPressed = Math.max(0, this.mouseButtonsPressed), this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: this.mouseButtonsPressed,
+WheelX: 0,
+WheelY: 0
+});
+}
+handleMouseWheel(data) {
+let { vertical, horizontal } = data, mouseWheelX = horizontal;
+if (this.mouseHorizontalMultiply && this.mouseHorizontalMultiply !== 1) mouseWheelX *= this.mouseHorizontalMultiply;
+let mouseWheelY = vertical;
+if (this.mouseVerticalMultiply && this.mouseVerticalMultiply !== 1) mouseWheelY *= this.mouseVerticalMultiply;
+return this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: this.mouseButtonsPressed,
+WheelX: mouseWheelX,
+WheelY: mouseWheelY
+}), !0;
+}
+setVerticalScrollMultiplier(vertical) {
+this.mouseVerticalMultiply = vertical;
+}
+setHorizontalScrollMultiplier(horizontal) {
+this.mouseHorizontalMultiply = horizontal;
+}
+waitForMouseData(showPopup) {
+this.popup.toggleVisibility(showPopup);
+}
+isEnabled() {
+return this.enabled;
+}
+sendMouseInput(data) {
+data.Type = 0, this.inputChannel?.queueMouseInput(data);
+}
+resetMouseInput() {
+this.mouseButtonsPressed = 0, this.sendMouseInput({
+X: 0,
+Y: 0,
+Buttons: 0,
+WheelX: 0,
+WheelY: 0
+});
+}
+}
+function showGamepadToast(gamepad) {
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) return;
+if (gamepad._noToast) return;
+BxLogger.info("Gamepad", gamepad);
+let text = "🎮";
+if (getStreamPref("localCoOp.enabled")) text += ` #${gamepad.index + 1}`;
+let gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");
+text += ` - ${gamepadId}`;
+let status;
+if (gamepad.connected) status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");
+else status = t("disconnected");
+Toast.show(text, status, { instant: !1 });
+}
+function simplifyGamepadName(name) {
+return name.replace(/\s+\(.*Vendor: ([0-9a-f]{4}) Product: ([0-9a-f]{4})\)$/, " ($1-$2)");
+}
+function getUniqueGamepadNames() {
+let gamepads = window.navigator.getGamepads(), names = [];
+for (let gamepad of gamepads)
+if (gamepad?.connected && gamepad.id !== VIRTUAL_GAMEPAD_ID) !names.includes(gamepad.id) && names.push(gamepad.id);
+return names;
+}
+function hasGamepad() {
+let gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads)
+if (gamepad?.connected) return !0;
+return !1;
+}
+function generateVirtualControllerMapping(index, override = {}) {
+return Object.assign({}, {
+GamepadIndex: index,
+A: 0,
+B: 0,
+X: 0,
+Y: 0,
+LeftShoulder: 0,
+RightShoulder: 0,
+LeftTrigger: 0,
+RightTrigger: 0,
+View: 0,
+Menu: 0,
+LeftThumb: 0,
+RightThumb: 0,
+DPadUp: 0,
+DPadDown: 0,
+DPadLeft: 0,
+DPadRight: 0,
+Nexus: 0,
+LeftThumbXAxis: 0,
+LeftThumbYAxis: 0,
+RightThumbXAxis: 0,
+RightThumbYAxis: 0,
+PhysicalPhysicality: 0,
+VirtualPhysicality: 0,
+Dirty: !1,
+Virtual: !1
+}, override);
+}
+function getGamepadPrompt(gamepadKey) {
+return GamepadKeyName[gamepadKey][1];
+}
+var XCLOUD_GAMEPAD_KEY_MAPPING = {
+0: "A",
+1: "B",
+2: "X",
+3: "Y",
+12: "DPadUp",
+15: "DPadRight",
+13: "DPadDown",
+14: "DPadLeft",
+4: "LeftShoulder",
+5: "RightShoulder",
+6: "LeftTrigger",
+7: "RightTrigger",
+10: "LeftThumb",
+11: "RightThumb",
+104: "LeftStickAxes",
+204: "RightStickAxes",
+8: "View",
+9: "Menu",
+16: "Nexus",
+17: "Share",
+102: "LeftThumbXAxis",
+103: "LeftThumbXAxis",
+100: "LeftThumbYAxis",
+101: "LeftThumbYAxis",
+202: "RightThumbXAxis",
+203: "RightThumbXAxis",
+200: "RightThumbYAxis",
+201: "RightThumbYAxis"
+};
+function toXcloudGamepadKey(gamepadKey) {
+return XCLOUD_GAMEPAD_KEY_MAPPING[gamepadKey];
}
var PointerToMouseButton = {
- 1: 0,
- 2: 2,
- 4: 1
+1: 0,
+2: 2,
+4: 1
}, VIRTUAL_GAMEPAD_ID = "Better xCloud Virtual Controller";
class WebSocketMouseDataProvider extends MouseDataProvider {
- pointerClient;
- isConnected = !1;
- init() {
- this.pointerClient = PointerClient.getInstance(), this.isConnected = !1;
- try {
- this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0;
- } catch (e) {
- Toast.show("Cannot enable Mouse & Keyboard feature");
- }
- }
- start() {
- this.isConnected && AppInterface.requestPointerCapture();
- }
- stop() {
- this.isConnected && AppInterface.releasePointerCapture();
- }
- destroy() {
- this.isConnected && this.pointerClient?.stop();
- }
+pointerClient;
+isConnected = !1;
+init() {
+this.pointerClient = PointerClient.getInstance(), this.isConnected = !1;
+try {
+this.pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.isConnected = !0;
+} catch (e) {
+Toast.show("Cannot enable Mouse & Keyboard feature");
+}
+}
+start() {
+this.isConnected && AppInterface.requestPointerCapture();
+}
+stop() {
+this.isConnected && AppInterface.releasePointerCapture();
+}
+destroy() {
+this.isConnected && this.pointerClient?.stop();
+}
}
class PointerLockMouseDataProvider extends MouseDataProvider {
- start() {
- window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu);
- }
- stop() {
- document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu);
- }
- onMouseMoveEvent = (e) => {
- this.mkbHandler.handleMouseMove({
- movementX: e.movementX,
- movementY: e.movementY
- });
- };
- onMouseEvent = (e) => {
- e.preventDefault();
- let data = {
- mouseButton: e.button,
- pressed: e.type === "mousedown"
- };
- this.mkbHandler.handleMouseClick(data);
- };
- onWheelEvent = (e) => {
- if (!KeyHelper.getKeyFromEvent(e)) return;
- let data = {
- vertical: e.deltaY,
- horizontal: e.deltaX
- };
- if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault();
- };
- disableContextMenu = (e) => e.preventDefault();
+start() {
+window.addEventListener("mousemove", this.onMouseMoveEvent), window.addEventListener("mousedown", this.onMouseEvent), window.addEventListener("mouseup", this.onMouseEvent), window.addEventListener("wheel", this.onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.disableContextMenu);
+}
+stop() {
+document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.onMouseMoveEvent), window.removeEventListener("mousedown", this.onMouseEvent), window.removeEventListener("mouseup", this.onMouseEvent), window.removeEventListener("wheel", this.onWheelEvent), window.removeEventListener("contextmenu", this.disableContextMenu);
+}
+onMouseMoveEvent = (e) => {
+this.mkbHandler.handleMouseMove({
+movementX: e.movementX,
+movementY: e.movementY
+});
+};
+onMouseEvent = (e) => {
+e.preventDefault();
+let data = {
+mouseButton: e.button,
+pressed: e.type === "mousedown"
+};
+this.mkbHandler.handleMouseClick(data);
+};
+onWheelEvent = (e) => {
+if (!KeyHelper.getKeyFromEvent(e)) return;
+let data = {
+vertical: e.deltaY,
+horizontal: e.deltaX
+};
+if (this.mkbHandler.handleMouseWheel(data)) e.preventDefault();
+};
+disableContextMenu = (e) => e.preventDefault();
}
class EmulatedMkbHandler extends MkbHandler {
- static instance;
- static getInstance() {
- if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler;
- else EmulatedMkbHandler.instance = null;
- return EmulatedMkbHandler.instance;
- }
- static LOG_TAG = "EmulatedMkbHandler";
- static isAllowed() {
- return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile());
- }
- PRESET;
- VIRTUAL_GAMEPAD = {
- id: VIRTUAL_GAMEPAD_ID,
- index: 0,
- connected: !1,
- hapticActuators: null,
- mapping: "standard",
- axes: [0, 0, 0, 0],
- buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),
- timestamp: performance.now(),
- vibrationActuator: null
- };
- nativeGetGamepads;
- xCloudGamepad = generateVirtualControllerMapping(0);
- initialized = !1;
- enabled = !1;
- mouseDataProvider;
- isPolling = !1;
- prevWheelCode = null;
- wheelStoppedTimeoutId = null;
- detectMouseStoppedTimeoutId = null;
- escKeyDownTime = -1;
- LEFT_STICK_X = [];
- LEFT_STICK_Y = [];
- RIGHT_STICK_X = [];
- RIGHT_STICK_Y = [];
- popup;
- STICK_MAP = {
- 102: [this.LEFT_STICK_X, -1],
- 103: [this.LEFT_STICK_X, 1],
- 100: [this.LEFT_STICK_Y, 1],
- 101: [this.LEFT_STICK_Y, -1],
- 202: [this.RIGHT_STICK_X, -1],
- 203: [this.RIGHT_STICK_X, 1],
- 200: [this.RIGHT_STICK_Y, 1],
- 201: [this.RIGHT_STICK_Y, -1]
- };
- constructor() {
- super();
- BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
- }
- isEnabled = () => this.enabled;
- patchedGetGamepads = () => {
- let gamepads = this.nativeGetGamepads() || [];
- return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads;
- };
- getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
- updateStick(stick, x, y) {
- let gamepad = this.xCloudGamepad;
- if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y;
- else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y;
- window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
- }
- vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
- resetXcloudGamepads() {
- let index = getStreamPref("mkb.p1.slot") - 1;
- this.xCloudGamepad = generateVirtualControllerMapping(0, {
- GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0,
- Dirty: !0
- }), this.VIRTUAL_GAMEPAD.index = index;
- }
- pressButton(buttonIndex, pressed) {
- let xCloudKey = toXcloudGamepadKey(buttonIndex);
- if (buttonIndex >= 100) {
- let [valueArr] = this.STICK_MAP[buttonIndex];
- for (let i = valueArr.length - 1;i >= 0; i--)
- if (valueArr[i] === buttonIndex) valueArr.splice(i, 1);
- pressed && valueArr.push(buttonIndex);
- let value;
- if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1];
- else value = 0;
- this.xCloudGamepad[xCloudKey] = value;
- } else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
- window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
- }
- onKeyboardEvent = (e) => {
- let isKeyDown = e.type === "keydown";
- if (e.code === "Escape") {
- if (e.preventDefault(), this.enabled && isKeyDown) {
- if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now();
- else if (performance.now() - this.escKeyDownTime >= 1000) this.stop();
- } else this.escKeyDownTime = -1;
- return;
- }
- if (!this.isPolling || !this.PRESET) return;
- if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
- let buttonIndex = this.PRESET.mapping[e.code || e.key];
- if (typeof buttonIndex === "undefined") return;
- if (e.repeat) return;
- e.preventDefault(), this.pressButton(buttonIndex, isKeyDown);
- };
- onMouseStopped = () => {
- if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return;
- let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1;
- this.updateStick(analog, 0, 0);
- };
- handleMouseClick(data) {
- let mouseButton;
- if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton;
- else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton];
- let key = {
- code: "Mouse" + mouseButton
- };
- if (!this.PRESET) return;
- let buttonIndex = this.PRESET.mapping[key.code];
- if (typeof buttonIndex === "undefined") return;
- this.pressButton(buttonIndex, data.pressed);
- }
- handleMouseMove(data) {
- let preset = this.PRESET;
- if (!preset) return;
- let mouseMapTo = preset.mouse["mapTo"];
- if (mouseMapTo === 0) return;
- this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
- let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y);
- if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;
- else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length;
- let analog = mouseMapTo === 1 ? 0 : 1;
- this.updateStick(analog, x, y);
- }
- handleMouseWheel(data) {
- let code = "";
- if (data.vertical < 0) code = "ScrollUp";
- else if (data.vertical > 0) code = "ScrollDown";
- else if (data.horizontal < 0) code = "ScrollLeft";
- else if (data.horizontal > 0) code = "ScrollRight";
- if (!code) return !1;
- if (!this.PRESET) return !1;
- let key = {
- code
- }, buttonIndex = this.PRESET.mapping[key.code];
- if (typeof buttonIndex === "undefined") return !1;
- if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0);
- return this.wheelStoppedTimeoutId = window.setTimeout(() => {
- this.prevWheelCode = null, this.pressButton(buttonIndex, !1);
- }, 20), !0;
- }
- async toggle(force) {
- if (!this.initialized) return;
- if (typeof force !== "undefined") this.enabled = force;
- else this.enabled = !this.enabled;
- if (this.enabled) try {
- await document.body.requestPointerLock({ unadjustedMovement: !0 });
- } catch (e) {
- document.body.requestPointerLock(), console.log(e);
- }
- else document.pointerLockElement && document.exitPointerLock();
- }
- refreshPresetData() {
- this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads();
- }
- waitForMouseData(showPopup) {
- this.popup.toggleVisibility(showPopup);
- }
- onPollingModeChanged = (e) => {
- let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
- this.popup.moveOffscreen(move);
- };
- onDialogShown = () => {
- document.pointerLockElement && document.exitPointerLock();
- };
- onPointerLockChange = () => {
- if (document.pointerLockElement) this.start();
- else this.stop();
- };
- onPointerLockError = (e) => {
- console.log(e), this.stop();
- };
- onPointerLockRequested = () => {
- this.start();
- };
- onPointerLockExited = () => {
- this.mouseDataProvider?.stop();
- };
- handleEvent(event) {
- switch (event.type) {
- case BxEvent.POINTER_LOCK_REQUESTED:
- this.onPointerLockRequested();
- break;
- case BxEvent.POINTER_LOCK_EXITED:
- this.onPointerLockExited();
- break;
- }
- }
- init() {
- if (!STATES.browser.capabilities.mkb) {
- this.initialized = !1;
- return;
- }
- if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this);
- else this.mouseDataProvider = new PointerLockMouseDataProvider(this);
- if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
- else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError);
- if (MkbPopup.getInstance().reset(), AppInterface) {
- let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
- if (shortcutKey) {
- let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
- Toast.show(msg, t("native-mkb"), { html: !0 });
- }
- this.waitForMouseData(!1);
- } else this.waitForMouseData(!0);
- }
- destroy() {
- if (!this.initialized) return;
- if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
- else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError);
- window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
- }
- start() {
- if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
- this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start();
- let virtualGamepad = this.getVirtualGamepad();
- virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {
- gamepad: virtualGamepad
- }), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
- }
- stop() {
- this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1;
- let virtualGamepad = this.getVirtualGamepad();
- if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {
- gamepad: virtualGamepad
- }), window.navigator.getGamepads = this.nativeGetGamepads;
- this.waitForMouseData(!0), this.mouseDataProvider?.stop();
- }
- static setupEvents() {
- if (BxEventBus.Stream.on("state.playing", () => {
- if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init();
- else EmulatedMkbHandler.getInstance()?.init();
- }), EmulatedMkbHandler.isAllowed())
- BxEventBus.Stream.on("mkb.setting.updated", () => {
- EmulatedMkbHandler.getInstance()?.refreshPresetData();
- });
- }
+static instance;
+static getInstance() {
+if (typeof EmulatedMkbHandler.instance === "undefined") if (EmulatedMkbHandler.isAllowed()) EmulatedMkbHandler.instance = new EmulatedMkbHandler;
+else EmulatedMkbHandler.instance = null;
+return EmulatedMkbHandler.instance;
+}
+static LOG_TAG = "EmulatedMkbHandler";
+static isAllowed() {
+return getGlobalPref("mkb.enabled") && (AppInterface || !UserAgent.isMobile());
+}
+PRESET;
+VIRTUAL_GAMEPAD = {
+id: VIRTUAL_GAMEPAD_ID,
+index: 0,
+connected: !1,
+hapticActuators: null,
+mapping: "standard",
+axes: [0, 0, 0, 0],
+buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),
+timestamp: performance.now(),
+vibrationActuator: null
+};
+nativeGetGamepads;
+xCloudGamepad = generateVirtualControllerMapping(0);
+initialized = !1;
+enabled = !1;
+mouseDataProvider;
+isPolling = !1;
+prevWheelCode = null;
+wheelStoppedTimeoutId = null;
+detectMouseStoppedTimeoutId = null;
+escKeyDownTime = -1;
+LEFT_STICK_X = [];
+LEFT_STICK_Y = [];
+RIGHT_STICK_X = [];
+RIGHT_STICK_Y = [];
+popup;
+STICK_MAP = {
+102: [this.LEFT_STICK_X, -1],
+103: [this.LEFT_STICK_X, 1],
+100: [this.LEFT_STICK_Y, 1],
+101: [this.LEFT_STICK_Y, -1],
+202: [this.RIGHT_STICK_X, -1],
+203: [this.RIGHT_STICK_X, 1],
+200: [this.RIGHT_STICK_Y, 1],
+201: [this.RIGHT_STICK_Y, -1]
+};
+constructor() {
+super();
+BxLogger.info(EmulatedMkbHandler.LOG_TAG, "constructor()"), this.nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator), this.popup = MkbPopup.getInstance(), this.popup.attachMkbHandler(this);
+}
+isEnabled = () => this.enabled;
+patchedGetGamepads = () => {
+let gamepads = this.nativeGetGamepads() || [];
+return gamepads[this.VIRTUAL_GAMEPAD.index] = this.VIRTUAL_GAMEPAD, gamepads;
+};
+getVirtualGamepad = () => this.VIRTUAL_GAMEPAD;
+updateStick(stick, x, y) {
+let gamepad = this.xCloudGamepad;
+if (stick === 0) gamepad.LeftThumbXAxis = x, gamepad.LeftThumbYAxis = -y;
+else gamepad.RightThumbXAxis = x, gamepad.RightThumbYAxis = -y;
+window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
+}
+vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
+resetXcloudGamepads() {
+let index = getStreamPref("mkb.p1.slot") - 1;
+this.xCloudGamepad = generateVirtualControllerMapping(0, {
+GamepadIndex: getStreamPref("localCoOp.enabled") ? index : 0,
+Dirty: !0
+}), this.VIRTUAL_GAMEPAD.index = index;
+}
+pressButton(buttonIndex, pressed) {
+let xCloudKey = toXcloudGamepadKey(buttonIndex);
+if (buttonIndex >= 100) {
+let [valueArr] = this.STICK_MAP[buttonIndex];
+for (let i = valueArr.length - 1;i >= 0; i--)
+if (valueArr[i] === buttonIndex) valueArr.splice(i, 1);
+pressed && valueArr.push(buttonIndex);
+let value;
+if (valueArr.length) value = this.STICK_MAP[valueArr[valueArr.length - 1]][1];
+else value = 0;
+this.xCloudGamepad[xCloudKey] = value;
+} else this.xCloudGamepad[xCloudKey] = pressed ? 1 : 0;
+window.BX_EXPOSED.inputChannel?.sendGamepadInput(performance.now(), [this.xCloudGamepad]);
+}
+onKeyboardEvent = (e) => {
+let isKeyDown = e.type === "keydown";
+if (e.code === "Escape") {
+if (e.preventDefault(), this.enabled && isKeyDown) {
+if (this.escKeyDownTime === -1) this.escKeyDownTime = performance.now();
+else if (performance.now() - this.escKeyDownTime >= 1000) this.stop();
+} else this.escKeyDownTime = -1;
+return;
+}
+if (!this.isPolling || !this.PRESET) return;
+if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
+let buttonIndex = this.PRESET.mapping[e.code || e.key];
+if (typeof buttonIndex === "undefined") return;
+if (e.repeat) return;
+e.preventDefault(), this.pressButton(buttonIndex, isKeyDown);
+};
+onMouseStopped = () => {
+if (this.detectMouseStoppedTimeoutId = null, !this.PRESET) return;
+let analog = this.PRESET.mouse["mapTo"] === 1 ? 0 : 1;
+this.updateStick(analog, 0, 0);
+};
+handleMouseClick(data) {
+let mouseButton;
+if (typeof data.mouseButton !== "undefined") mouseButton = data.mouseButton;
+else if (typeof data.pointerButton !== "undefined") mouseButton = PointerToMouseButton[data.pointerButton];
+let key = {
+code: "Mouse" + mouseButton
+};
+if (!this.PRESET) return;
+let buttonIndex = this.PRESET.mapping[key.code];
+if (typeof buttonIndex === "undefined") return;
+this.pressButton(buttonIndex, data.pressed);
+}
+handleMouseMove(data) {
+let preset = this.PRESET;
+if (!preset) return;
+let mouseMapTo = preset.mouse["mapTo"];
+if (mouseMapTo === 0) return;
+this.detectMouseStoppedTimeoutId && clearTimeout(this.detectMouseStoppedTimeoutId), this.detectMouseStoppedTimeoutId = window.setTimeout(this.onMouseStopped, 50);
+let deadzoneCounterweight = preset.mouse["deadzoneCounterweight"], x = data.movementX * preset.mouse["sensitivityX"], y = data.movementY * preset.mouse["sensitivityY"], length = this.vectorLength(x, y);
+if (length !== 0 && length < deadzoneCounterweight) x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;
+else if (length > 1.1) x *= 1.1 / length, y *= 1.1 / length;
+let analog = mouseMapTo === 1 ? 0 : 1;
+this.updateStick(analog, x, y);
+}
+handleMouseWheel(data) {
+let code = "";
+if (data.vertical < 0) code = "ScrollUp";
+else if (data.vertical > 0) code = "ScrollDown";
+else if (data.horizontal < 0) code = "ScrollLeft";
+else if (data.horizontal > 0) code = "ScrollRight";
+if (!code) return !1;
+if (!this.PRESET) return !1;
+let key = {
+code
+}, buttonIndex = this.PRESET.mapping[key.code];
+if (typeof buttonIndex === "undefined") return !1;
+if (this.prevWheelCode === null || this.prevWheelCode === key.code) this.wheelStoppedTimeoutId && clearTimeout(this.wheelStoppedTimeoutId), this.pressButton(buttonIndex, !0);
+return this.wheelStoppedTimeoutId = window.setTimeout(() => {
+this.prevWheelCode = null, this.pressButton(buttonIndex, !1);
+}, 20), !0;
+}
+async toggle(force) {
+if (!this.initialized) return;
+if (typeof force !== "undefined") this.enabled = force;
+else this.enabled = !this.enabled;
+if (this.enabled) try {
+await document.body.requestPointerLock({ unadjustedMovement: !0 });
+} catch (e) {
+document.body.requestPointerLock(), console.log(e);
+}
+else document.pointerLockElement && document.exitPointerLock();
+}
+refreshPresetData() {
+this.PRESET = window.BX_STREAM_SETTINGS.mkbPreset, this.resetXcloudGamepads();
+}
+waitForMouseData(showPopup) {
+this.popup.toggleVisibility(showPopup);
+}
+onPollingModeChanged = (e) => {
+let move = window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none";
+this.popup.moveOffscreen(move);
+};
+onDialogShown = () => {
+document.pointerLockElement && document.exitPointerLock();
+};
+onPointerLockChange = () => {
+if (document.pointerLockElement) this.start();
+else this.stop();
+};
+onPointerLockError = (e) => {
+console.log(e), this.stop();
+};
+onPointerLockRequested = () => {
+this.start();
+};
+onPointerLockExited = () => {
+this.mouseDataProvider?.stop();
+};
+handleEvent(event) {
+switch (event.type) {
+case BxEvent.POINTER_LOCK_REQUESTED:
+this.onPointerLockRequested();
+break;
+case BxEvent.POINTER_LOCK_EXITED:
+this.onPointerLockExited();
+break;
+}
+}
+init() {
+if (!STATES.browser.capabilities.mkb) {
+this.initialized = !1;
+return;
+}
+if (this.initialized = !0, this.refreshPresetData(), this.enabled = !1, AppInterface) this.mouseDataProvider = new WebSocketMouseDataProvider(this);
+else this.mouseDataProvider = new PointerLockMouseDataProvider(this);
+if (this.mouseDataProvider.init(), window.addEventListener("keydown", this.onKeyboardEvent), window.addEventListener("keyup", this.onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.on("dialog.shown", this.onDialogShown), AppInterface) window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
+else document.addEventListener("pointerlockchange", this.onPointerLockChange), document.addEventListener("pointerlockerror", this.onPointerLockError);
+if (MkbPopup.getInstance().reset(), AppInterface) {
+let shortcutKey = StreamSettings.findKeyboardShortcut("mkb.toggle");
+if (shortcutKey) {
+let msg = t("press-key-to-toggle-mkb", { key: `${KeyHelper.codeToKeyName(shortcutKey)}` });
+Toast.show(msg, t("native-mkb"), { html: !0 });
+}
+this.waitForMouseData(!1);
+} else this.waitForMouseData(!0);
+}
+destroy() {
+if (!this.initialized) return;
+if (this.initialized = !1, this.isPolling = !1, this.enabled = !1, this.stop(), this.waitForMouseData(!1), document.exitPointerLock(), window.removeEventListener("keydown", this.onKeyboardEvent), window.removeEventListener("keyup", this.onKeyboardEvent), AppInterface) window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
+else document.removeEventListener("pointerlockchange", this.onPointerLockChange), document.removeEventListener("pointerlockerror", this.onPointerLockError);
+window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged), BxEventBus.Script.off("dialog.shown", this.onDialogShown), this.mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.onPollingModeChanged);
+}
+start() {
+if (!this.enabled) this.enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
+this.isPolling = !0, this.escKeyDownTime = -1, window.BX_EXPOSED.toggleLocalCoOp(getStreamPref("localCoOp.enabled")), this.resetXcloudGamepads(), window.navigator.getGamepads = this.patchedGetGamepads, this.waitForMouseData(!1), this.mouseDataProvider?.start();
+let virtualGamepad = this.getVirtualGamepad();
+virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {
+gamepad: virtualGamepad
+}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
+}
+stop() {
+this.enabled = !1, this.isPolling = !1, this.escKeyDownTime = -1;
+let virtualGamepad = this.getVirtualGamepad();
+if (virtualGamepad.connected) this.resetXcloudGamepads(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {
+gamepad: virtualGamepad
+}), window.navigator.getGamepads = this.nativeGetGamepads;
+this.waitForMouseData(!0), this.mouseDataProvider?.stop();
+}
+static setupEvents() {
+if (BxEventBus.Stream.on("state.playing", () => {
+if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.init();
+else EmulatedMkbHandler.getInstance()?.init();
+}), EmulatedMkbHandler.isAllowed())
+BxEventBus.Stream.on("mkb.setting.updated", () => {
+EmulatedMkbHandler.getInstance()?.refreshPresetData();
+});
+}
+}
+class StreamSettings {
+static settings = {
+settings: {},
+xCloudPollingMode: "all",
+deviceVibrationIntensity: 0,
+controllerPollingRate: 4,
+controllers: {},
+mkbPreset: null,
+keyboardShortcuts: {}
+};
+static async refreshControllerSettings() {
+let settings = StreamSettings.settings, controllers = {}, shortcutsTable = ControllerShortcutsTable.getInstance(), mappingTable = ControllerCustomizationsTable.getInstance(), gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads) {
+if (!gamepad?.connected) continue;
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
+let controllerSetting = STORAGE.Stream.getControllerSetting(gamepad.id), shortcutsPreset = await shortcutsTable.getPreset(controllerSetting.shortcutPresetId), shortcutsMapping = !shortcutsPreset ? null : shortcutsPreset.data.mapping, customizationPreset = await mappingTable.getPreset(controllerSetting.customizationPresetId), customizationData = StreamSettings.convertControllerCustomization(customizationPreset?.data);
+controllers[gamepad.id] = {
+shortcuts: shortcutsMapping,
+customization: customizationData
+};
+}
+settings.controllers = controllers, settings.controllerPollingRate = getStreamPref("controller.pollingRate"), await StreamSettings.refreshDeviceVibration();
+}
+static preCalculateControllerRange(obj, target, values) {
+if (values && Array.isArray(values)) {
+let [from, to] = values;
+if (from > 1 || to < 100) obj[target] = [from / 100, to / 100];
+}
+}
+static convertControllerCustomization(customization) {
+if (!customization) return null;
+let converted = {
+mapping: {},
+ranges: {},
+vibrationIntensity: 1
+}, gamepadKey;
+for (gamepadKey in customization.mapping) {
+let gamepadStr = toXcloudGamepadKey(gamepadKey);
+if (!gamepadStr) continue;
+let mappedKey = customization.mapping[gamepadKey];
+if (typeof mappedKey === "number") converted.mapping[gamepadStr] = toXcloudGamepadKey(mappedKey);
+else converted.mapping[gamepadStr] = !1;
+}
+return StreamSettings.preCalculateControllerRange(converted.ranges, "LeftTrigger", customization.settings.leftTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "RightTrigger", customization.settings.rightTriggerRange), StreamSettings.preCalculateControllerRange(converted.ranges, "LeftThumb", customization.settings.leftStickDeadzone), StreamSettings.preCalculateControllerRange(converted.ranges, "RightThumb", customization.settings.rightStickDeadzone), converted.vibrationIntensity = customization.settings.vibrationIntensity / 100, converted;
+}
+static async refreshDeviceVibration() {
+if (!STATES.browser.capabilities.deviceVibration) return;
+let mode = getStreamPref("deviceVibration.mode"), intensity = 0;
+if (mode === "on" || mode === "auto" && !hasGamepad()) intensity = getStreamPref("deviceVibration.intensity") / 100;
+StreamSettings.settings.deviceVibrationIntensity = intensity, BxEventBus.Stream.emit("deviceVibration.updated", {});
+}
+static async refreshMkbSettings() {
+let settings = StreamSettings.settings, presetId = getStreamPref("mkb.p1.preset.mappingId"), orgPreset = await MkbMappingPresetsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data, converted = {
+mapping: {},
+mouse: Object.assign({}, orgPresetData.mouse)
+}, key;
+for (key in orgPresetData.mapping) {
+let buttonIndex = parseInt(key);
+if (!orgPresetData.mapping[buttonIndex]) continue;
+for (let keyName of orgPresetData.mapping[buttonIndex])
+if (typeof keyName === "string") converted.mapping[keyName] = buttonIndex;
+}
+let mouse = converted.mouse;
+mouse["sensitivityX"] *= 0.001, mouse["sensitivityY"] *= 0.001, mouse["deadzoneCounterweight"] *= 0.01, settings.mkbPreset = converted, setStreamPref("mkb.p1.preset.mappingId", orgPreset.id, "direct"), BxEventBus.Stream.emit("mkb.setting.updated", {});
+}
+static async refreshKeyboardShortcuts() {
+let settings = StreamSettings.settings, presetId = getStreamPref("keyboardShortcuts.preset.inGameId");
+if (presetId === 0) {
+settings.keyboardShortcuts = null, setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
+return;
+}
+let orgPreset = await KeyboardShortcutsTable.getInstance().getPreset(presetId), orgPresetData = orgPreset.data.mapping, converted = {}, action;
+for (action in orgPresetData) {
+let info = orgPresetData[action], key = `${info.code}:${info.modifiers || 0}`;
+converted[key] = action;
+}
+settings.keyboardShortcuts = converted, setStreamPref("keyboardShortcuts.preset.inGameId", orgPreset.id, "direct"), BxEventBus.Stream.emit("keyboardShortcuts.updated", {});
+}
+static async refreshAllSettings() {
+window.BX_STREAM_SETTINGS = StreamSettings.settings, await StreamSettings.refreshControllerSettings(), await StreamSettings.refreshMkbSettings(), await StreamSettings.refreshKeyboardShortcuts();
+}
+static findKeyboardShortcut(targetAction) {
+let shortcuts = StreamSettings.settings.keyboardShortcuts;
+for (let codeStr in shortcuts)
+if (shortcuts[codeStr] === targetAction) return KeyHelper.parseFullKeyCode(codeStr);
+return null;
+}
+static setup() {
+let listener = () => {
+StreamSettings.refreshControllerSettings();
+};
+window.addEventListener("gamepadconnected", listener), window.addEventListener("gamepaddisconnected", listener), StreamSettings.refreshAllSettings();
+}
}
class BxNumberStepper extends HTMLInputElement {
- intervalId = null;
- isHolding;
- controlValue;
- controlMin;
- controlMax;
- uiMin;
- uiMax;
- steps;
- options;
- onChange;
- $text;
- $btnInc;
- $btnDec;
- $range;
- onRangeInput;
- onClick;
- onPointerUp;
- onPointerDown;
- setValue;
- normalizeValue;
- static create(key, value, min, max, options = {}, onChange) {
- options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
- let $text, $btnInc, $btnDec, $range, self = CE("div", {
- class: "bx-number-stepper",
- id: `bx_setting_${escapeCssSelector(key)}`
- }, CE("div", !1, $btnDec = CE("button", {
- _dataset: {
- type: "dec"
- },
- type: "button",
- class: options.hideSlider ? "bx-focusable" : "",
- tabindex: options.hideSlider ? 0 : -1
- }, "-"), $text = CE("span"), $btnInc = CE("button", {
- _dataset: {
- type: "inc"
- },
- type: "button",
- class: options.hideSlider ? "bx-focusable" : "",
- tabindex: options.hideSlider ? 0 : -1
- }, "+")));
- if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self;
- if ($range = CE("input", {
- id: `bx_inp_setting_${key}`,
- type: "range",
- min: self.uiMin,
- max: self.uiMax,
- value: options.reverse ? -value : value,
- step: self.steps,
- tabindex: 0
- }), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) {
- let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });
- if ($range.setAttribute("list", markersId), options.exactTicks) {
- let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks;
- if (start === min) start += options.exactTicks;
- for (let i = start;i < max; i += options.exactTicks)
- $markers.appendChild(CE("option", {
- value: options.reverse ? -i : i
- }));
- } else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks)
- $markers.appendChild(CE("option", { value: i }));
- self.appendChild($markers);
- }
- return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, {
- focus: options.hideSlider ? $btnInc : $range
- }), Object.defineProperty(self, "value", {
- get() {
- return self.controlValue;
- },
- set(value2) {
- BxNumberStepper.setValue.call(self, value2);
- }
- }), Object.defineProperty(self, "disabled", {
- get() {
- return $range.disabled;
- },
- set(value2) {
- $btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2;
- }
- }), self;
- }
- static setValue(value) {
- if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString();
- BxNumberStepper.updateButtonsVisibility.call(this);
- }
- static normalizeValue(value) {
- return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value;
- }
- static onRangeInput(e) {
- let value = parseInt(e.target.value);
- if (this.options.reverse) value *= -1;
- if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value);
- }
- static onClick(e) {
- if (e.preventDefault(), this.isHolding) return;
- let $btn = e.target.closest("button");
- $btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
- }
- static onPointerDown(e) {
- BxNumberStepper.clearIntervalId.call(this);
- let $btn = e.target.closest("button");
- if (!$btn) return;
- this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => {
- BxNumberStepper.buttonPressed.call(this, e2, $btn);
- }, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 });
- }
- static onPointerUp(e) {
- BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
- }
- static onContextMenu(e) {
- e.preventDefault();
- }
- static updateTextValue() {
- let value = this.controlValue, textContent = null;
- if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);
- if (textContent === null) textContent = value.toString() + this.options.suffix;
- return textContent;
- }
- static buttonPressed(e, $btn) {
- BxNumberStepper.change.call(this, $btn.dataset.type);
- }
- static change(direction) {
- let value = this.controlValue;
- if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps);
- else value = Math.min(this.uiMax, value + this.steps);
- value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue);
- }
- static clearIntervalId() {
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
- }
- static updateButtonsVisibility() {
- if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this);
- }
+intervalId = null;
+isHolding;
+controlValue;
+controlMin;
+controlMax;
+uiMin;
+uiMax;
+steps;
+options;
+onChange;
+$text;
+$btnInc;
+$btnDec;
+$range;
+onRangeInput;
+onClick;
+onPointerUp;
+onPointerDown;
+setValue;
+normalizeValue;
+static create(key, value, min, max, options = {}, onChange) {
+options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
+let $text, $btnInc, $btnDec, $range, self = CE("div", {
+class: "bx-number-stepper",
+id: `bx_setting_${escapeCssSelector(key)}`
+}, CE("div", !1, $btnDec = CE("button", {
+_dataset: {
+type: "dec"
+},
+type: "button",
+class: options.hideSlider ? "bx-focusable" : "",
+tabindex: options.hideSlider ? 0 : -1
+}, "-"), $text = CE("span"), $btnInc = CE("button", {
+_dataset: {
+type: "inc"
+},
+type: "button",
+class: options.hideSlider ? "bx-focusable" : "",
+tabindex: options.hideSlider ? 0 : -1
+}, "+")));
+if (self.$text = $text, self.$btnInc = $btnInc, self.$btnDec = $btnDec, self.onChange = onChange, self.onRangeInput = BxNumberStepper.onRangeInput.bind(self), self.onClick = BxNumberStepper.onClick.bind(self), self.onPointerUp = BxNumberStepper.onPointerUp.bind(self), self.onPointerDown = BxNumberStepper.onPointerDown.bind(self), self.controlMin = min, self.controlMax = max, self.isHolding = !1, self.options = options, self.uiMin = options.reverse ? -max : min, self.uiMax = options.reverse ? -min : max, self.steps = Math.max(options.steps || 1, 1), BxNumberStepper.setValue.call(self, value), options.disabled) return $btnInc.disabled = !0, $btnInc.classList.add("bx-inactive"), $btnDec.disabled = !0, $btnDec.classList.add("bx-inactive"), self.disabled = !0, self;
+if ($range = CE("input", {
+id: `bx_inp_setting_${key}`,
+type: "range",
+min: self.uiMin,
+max: self.uiMax,
+value: options.reverse ? -value : value,
+step: self.steps,
+tabindex: 0
+}), self.$range = $range, options.hideSlider && $range.classList.add("bx-gone"), self.addEventListener("input", self.onRangeInput), self.appendChild($range), options.ticks || options.exactTicks) {
+let markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });
+if ($range.setAttribute("list", markersId), options.exactTicks) {
+let start = Math.max(Math.floor(min / options.exactTicks), 1) * options.exactTicks;
+if (start === min) start += options.exactTicks;
+for (let i = start;i < max; i += options.exactTicks)
+$markers.appendChild(CE("option", {
+value: options.reverse ? -i : i
+}));
+} else for (let i = self.uiMin + options.ticks;i < self.uiMax; i += options.ticks)
+$markers.appendChild(CE("option", { value: i }));
+self.appendChild($markers);
}
+return BxNumberStepper.updateButtonsVisibility.call(self), self.addEventListener("click", self.onClick), self.addEventListener("pointerdown", self.onPointerDown), self.addEventListener("contextmenu", BxNumberStepper.onContextMenu), setNearby(self, {
+focus: options.hideSlider ? $btnInc : $range
+}), Object.defineProperty(self, "value", {
+get() {
+return self.controlValue;
+},
+set(value2) {
+BxNumberStepper.setValue.call(self, value2);
+}
+}), Object.defineProperty(self, "disabled", {
+get() {
+return $range.disabled;
+},
+set(value2) {
+$btnDec.disabled = value2, $btnInc.disabled = value2, $range.disabled = value2;
+}
+}), self;
+}
+static setValue(value) {
+if (this.controlValue = BxNumberStepper.normalizeValue.call(this, value), this.$text.textContent = BxNumberStepper.updateTextValue.call(this), this.$range) this.$range.value = (this.options.reverse ? -this.controlValue : this.controlValue).toString();
+BxNumberStepper.updateButtonsVisibility.call(this);
+}
+static normalizeValue(value) {
+return value = parseInt(value), value = Math.max(this.controlMin, value), value = Math.min(this.controlMax, value), value;
+}
+static onRangeInput(e) {
+let value = parseInt(e.target.value);
+if (this.options.reverse) value *= -1;
+if (BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), !e.ignoreOnChange && this.onChange) this.onChange(e, value);
+}
+static onClick(e) {
+if (e.preventDefault(), this.isHolding) return;
+let $btn = e.target.closest("button");
+$btn && BxNumberStepper.buttonPressed.call(this, e, $btn), BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
+}
+static onPointerDown(e) {
+BxNumberStepper.clearIntervalId.call(this);
+let $btn = e.target.closest("button");
+if (!$btn) return;
+this.isHolding = !0, e.preventDefault(), this.intervalId = window.setInterval((e2) => {
+BxNumberStepper.buttonPressed.call(this, e2, $btn);
+}, 200), window.addEventListener("pointerup", this.onPointerUp, { once: !0 }), window.addEventListener("pointercancel", this.onPointerUp, { once: !0 });
+}
+static onPointerUp(e) {
+BxNumberStepper.clearIntervalId.call(this), this.isHolding = !1;
+}
+static onContextMenu(e) {
+e.preventDefault();
+}
+static updateTextValue() {
+let value = this.controlValue, textContent = null;
+if (this.options.customTextValue) textContent = this.options.customTextValue(value, this.controlMin, this.controlMax);
+if (textContent === null) textContent = value.toString() + this.options.suffix;
+return textContent;
+}
+static buttonPressed(e, $btn) {
+BxNumberStepper.change.call(this, $btn.dataset.type);
+}
+static change(direction) {
+let value = this.controlValue;
+if (value = this.options.reverse ? -value : value, direction === "dec") value = Math.max(this.uiMin, value - this.steps);
+else value = Math.min(this.uiMax, value + this.steps);
+value = this.options.reverse ? -value : value, BxNumberStepper.setValue.call(this, value), BxNumberStepper.updateButtonsVisibility.call(this), this.onChange && this.onChange(null, this.controlValue);
+}
+static clearIntervalId() {
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
+}
+static updateButtonsVisibility() {
+if (this.$btnDec.classList.toggle("bx-inactive", this.controlValue === this.uiMin), this.$btnInc.classList.toggle("bx-inactive", this.controlValue === this.uiMax), this.controlValue === this.uiMin || this.controlValue === this.uiMax) BxNumberStepper.clearIntervalId.call(this);
+}
+}
+class SettingElement {
+static renderOptions(key, setting, currentValue, onChange) {
+let $control = CE("select", {
+tabindex: 0
+}), $parent;
+if (setting.optionsGroup) $parent = CE("optgroup", {
+label: setting.optionsGroup
+}), $control.appendChild($parent);
+else $parent = $control;
+for (let value in setting.options) {
+let label = setting.options[value], $option = CE("option", { value }, label);
+$parent.appendChild($option);
+}
+return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {
+let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
+!e.ignoreOnChange && onChange(e, value);
+}), $control.setValue = (value) => {
+$control.value = value;
+}, $control;
+}
+static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
+let $control = CE("select", {
+multiple: !0,
+tabindex: 0
+}), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;
+$control.setAttribute("size", size.toString());
+for (let value in setting.multipleOptions) {
+let label = setting.multipleOptions[value], $option = CE("option", { value }, label);
+$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {
+e.preventDefault();
+let target = e.target;
+target.selected = !target.selected;
+let $parent = target.parentElement;
+$parent.focus(), BxEvent.dispatch($parent, "input");
+}), $control.appendChild($option);
+}
+return $control.addEventListener("mousedown", function(e) {
+let self = this, orgScrollTop = self.scrollTop;
+window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
+}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {
+let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);
+!e.ignoreOnChange && onChange(e, values);
+}), Object.defineProperty($control, "value", {
+get() {
+return Array.from($control.options).filter((option) => option.selected).map((option) => option.value);
+},
+set(value) {
+let values = value.split(",");
+Array.from($control.options).forEach((option) => {
+option.selected = values.includes(option.value);
+});
+}
+}), $control;
+}
+static renderCheckbox(key, setting, currentValue, onChange) {
+let $control = CE("input", { type: "checkbox", tabindex: 0 });
+return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {
+!e.ignoreOnChange && onChange(e, e.target.checked);
+}), $control.setValue = (value) => {
+$control.checked = !!value;
+}, $control;
+}
+static renderNumberStepper(key, setting, value, onChange, options = {}) {
+return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);
+}
+static METHOD_MAP = {
+options: SettingElement.renderOptions,
+"multiple-options": SettingElement.renderMultipleOptions,
+"number-stepper": SettingElement.renderNumberStepper,
+checkbox: SettingElement.renderCheckbox
+};
+static render(type, key, setting, currentValue, onChange, options) {
+let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));
+if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;
+if (type === "options" || type === "multiple-options") $control.name = $control.id;
+return $control;
+}
+static fromPref(key, onChange, overrideParams = {}) {
+let { definition, storage } = getPrefInfo(key);
+if (!definition) return null;
+let currentValue = storage.getSetting(key), type;
+if ("options" in definition) type = "options";
+else if ("multipleOptions" in definition) type = "multiple-options";
+else if (typeof definition.default === "number") type = "number-stepper";
+else type = "checkbox";
+let params = {};
+if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});
+if (params.disabled) currentValue = definition.default;
+return SettingElement.render(type, key, definition, currentValue, (e, value) => {
+if (isGlobalPref(key)) setGlobalPref(key, value, "ui");
+else {
+let id = SettingsManager.getInstance().getTargetGameId();
+setGamePref(id, key, value, "ui");
+}
+onChange && onChange(e, value);
+}, params);
+}
+}
+class BxSelectElement extends HTMLSelectElement {
+isControllerFriendly;
+optionsList;
+indicatorsList;
+$indicators;
+visibleIndex;
+isMultiple;
+$select;
+$btnNext;
+$btnPrev;
+$label;
+$checkBox;
+static create($select, forceFriendly = !1) {
+let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly");
+if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;
+$select.removeAttribute("tabindex");
+let $wrapper = CE("div", {
+class: "bx-select",
+_dataset: {
+controllerFriendly: isControllerFriendly
+}
+});
+if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");
+let $content, self = $wrapper;
+self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];
+let $btnPrev, $btnNext;
+if (isControllerFriendly) {
+$btnPrev = createButton({
+label: "<",
+style: 64
+}), $btnNext = createButton({
+label: ">",
+style: 64
+}), setNearby($wrapper, {
+orientation: "horizontal",
+focus: $btnNext
+}), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;
+let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);
+$btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);
+} else $select.addEventListener("change", (e) => {
+self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+});
+if (self.isMultiple) $content = CE("button", {
+class: "bx-select-value bx-focusable",
+tabindex: 0
+}, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {
+self.$checkBox.click();
+}), self.$checkBox.addEventListener("input", (e) => {
+let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);
+$option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");
+});
+else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);
+return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {
+mutationList.forEach((mutation) => {
+if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+});
+}).observe($select, {
+subtree: !0,
+childList: !0,
+attributes: !0
+}), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {
+get() {
+return $select.value;
+},
+set(value) {
+self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
+}
+}), Object.defineProperty(self, "disabled", {
+get() {
+return $select.disabled;
+},
+set(value) {
+$select.disabled = value;
+}
+}), self.addEventListener = function() {
+$select.addEventListener.apply($select, arguments);
+}, self.removeEventListener = function() {
+$select.removeEventListener.apply($select, arguments);
+}, self.dispatchEvent = function() {
+return $select.dispatchEvent.apply($select, arguments);
+}, self.appendChild = function(node) {
+return $select.appendChild(node), node;
+}, self;
+}
+static resetIndicators() {
+let {
+optionsList,
+indicatorsList,
+$indicators
+} = this, targetSize = optionsList.length;
+if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)
+indicatorsList.pop()?.remove();
+else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {
+let $indicator = CE("span", {});
+indicatorsList.push($indicator), $indicators.appendChild($indicator);
+}
+for (let $indicator of indicatorsList)
+clearDataSet($indicator);
+$indicators.classList.toggle("bx-invisible", targetSize <= 1);
+}
+static getOptionAtIndex(index) {
+return this.optionsList[index];
+}
+static render(e) {
+let {
+$label,
+$btnNext,
+$btnPrev,
+$checkBox,
+optionsList,
+indicatorsList
+} = this;
+if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;
+this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);
+let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";
+if ($option) {
+let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");
+if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {
+let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";
+$label.innerHTML = "";
+let fragment = document.createDocumentFragment();
+fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);
+} else $label.textContent = content;
+} else $label.textContent = content;
+if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);
+let disableButtons = optionsList.length <= 1;
+$btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons);
+for (let i = 0;i < optionsList.length; i++) {
+let $option2 = optionsList[i], $indicator = indicatorsList[i];
+if (!$option2 || !$indicator) continue;
+if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";
+if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";
+}
+}
+static normalizeIndex(index) {
+return Math.min(Math.max(index, 0), this.optionsList.length - 1);
+}
+static onPrevNext(e) {
+if (!e.target) return;
+let {
+$btnNext,
+$select,
+isMultiple,
+visibleIndex: currentIndex
+} = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;
+if (newIndex > this.optionsList.length - 1) newIndex = 0;
+else if (newIndex < 0) newIndex = this.optionsList.length - 1;
+if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;
+if (isMultiple) BxSelectElement.render.call(this);
+else BxEvent.dispatch($select, "input");
+}
+}
+class XboxApi {
+static CACHED_TITLES = {};
+static async getProductTitle(xboxTitleId) {
+if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];
+try {
+let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;
+return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle;
+} catch (e) {}
+return;
+}
+}
+class SettingsManager {
+static instance;
+static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);
+$streamSettingsSelection;
+$tips;
+playingGameId = -1;
+targetGameId = -1;
+SETTINGS = {
+"localCoOp.enabled": {
+onChange: () => {
+BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));
+}
+},
+"deviceVibration.mode": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"deviceVibration.intensity": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"controller.pollingRate": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"controller.settings": {
+onChange: StreamSettings.refreshControllerSettings
+},
+"nativeMkb.scroll.sensitivityX": {
+onChange: () => {
+let value = getStreamPref("nativeMkb.scroll.sensitivityX");
+NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);
+}
+},
+"nativeMkb.scroll.sensitivityY": {
+onChange: () => {
+let value = getStreamPref("nativeMkb.scroll.sensitivityY");
+NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);
+}
+},
+"video.player.type": {
+onChange: () => {
+if (onChangeVideoPlayerType(), STATES.isPlaying) updateVideoPlayer();
+},
+alwaysTriggerOnChange: !0
+},
+"video.player.powerPreference": {
+onChange: () => {
+let streamPlayer = STATES.currentStream.streamPlayer;
+if (!streamPlayer) return;
+streamPlayer.reloadPlayer(), updateVideoPlayer();
+}
+},
+"video.processing": {
+onChange: updateVideoPlayer
+},
+"video.processing.sharpness": {
+onChange: updateVideoPlayer
+},
+"video.maxFps": {
+onChange: () => {
+let value = getStreamPref("video.maxFps");
+limitVideoPlayerFps(value);
+}
+},
+"video.ratio": {
+onChange: updateVideoPlayer
+},
+"video.brightness": {
+onChange: updateVideoPlayer
+},
+"video.contrast": {
+onChange: updateVideoPlayer
+},
+"video.saturation": {
+onChange: updateVideoPlayer
+},
+"video.position": {
+onChange: updateVideoPlayer
+},
+"audio.volume": {
+onChange: () => {
+let value = getStreamPref("audio.volume");
+SoundShortcut.setGainNodeVolume(value);
+}
+},
+"stats.items": {
+onChange: StreamStats.refreshStyles
+},
+"stats.quickGlance.enabled": {
+onChange: () => {
+let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();
+value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
+}
+},
+"stats.position": {
+onChange: StreamStats.refreshStyles
+},
+"stats.textSize": {
+onChange: StreamStats.refreshStyles
+},
+"stats.opacity.all": {
+onChange: StreamStats.refreshStyles
+},
+"stats.opacity.background": {
+onChange: StreamStats.refreshStyles
+},
+"stats.colors": {
+onChange: StreamStats.refreshStyles
+},
+"mkb.p1.preset.mappingId": {
+onChange: StreamSettings.refreshMkbSettings
+},
+"mkb.p1.slot": {
+onChange: () => {
+EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
+}
+},
+"keyboardShortcuts.preset.inGameId": {
+onChange: StreamSettings.refreshKeyboardShortcuts
+}
+};
+constructor() {
+BxEventBus.Stream.on("setting.changed", (data) => {
+if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);
+}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
+this.switchGameSettings(id);
+}), this.renderStreamSettingsSelection();
+}
+updateStreamElement(key, onChanges) {
+let info = this.SETTINGS[key];
+if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange);
+else info.onChange();
+let $elm = info.$element;
+if (!$elm) return;
+let value = getGamePref(this.targetGameId, key, !0);
+if ("setValue" in $elm) $elm.setValue(value);
+else $elm.value = value.toString();
+this.updateDataset($elm, key);
+}
+switchGameSettings(id) {
+if (setGameIdPref(id), this.targetGameId === id) return;
+let onChanges = new Set, oldGameId = this.targetGameId;
+this.targetGameId = id;
+let key;
+for (key in this.SETTINGS) {
+if (!isStreamPref(key)) continue;
+let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0);
+if (oldValue === newValue) continue;
+this.updateStreamElement(key, onChanges);
+}
+onChanges.forEach((onChange) => {
+onChange && onChange();
+}), this.$tips.classList.toggle("bx-gone", id < 0);
+}
+setElement(pref, $elm) {
+if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
+this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;
+}
+getElement(pref, params) {
+if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
+let $elm = this.SETTINGS[pref].$element;
+if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;
+return this.updateDataset($elm, pref), $elm;
+}
+hasElement(pref) {
+return !!this.SETTINGS[pref]?.$element;
+}
+updateDataset($elm, pref) {
+if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";
+else delete $elm.dataset.override;
+}
+renderStreamSettingsSelection() {
+this.$tips = CE("p", { class: "bx-gone" }, `⇐ Q ⟶: ${t("reset-highlighted-setting")}`);
+let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: t("settings-for") }, CE("option", { value: -1 }, t("all-games")))), !0);
+$select.addEventListener("input", (e) => {
+let id = parseInt($select.value);
+BxEventBus.Stream.emit("gameSettings.switched", { id });
+}), this.$streamSettingsSelection = CE("div", {
+class: "bx-stream-settings-selection bx-gone",
+_nearby: { orientation: "vertical" }
+}, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {
+this.playingGameId = id, setGameIdPref(id);
+let $optGroup = $select.querySelector("optgroup");
+while ($optGroup.childElementCount > 1)
+$optGroup.lastElementChild?.remove();
+if (id >= 0) {
+let title = id === 0 ? "Xbox" : await XboxApi.getProductTitle(id);
+$optGroup.appendChild(CE("option", {
+value: id
+}, title)), $select.value = id.toString();
+} else $select.value = "-1";
+BxEventBus.Stream.emit("gameSettings.switched", { id });
+});
+}
+getStreamSettingsSelection() {
+return this.$streamSettingsSelection;
+}
+getTargetGameId() {
+return this.targetGameId;
+}
+}
+function onChangeVideoPlayerType() {
+let playerType = getStreamPref("video.player.type"), settingsManager = SettingsManager.getInstance();
+if (!settingsManager.hasElement("video.processing")) return;
+let isDisabled = !1, $videoProcessing = settingsManager.getElement("video.processing"), $videoSharpness = settingsManager.getElement("video.processing.sharpness"), $videoPowerPreference = settingsManager.getElement("video.player.powerPreference"), $videoMaxFps = settingsManager.getElement("video.maxFps"), $optCas = $videoProcessing.querySelector(`option[value=${"cas"}]`);
+if (playerType === "webgl2") $optCas && ($optCas.disabled = !1);
+else if ($videoProcessing.value = "usm", setStreamPref("video.processing", "usm", "direct"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
+$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2");
+}
+function limitVideoPlayerFps(targetFps) {
+STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
+}
+function updateVideoPlayer() {
+let streamPlayer = STATES.currentStream.streamPlayer;
+if (!streamPlayer) return;
+limitVideoPlayerFps(getStreamPref("video.maxFps"));
+let options = {
+processing: getStreamPref("video.processing"),
+sharpness: getStreamPref("video.processing.sharpness"),
+saturation: getStreamPref("video.saturation"),
+contrast: getStreamPref("video.contrast"),
+brightness: getStreamPref("video.brightness")
+};
+streamPlayer.setPlayerType(getStreamPref("video.player.type")), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
+}
+window.addEventListener("resize", updateVideoPlayer);
class NavigationDialog {
- dialogManager;
- onMountedCallbacks = [];
- constructor() {
- this.dialogManager = NavigationDialogManager.getInstance();
- }
- isCancellable() {
- return !0;
- }
- isOverlayVisible() {
- return !0;
- }
- show(configs = {}, clearStack = !1) {
- if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();
- }
- hide() {
- NavigationDialogManager.getInstance().hide();
- }
- getFocusedElement() {
- let $activeElement = document.activeElement;
- if (!$activeElement) return null;
- if (this.$container.contains($activeElement)) return $activeElement;
- return null;
- }
- onBeforeMount(configs = {}) {}
- onMounted(configs = {}) {
- for (let callback of this.onMountedCallbacks)
- callback.call(this);
- }
- onBeforeUnmount() {}
- onUnmounted() {}
- handleKeyPress(key) {
- return !1;
- }
- handleGamepad(button) {
- return !1;
- }
+dialogManager;
+onMountedCallbacks = [];
+constructor() {
+this.dialogManager = NavigationDialogManager.getInstance();
+}
+isCancellable() {
+return !0;
+}
+isOverlayVisible() {
+return !0;
+}
+show(configs = {}, clearStack = !1) {
+if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();
+}
+hide() {
+NavigationDialogManager.getInstance().hide();
+}
+getFocusedElement() {
+let $activeElement = document.activeElement;
+if (!$activeElement) return null;
+if (this.$container.contains($activeElement)) return $activeElement;
+return null;
+}
+onBeforeMount(configs = {}) {}
+onMounted(configs = {}) {
+for (let callback of this.onMountedCallbacks)
+callback.call(this);
+}
+onBeforeUnmount() {}
+onUnmounted() {}
+handleKeyPress(key) {
+return !1;
+}
+handleGamepad(button) {
+return !1;
+}
}
class NavigationDialogManager {
- static instance;
- static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
- LOG_TAG = "NavigationDialogManager";
- static GAMEPAD_POLLING_INTERVAL = 50;
- static GAMEPAD_KEYS = [
- 0,
- 1,
- 2,
- 3,
- 12,
- 15,
- 13,
- 14,
- 4,
- 5,
- 6,
- 7,
- 10,
- 11,
- 8,
- 9
- ];
- static GAMEPAD_DIRECTION_MAP = {
- 12: 1,
- 13: 3,
- 14: 4,
- 15: 2,
- 100: 1,
- 101: 3,
- 102: 4,
- 103: 2
- };
- static SIBLING_PROPERTY_MAP = {
- horizontal: {
- 4: "previousElementSibling",
- 2: "nextElementSibling"
- },
- vertical: {
- 1: "previousElementSibling",
- 3: "nextElementSibling"
- }
- };
- gamepadPollingIntervalId = null;
- gamepadLastStates = [];
- gamepadHoldingIntervalId = null;
- $overlay;
- $container;
- dialog = null;
- dialogsStack = [];
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
- e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();
- }), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {
- if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;
- let $dialog = mutationList[0].addedNodes[0];
- if (!$dialog || !($dialog instanceof HTMLElement)) return;
- calculateSelectBoxes($dialog);
- }).observe(this.$container, { childList: !0 });
- }
- updateActiveInput(input) {
- document.documentElement.dataset.activeInput = input;
- }
- handleEvent(event) {
- switch (event.type) {
- case "keydown":
- this.updateActiveInput("keyboard");
- let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);
- if (handled) {
- event.preventDefault(), event.stopPropagation();
- return;
- }
- if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);
- else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
- if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
- } else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
- if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
- } else if (keyCode === "Escape") handled = !0, this.hide();
- if (handled) event.preventDefault(), event.stopPropagation();
- break;
- }
- }
- isShowing() {
- return this.$container && !this.$container.classList.contains("bx-gone");
- }
- pollGamepad = () => {
- let gamepads = window.navigator.getGamepads();
- for (let gamepad of gamepads) {
- if (!gamepad || !gamepad.connected) continue;
- if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
- let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;
- if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;
- if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;
- for (let key of NavigationDialogManager.GAMEPAD_KEYS)
- if (lastKey === key && !buttons[key].pressed) {
- releasedButton = key;
- break;
- } else if (buttons[key].pressed) {
- heldButton = key;
- break;
- }
- if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
- if (lastKey) {
- let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);
- if (releasedHorizontal || releasedVertical) releasedButton = lastKey;
- else heldButton = lastKey;
- } else if (axes[0] < -0.5) heldButton = 102;
- else if (axes[0] > 0.5) heldButton = 103;
- else if (axes[1] < -0.5) heldButton = 100;
- else if (axes[1] > 0.5) heldButton = 101;
- }
- if (heldButton !== null) {
- if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {
- let lastState2 = this.gamepadLastStates[gamepad.index];
- if (lastState2) {
- if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {
- this.handleGamepad(gamepad, heldButton);
- return;
- }
- }
- this.clearGamepadHoldingInterval();
- }, 100);
- continue;
- }
- if (releasedButton === null) {
- this.clearGamepadHoldingInterval();
- continue;
- }
- if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
- if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;
- if (releasedButton === 0) {
- document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
- return;
- } else if (releasedButton === 1) {
- this.hide();
- return;
- }
- }
- };
- handleGamepad(gamepad, key) {
- let handled = this.dialog?.handleGamepad(key);
- if (handled) return !0;
- let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];
- if (!direction) return !1;
- if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {
- let $range = document.activeElement;
- if (direction === 4 || direction === 2) {
- let $numberStepper = $range.closest(".bx-number-stepper");
- if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");
- else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));
- handled = !0;
- }
- }
- if (!handled) this.focusDirection(direction);
- return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;
- }
- clearGamepadHoldingInterval() {
- this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;
- }
- show(dialog, configs = {}, clearStack = !1) {
- this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();
- }
- hide() {
- if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;
- if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {
- let dialogIndex = this.dialogsStack.indexOf(this.dialog);
- if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
- }
- if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();
- }
- focus($elm) {
- if (!$elm) return !1;
- if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);
- else return $elm.nearby.focus();
- return $elm.focus(), $elm === document.activeElement;
- }
- getOrientation($elm) {
- let nearby = $elm.nearby || {};
- if (nearby.selfOrientation) return nearby.selfOrientation;
- let orientation, $current = $elm.parentElement;
- while ($current !== this.$container) {
- let tmp = $current.nearby?.orientation;
- if ($current.nearby && tmp) {
- orientation = tmp;
- break;
- }
- $current = $current.parentElement;
- }
- return orientation = orientation || "vertical", setNearby($elm, {
- selfOrientation: orientation
- }), orientation;
- }
- findNextTarget($focusing, direction, checkParent = !1, checked = []) {
- if (!$focusing || $focusing === this.$container) return null;
- if (checked.includes($focusing)) return null;
- checked.push($focusing);
- let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);
- if (nearby[1] && direction === 1) return nearby[1];
- else if (nearby[3] && direction === 3) return nearby[3];
- else if (nearby[4] && direction === 4) return nearby[4];
- else if (nearby[2] && direction === 2) return nearby[2];
- let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];
- if (siblingProperty) {
- let $sibling = $target;
- while ($sibling[siblingProperty]) {
- $sibling = $sibling[siblingProperty];
- let $focusable = this.findFocusableElement($sibling, direction);
- if ($focusable) return $focusable;
- }
- }
- if (nearby.loop) {
- if (nearby.loop(direction)) return null;
- }
- if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);
- return null;
- }
- findFocusableElement($elm, direction) {
- if (!$elm) return null;
- if (!!$elm.disabled) return null;
- if (!isElementVisible($elm)) return null;
- if ($elm.tabIndex > -1) return $elm;
- let focus = $elm.nearby?.focus;
- if (focus) {
- if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);
- else if (typeof focus === "function") {
- if (focus()) return document.activeElement;
- }
- }
- let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";
- if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();
- for (let $child of children) {
- if (!$child || !($child instanceof HTMLElement)) return null;
- let $target = this.findFocusableElement($child, direction);
- if ($target) return $target;
- }
- return null;
- }
- startGamepadPolling() {
- this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
- }
- stopGamepadPolling() {
- this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;
- }
- focusDirection(direction) {
- let dialog = this.dialog;
- if (!dialog) return;
- let $focusing = dialog.getFocusedElement();
- if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;
- let $target = this.findNextTarget($focusing, direction, !0);
- this.focus($target);
- }
- unmountCurrentDialog() {
- let dialog = this.dialog;
- dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;
- }
+static instance;
+static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
+LOG_TAG = "NavigationDialogManager";
+static GAMEPAD_POLLING_INTERVAL = 50;
+static GAMEPAD_KEYS = [
+0,
+1,
+2,
+3,
+12,
+15,
+13,
+14,
+4,
+5,
+6,
+7,
+10,
+11,
+8,
+9
+];
+static GAMEPAD_DIRECTION_MAP = {
+12: 1,
+13: 3,
+14: 4,
+15: 2,
+100: 1,
+101: 3,
+102: 4,
+103: 2
+};
+static SIBLING_PROPERTY_MAP = {
+horizontal: {
+4: "previousElementSibling",
+2: "nextElementSibling"
+},
+vertical: {
+1: "previousElementSibling",
+3: "nextElementSibling"
+}
+};
+gamepadPollingIntervalId = null;
+gamepadLastStates = [];
+gamepadHoldingIntervalId = null;
+$overlay;
+$container;
+dialog = null;
+dialogsStack = [];
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {
+e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();
+}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {
+if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;
+let $dialog = mutationList[0].addedNodes[0];
+if (!$dialog || !($dialog instanceof HTMLElement)) return;
+calculateSelectBoxes($dialog);
+}).observe(this.$container, { childList: !0 });
+}
+updateActiveInput(input) {
+document.documentElement.dataset.activeInput = input;
+}
+handleEvent(event) {
+switch (event.type) {
+case "keydown":
+this.updateActiveInput("keyboard");
+let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);
+if (handled) {
+event.preventDefault(), event.stopPropagation();
+return;
+}
+if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);
+else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {
+if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);
+} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {
+if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
+} else if (keyCode === "Escape") handled = !0, this.hide();
+if (handled) event.preventDefault(), event.stopPropagation();
+break;
+}
+}
+isShowing() {
+return this.$container && !this.$container.classList.contains("bx-gone");
+}
+pollGamepad = () => {
+let gamepads = window.navigator.getGamepads();
+for (let gamepad of gamepads) {
+if (!gamepad || !gamepad.connected) continue;
+if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;
+let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;
+if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;
+if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;
+for (let key of NavigationDialogManager.GAMEPAD_KEYS)
+if (lastKey === key && !buttons[key].pressed) {
+releasedButton = key;
+break;
+} else if (buttons[key].pressed) {
+heldButton = key;
+break;
+}
+if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {
+if (lastKey) {
+let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);
+if (releasedHorizontal || releasedVertical) releasedButton = lastKey;
+else heldButton = lastKey;
+} else if (axes[0] < -0.5) heldButton = 102;
+else if (axes[0] > 0.5) heldButton = 103;
+else if (axes[1] < -0.5) heldButton = 100;
+else if (axes[1] > 0.5) heldButton = 101;
+}
+if (heldButton !== null) {
+if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {
+let lastState2 = this.gamepadLastStates[gamepad.index];
+if (lastState2) {
+if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {
+this.handleGamepad(gamepad, heldButton);
+return;
+}
+}
+this.clearGamepadHoldingInterval();
+}, 100);
+continue;
+}
+if (releasedButton === null) {
+this.clearGamepadHoldingInterval();
+continue;
+}
+if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;
+if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;
+if (releasedButton === 0) {
+document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));
+return;
+} else if (releasedButton === 1) {
+this.hide();
+return;
+}
+}
+};
+handleGamepad(gamepad, key) {
+let handled = this.dialog?.handleGamepad(key);
+if (handled) return !0;
+let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];
+if (!direction) return !1;
+if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {
+let $range = document.activeElement;
+if (direction === 4 || direction === 2) {
+let $numberStepper = $range.closest(".bx-number-stepper");
+if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");
+else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));
+handled = !0;
+}
+}
+if (!handled) this.focusDirection(direction);
+return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;
+}
+clearGamepadHoldingInterval() {
+this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;
+}
+show(dialog, configs = {}, clearStack = !1) {
+this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();
+}
+hide() {
+if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;
+if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {
+let dialogIndex = this.dialogsStack.indexOf(this.dialog);
+if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);
+}
+if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();
+}
+focus($elm) {
+if (!$elm) return !1;
+if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);
+else return $elm.nearby.focus();
+return $elm.focus(), $elm === document.activeElement;
+}
+getOrientation($elm) {
+let nearby = $elm.nearby || {};
+if (nearby.selfOrientation) return nearby.selfOrientation;
+let orientation, $current = $elm.parentElement;
+while ($current !== this.$container) {
+let tmp = $current.nearby?.orientation;
+if ($current.nearby && tmp) {
+orientation = tmp;
+break;
+}
+$current = $current.parentElement;
+}
+return orientation = orientation || "vertical", setNearby($elm, {
+selfOrientation: orientation
+}), orientation;
+}
+findNextTarget($focusing, direction, checkParent = !1, checked = []) {
+if (!$focusing || $focusing === this.$container) return null;
+if (checked.includes($focusing)) return null;
+checked.push($focusing);
+let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);
+if (nearby[1] && direction === 1) return nearby[1];
+else if (nearby[3] && direction === 3) return nearby[3];
+else if (nearby[4] && direction === 4) return nearby[4];
+else if (nearby[2] && direction === 2) return nearby[2];
+let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];
+if (siblingProperty) {
+let $sibling = $target;
+while ($sibling[siblingProperty]) {
+$sibling = $sibling[siblingProperty];
+let $focusable = this.findFocusableElement($sibling, direction);
+if ($focusable) return $focusable;
+}
+}
+if (nearby.loop) {
+if (nearby.loop(direction)) return null;
+}
+if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);
+return null;
+}
+findFocusableElement($elm, direction) {
+if (!$elm) return null;
+if (!!$elm.disabled) return null;
+if (!isElementVisible($elm)) return null;
+if ($elm.tabIndex > -1) return $elm;
+let focus = $elm.nearby?.focus;
+if (focus) {
+if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);
+else if (typeof focus === "function") {
+if (focus()) return document.activeElement;
+}
+}
+let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";
+if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();
+for (let $child of children) {
+if (!$child || !($child instanceof HTMLElement)) return null;
+let $target = this.findFocusableElement($child, direction);
+if ($target) return $target;
+}
+return null;
+}
+startGamepadPolling() {
+this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);
+}
+stopGamepadPolling() {
+this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;
+}
+focusDirection(direction) {
+let dialog = this.dialog;
+if (!dialog) return;
+let $focusing = dialog.getFocusedElement();
+if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;
+let $target = this.findNextTarget($focusing, direction, !0);
+this.focus($target);
+}
+unmountCurrentDialog() {
+let dialog = this.dialog;
+dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;
+}
}
var LOG_TAG = "TouchController";
class TouchController {
- static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", {
- data: JSON.stringify({
- content: '{"layoutId":""}',
- target: "/streaming/touchcontrols/showlayoutv2",
- type: "Message"
- }),
- origin: "better-xcloud"
- });
- static #$style;
- static #enabled = !1;
- static #dataChannel;
- static #customLayouts = {};
- static #baseCustomLayouts = {};
- static #currentLayoutId;
- static #customList;
- static #xboxTitleId = null;
- static setXboxTitleId(xboxTitleId) {
- TouchController.#xboxTitleId = xboxTitleId;
- }
- static getCustomLayouts() {
- let xboxTitleId = TouchController.#xboxTitleId;
- if (!xboxTitleId) return null;
- return TouchController.#customLayouts[xboxTitleId];
- }
- static enable() {
- TouchController.#enabled = !0;
- }
- static disable() {
- TouchController.#enabled = !1;
- }
- static isEnabled() {
- return TouchController.#enabled;
- }
- static #showDefault() {
- TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);
- }
- static #show() {
- document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");
- }
- static toggleVisibility() {
- if (!TouchController.#dataChannel) return !1;
- let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement;
- if (!$container) return !1;
- return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen");
- }
- static reset() {
- TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = "");
- }
- static #dispatchMessage(msg) {
- TouchController.#dataChannel && window.setTimeout(() => {
- TouchController.#dataChannel.dispatchEvent(msg);
- }, 10);
- }
- static #dispatchLayouts(data) {
- TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
- }
- static async requestCustomLayouts(retries = 1) {
- let xboxTitleId = TouchController.#xboxTitleId;
- if (!xboxTitleId) return;
- if (xboxTitleId in TouchController.#customLayouts) {
- TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);
- return;
- }
- if (retries = retries || 1, retries > 2) {
- TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);
- return;
- }
- try {
- let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {};
- json.layouts.forEach(async (layoutName) => {
- let baseLayouts = {};
- if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName];
- else try {
- let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);
- baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;
- } catch (e) {}
- Object.assign(layouts, baseLayouts);
- }), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);
- } catch (e) {
- TouchController.requestCustomLayouts(retries + 1);
- }
- }
- static applyCustomLayout(layoutId, delay = 0) {
- if (!window.BX_EXPOSED.touchLayoutManager) {
- let listener = (e) => {
- if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0);
- };
- window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 });
- return;
- }
- let xboxTitleId = TouchController.#xboxTitleId;
- if (!xboxTitleId) {
- BxLogger.error(LOG_TAG, "Invalid xboxTitleId");
- return;
- }
- if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;
- if (!layoutId) {
- BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault();
- return;
- }
- let layoutChanged = TouchController.#currentLayoutId !== layoutId;
- TouchController.#currentLayoutId = layoutId;
- let layoutData = TouchController.#customLayouts[xboxTitleId];
- if (!xboxTitleId || !layoutId || !layoutData) {
- TouchController.#enabled && TouchController.#showDefault();
- return;
- }
- let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];
- if (!layout) return;
- let msg, html = !1;
- if (layout.author) {
- let author = `${escapeHtml(layout.author)}`;
- msg = t("touch-control-layout-by", { name: author }), html = !0;
- } else msg = t("touch-control-layout");
- layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => {
- window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({
- type: "showLayout",
- scope: xboxTitleId,
- subscope: "base",
- layout: {
- id: "System.Standard",
- displayName: "System",
- layoutFile: layout
- }
- });
- }, delay);
- }
- static updateCustomList() {
- TouchController.#customList = GhPagesUtils.getTouchControlCustomList();
- }
- static getCustomList() {
- return TouchController.#customList;
- }
- static setup() {
- window.testTouchLayout = (layout) => {
- let { touchLayoutManager } = window.BX_EXPOSED;
- touchLayoutManager && touchLayoutManager.changeLayoutForScope({
- type: "showLayout",
- scope: "" + TouchController.#xboxTitleId,
- subscope: "base",
- layout: {
- id: "System.Standard",
- displayName: "Custom",
- layoutFile: layout
- }
- });
- };
- let $style = document.createElement("style");
- document.documentElement.appendChild($style), TouchController.#$style = $style;
- let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom");
- BxEventBus.Stream.on("dataChannelCreated", (payload) => {
- let { dataChannel } = payload;
- if (dataChannel?.label !== "message") return;
- let filter = "";
- if (TouchController.#enabled) {
- if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)";
- else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)";
- } else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)";
- if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;
- else $style.textContent = "";
- TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {
- window.setTimeout(TouchController.#show, 1000);
- });
- let focused = !1;
- dataChannel.addEventListener("message", (msg) => {
- if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
- if (msg.data.includes("touchcontrols/showtitledefault")) {
- if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts();
- else TouchController.#showDefault();
- return;
- }
- try {
- if (msg.data.includes("/titleinfo")) {
- let json = JSON.parse(JSON.parse(msg.data).content);
- if (focused = json.focused, !json.focused) TouchController.#show();
- TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());
- }
- } catch (e) {
- BxLogger.error(LOG_TAG, "Load custom layout", e);
- }
- });
- });
- }
+static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", {
+data: JSON.stringify({
+content: '{"layoutId":""}',
+target: "/streaming/touchcontrols/showlayoutv2",
+type: "Message"
+}),
+origin: "better-xcloud"
+});
+static #$style;
+static #enabled = !1;
+static #dataChannel;
+static #customLayouts = {};
+static #baseCustomLayouts = {};
+static #currentLayoutId;
+static #customList;
+static #xboxTitleId = null;
+static setXboxTitleId(xboxTitleId) {
+TouchController.#xboxTitleId = xboxTitleId;
+}
+static getCustomLayouts() {
+let xboxTitleId = TouchController.#xboxTitleId;
+if (!xboxTitleId) return null;
+return TouchController.#customLayouts[xboxTitleId];
+}
+static enable() {
+TouchController.#enabled = !0;
+}
+static disable() {
+TouchController.#enabled = !1;
+}
+static isEnabled() {
+return TouchController.#enabled;
+}
+static #showDefault() {
+TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);
+}
+static #show() {
+document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");
+}
+static toggleVisibility() {
+if (!TouchController.#dataChannel) return !1;
+let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement;
+if (!$container) return !1;
+return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen");
+}
+static reset() {
+TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = "");
+}
+static #dispatchMessage(msg) {
+TouchController.#dataChannel && window.setTimeout(() => {
+TouchController.#dataChannel.dispatchEvent(msg);
+}, 10);
+}
+static #dispatchLayouts(data) {
+TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
+}
+static async requestCustomLayouts(retries = 1) {
+let xboxTitleId = TouchController.#xboxTitleId;
+if (!xboxTitleId) return;
+if (xboxTitleId in TouchController.#customLayouts) {
+TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);
+return;
+}
+if (retries = retries || 1, retries > 2) {
+TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);
+return;
+}
+try {
+let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {};
+json.layouts.forEach(async (layoutName) => {
+let baseLayouts = {};
+if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName];
+else try {
+let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);
+baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;
+} catch (e) {}
+Object.assign(layouts, baseLayouts);
+}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);
+} catch (e) {
+TouchController.requestCustomLayouts(retries + 1);
+}
+}
+static applyCustomLayout(layoutId, delay = 0) {
+if (!window.BX_EXPOSED.touchLayoutManager) {
+let listener = (e) => {
+if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0);
+};
+window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 });
+return;
+}
+let xboxTitleId = TouchController.#xboxTitleId;
+if (!xboxTitleId) {
+BxLogger.error(LOG_TAG, "Invalid xboxTitleId");
+return;
+}
+if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;
+if (!layoutId) {
+BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault();
+return;
+}
+let layoutChanged = TouchController.#currentLayoutId !== layoutId;
+TouchController.#currentLayoutId = layoutId;
+let layoutData = TouchController.#customLayouts[xboxTitleId];
+if (!xboxTitleId || !layoutId || !layoutData) {
+TouchController.#enabled && TouchController.#showDefault();
+return;
+}
+let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];
+if (!layout) return;
+let msg, html = !1;
+if (layout.author) {
+let author = `${escapeHtml(layout.author)}`;
+msg = t("touch-control-layout-by", { name: author }), html = !0;
+} else msg = t("touch-control-layout");
+layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => {
+window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({
+type: "showLayout",
+scope: xboxTitleId,
+subscope: "base",
+layout: {
+id: "System.Standard",
+displayName: "System",
+layoutFile: layout
+}
+});
+}, delay);
+}
+static updateCustomList() {
+TouchController.#customList = GhPagesUtils.getTouchControlCustomList();
+}
+static getCustomList() {
+return TouchController.#customList;
+}
+static setup() {
+window.testTouchLayout = (layout) => {
+let { touchLayoutManager } = window.BX_EXPOSED;
+touchLayoutManager && touchLayoutManager.changeLayoutForScope({
+type: "showLayout",
+scope: "" + TouchController.#xboxTitleId,
+subscope: "base",
+layout: {
+id: "System.Standard",
+displayName: "Custom",
+layoutFile: layout
+}
+});
+};
+let $style = document.createElement("style");
+document.documentElement.appendChild($style), TouchController.#$style = $style;
+let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom");
+BxEventBus.Stream.on("dataChannelCreated", (payload) => {
+let { dataChannel } = payload;
+if (dataChannel?.label !== "message") return;
+let filter = "";
+if (TouchController.#enabled) {
+if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)";
+else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)";
+} else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)";
+if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;
+else $style.textContent = "";
+TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {
+window.setTimeout(TouchController.#show, 1000);
+});
+let focused = !1;
+dataChannel.addEventListener("message", (msg) => {
+if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
+if (msg.data.includes("touchcontrols/showtitledefault")) {
+if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts();
+else TouchController.#showDefault();
+return;
+}
+try {
+if (msg.data.includes("/titleinfo")) {
+let json = JSON.parse(JSON.parse(msg.data).content);
+if (focused = json.focused, !json.focused) TouchController.#show();
+TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());
+}
+} catch (e) {
+BxLogger.error(LOG_TAG, "Load custom layout", e);
+}
+});
+});
}
-class BxSelectElement extends HTMLSelectElement {
- isControllerFriendly;
- optionsList;
- indicatorsList;
- $indicators;
- visibleIndex;
- isMultiple;
- $select;
- $btnNext;
- $btnPrev;
- $label;
- $checkBox;
- static create($select, forceFriendly = !1) {
- let isControllerFriendly = forceFriendly || getGlobalPref("ui.controllerFriendly");
- if ($select.multiple && !isControllerFriendly) return $select.classList.add("bx-select"), $select;
- $select.removeAttribute("tabindex");
- let $wrapper = CE("div", {
- class: "bx-select",
- _dataset: {
- controllerFriendly: isControllerFriendly
- }
- });
- if ($select.classList.contains("bx-full-width")) $wrapper.classList.add("bx-full-width");
- let $content, self = $wrapper;
- self.isControllerFriendly = isControllerFriendly, self.isMultiple = $select.multiple, self.visibleIndex = $select.selectedIndex, self.$select = $select, self.optionsList = Array.from($select.querySelectorAll("option")), self.$indicators = CE("div", { class: "bx-select-indicators" }), self.indicatorsList = [];
- let $btnPrev, $btnNext;
- if (isControllerFriendly) {
- $btnPrev = createButton({
- label: "<",
- style: 64
- }), $btnNext = createButton({
- label: ">",
- style: 64
- }), setNearby($wrapper, {
- orientation: "horizontal",
- focus: $btnNext
- }), self.$btnNext = $btnNext, self.$btnPrev = $btnPrev;
- let boundOnPrevNext = BxSelectElement.onPrevNext.bind(self);
- $btnPrev.addEventListener("click", boundOnPrevNext), $btnNext.addEventListener("click", boundOnPrevNext);
- } else $select.addEventListener("change", (e) => {
- self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- });
- if (self.isMultiple) $content = CE("button", {
- class: "bx-select-value bx-focusable",
- tabindex: 0
- }, CE("div", !1, self.$checkBox = CE("input", { type: "checkbox" }), self.$label = CE("span", !1, "")), self.$indicators), $content.addEventListener("click", (e) => {
- self.$checkBox.click();
- }), self.$checkBox.addEventListener("input", (e) => {
- let $option = BxSelectElement.getOptionAtIndex.call(self, self.visibleIndex);
- $option && ($option.selected = e.target.checked), BxEvent.dispatch($select, "input");
- });
- else $content = CE("div", !1, self.$label = CE("label", { for: $select.id + "_checkbox" }, ""), self.$indicators);
- return $select.addEventListener("input", BxSelectElement.render.bind(self)), new MutationObserver((mutationList, observer2) => {
- mutationList.forEach((mutation) => {
- if (mutation.type === "childList" || mutation.type === "attributes") self.visibleIndex = $select.selectedIndex, self.optionsList = Array.from($select.querySelectorAll("option")), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- });
- }).observe($select, {
- subtree: !0,
- childList: !0,
- attributes: !0
- }), self.append($select, $btnPrev || "", $content, $btnNext || ""), BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self), Object.defineProperty(self, "value", {
- get() {
- return $select.value;
- },
- set(value) {
- self.optionsList = Array.from($select.querySelectorAll("option")), $select.value = value, self.visibleIndex = $select.selectedIndex, BxSelectElement.resetIndicators.call(self), BxSelectElement.render.call(self);
- }
- }), Object.defineProperty(self, "disabled", {
- get() {
- return $select.disabled;
- },
- set(value) {
- $select.disabled = value;
- }
- }), self.addEventListener = function() {
- $select.addEventListener.apply($select, arguments);
- }, self.removeEventListener = function() {
- $select.removeEventListener.apply($select, arguments);
- }, self.dispatchEvent = function() {
- return $select.dispatchEvent.apply($select, arguments);
- }, self.appendChild = function(node) {
- return $select.appendChild(node), node;
- }, self;
- }
- static resetIndicators() {
- let {
- optionsList,
- indicatorsList,
- $indicators
- } = this, targetSize = optionsList.length;
- if (indicatorsList.length > targetSize) while (indicatorsList.length > targetSize)
- indicatorsList.pop()?.remove();
- else if (indicatorsList.length < targetSize) while (indicatorsList.length < targetSize) {
- let $indicator = CE("span", {});
- indicatorsList.push($indicator), $indicators.appendChild($indicator);
- }
- for (let $indicator of indicatorsList)
- clearDataSet($indicator);
- $indicators.classList.toggle("bx-invisible", targetSize <= 1);
- }
- static getOptionAtIndex(index) {
- return this.optionsList[index];
- }
- static render(e) {
- let {
- $label,
- $btnNext,
- $btnPrev,
- $checkBox,
- optionsList,
- indicatorsList
- } = this;
- if (e && e.manualTrigger) this.visibleIndex = this.$select.selectedIndex;
- this.visibleIndex = BxSelectElement.normalizeIndex.call(this, this.visibleIndex);
- let $option = BxSelectElement.getOptionAtIndex.call(this, this.visibleIndex), content = "";
- if ($option) {
- let $parent = $option.parentElement, hasLabel = $parent instanceof HTMLOptGroupElement || this.$select.querySelector("optgroup");
- if (content = $option.dataset.label || $option.textContent || "", content && hasLabel) {
- let groupLabel = $parent instanceof HTMLOptGroupElement ? $parent.label : " ";
- $label.innerHTML = "";
- let fragment = document.createDocumentFragment();
- fragment.appendChild(CE("span", !1, groupLabel)), fragment.appendChild(document.createTextNode(content)), $label.appendChild(fragment);
- } else $label.textContent = content;
- } else $label.textContent = content;
- if ($label.classList.toggle("bx-line-through", $option && $option.disabled), this.isMultiple) $checkBox.checked = $option?.selected || !1, $checkBox.classList.toggle("bx-gone", !content);
- let disableButtons = optionsList.length <= 1;
- $btnPrev?.classList.toggle("bx-gone", disableButtons), $btnNext?.classList.toggle("bx-gone", disableButtons);
- for (let i = 0;i < optionsList.length; i++) {
- let $option2 = optionsList[i], $indicator = indicatorsList[i];
- if (!$option2 || !$indicator) continue;
- if (clearDataSet($indicator), $option2.selected) $indicator.dataset.selected = "true";
- if ($option2.index === this.visibleIndex) $indicator.dataset.highlighted = "true";
- }
- }
- static normalizeIndex(index) {
- return Math.min(Math.max(index, 0), this.optionsList.length - 1);
- }
- static onPrevNext(e) {
- if (!e.target) return;
- let {
- $btnNext,
- $select,
- isMultiple,
- visibleIndex: currentIndex
- } = this, newIndex = e.target.closest("button") === $btnNext ? currentIndex + 1 : currentIndex - 1;
- if (newIndex > this.optionsList.length - 1) newIndex = 0;
- else if (newIndex < 0) newIndex = this.optionsList.length - 1;
- if (newIndex = BxSelectElement.normalizeIndex.call(this, newIndex), this.visibleIndex = newIndex, !isMultiple && newIndex !== currentIndex) $select.selectedIndex = newIndex;
- if (isMultiple) BxSelectElement.render.call(this);
- else BxEvent.dispatch($select, "input");
- }
}
var controller_customization_default = "var shareButtonPressed=currentGamepad.buttons[17]?.pressed,shareButtonHandled=!1,xCloudGamepad=$xCloudGamepadVar$;if(currentGamepad.id in window.BX_STREAM_SETTINGS.controllers){let controller=window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];if(controller?.customization){let{mapping,ranges}=controller.customization,pressedButtons={},releasedButtons={},isModified=!1;if(ranges.LeftTrigger){let[from,to]=ranges.LeftTrigger;xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTrigger>to?1:xCloudGamepad.LeftTrigger,xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTriggerto?1:xCloudGamepad.RightTrigger,xCloudGamepad.RightTrigger=xCloudGamepad.RightTriggerto?1:range;if(newRange=newRangeto?1:range;if(newRange=newRange=0.1)pressedButtons[targetX]=rangeX,pressedButtons[targetY]=rangeY}releasedButtons[sourceX]=0,releasedButtons[sourceY]=0,isModified=!0}else if(typeof mappedKey===\"string\"){let pressed=!1,value=0;if(key===\"LeftTrigger\"||key===\"RightTrigger\"){let currentRange=xCloudGamepad[key];if(mappedKey===\"LeftTrigger\"||mappedKey===\"RightTrigger\")pressed=currentRange>=0.1,value=currentRange;else pressed=!0,value=currentRange>=0.9?1:0}else if(xCloudGamepad[key])pressed=!0,value=xCloudGamepad[key];if(pressed)pressedButtons[mappedKey]=value,releasedButtons[key]=0,isModified=!0}else if(mappedKey===!1)pressedButtons[key]=0,isModified=!0}isModified&&Object.assign(xCloudGamepad,releasedButtons,pressedButtons)}}if(shareButtonPressed&&!shareButtonHandled)window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n";
var poll_gamepad_default = "var self=this;if(window.BX_EXPOSED.disableGamepadPolling){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(50):self.pollGamepadssetTimeoutTimerID=window.setTimeout(self.pollGamepads,50);return}var currentGamepad=$gamepadVar$,btnHome=currentGamepad.buttons[16];if(btnHome){if(!self.bxHomeStates)self.bxHomeStates={};let intervalMs=0,hijack=!1;if(btnHome.pressed)if(hijack=!0,intervalMs=16,self.gamepadIsIdle.set(currentGamepad.index,!1),self.bxHomeStates[currentGamepad.index]){let lastTimestamp=self.bxHomeStates[currentGamepad.index].timestamp;if(currentGamepad.timestamp!==lastTimestamp){if(self.bxHomeStates[currentGamepad.index].timestamp=currentGamepad.timestamp,window.BX_EXPOSED.handleControllerShortcut(currentGamepad))self.bxHomeStates[currentGamepad.index].shortcutPressed+=1}}else window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index),self.bxHomeStates[currentGamepad.index]={shortcutPressed:0,timestamp:currentGamepad.timestamp};else if(self.bxHomeStates[currentGamepad.index]){hijack=!0;let info=structuredClone(self.bxHomeStates[currentGamepad.index]);if(self.bxHomeStates[currentGamepad.index]=null,info.shortcutPressed===0){let fakeGamepadMappings=[{GamepadIndex:currentGamepad.index,A:0,B:0,X:0,Y:0,LeftShoulder:0,RightShoulder:0,LeftTrigger:0,RightTrigger:0,View:0,Menu:0,LeftThumb:0,RightThumb:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,Nexus:1,LeftThumbXAxis:0,LeftThumbYAxis:0,RightThumbXAxis:0,RightThumbYAxis:0,PhysicalPhysicality:0,VirtualPhysicality:0,Dirty:!0,Virtual:!1}];intervalMs=currentGamepad.timestamp-info.timestamp>=500?500:100,self.inputSink.onGamepadInput(performance.now()-intervalMs,fakeGamepadMappings)}else intervalMs=window.BX_STREAM_SETTINGS.controllerPollingRate}if(hijack&&intervalMs){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(intervalMs):self.pollGamepadssetTimeoutTimerID=setTimeout(self.pollGamepads,intervalMs);return}}\n";
@@ -4519,3339 +4827,3036 @@ var local_co_op_enable_default = 'this.orgOnGamepadChanged=this.onGamepadChanged
var remote_play_keep_alive_default = `try{if(JSON.parse(e).reason==="WarningForBeingIdle"&&!window.location.pathname.includes("/launch/")){this.sendKeepAlive();return}}catch(ex){console.log(ex)}`;
var vibration_adjust_default = `if(e?.gamepad?.connected){let gamepadSettings=window.BX_STREAM_SETTINGS.controllers[e.gamepad.id];if(gamepadSettings?.customization){let intensity=gamepadSettings.customization.vibrationIntensity;if(intensity<=0){e.repeat=0;return}else if(intensity<1)e.leftMotorPercent*=intensity,e.rightMotorPercent*=intensity,e.leftTriggerMotorPercent*=intensity,e.rightTriggerMotorPercent*=intensity}}`;
class PatcherUtils {
- static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {
- if (startIndex < 0) return -1;
- let index = txt.indexOf(searchString, startIndex);
- if (index < 0 || maxRange && index - startIndex > maxRange) return -1;
- return after ? index + searchString.length : index;
- }
- static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {
- if (startIndex < 0) return -1;
- let index = txt.lastIndexOf(searchString, startIndex);
- if (index < 0 || maxRange && startIndex - index > maxRange) return -1;
- return after ? index + searchString.length : index;
- }
- static insertAt(txt, index, insertString) {
- return txt.substring(0, index) + insertString + txt.substring(index);
- }
- static replaceWith(txt, index, fromString, toString) {
- return txt.substring(0, index) + toString + txt.substring(index + fromString.length);
- }
- static filterPatches(patches) {
- return patches.filter((item2) => !!item2);
- }
- static patchBeforePageLoad(str, page) {
- let text = `chunkName:()=>"${page}-page",`;
- if (!str.includes(text)) return !1;
- return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str;
- }
- static isVarCharacter(char) {
- let code = char.charCodeAt(0), isUppercase = code >= 65 && code <= 90, isLowercase = code >= 97 && code <= 122, isDigit = code >= 48 && code <= 57;
- return isUppercase || isLowercase || isDigit || (char === "_" || char === "$");
- }
- static getVariableNameBefore(str, index) {
- if (index < 0) return null;
- let end = index, start = end - 1;
- while (PatcherUtils.isVarCharacter(str[start]))
- start -= 1;
- return str.substring(start + 1, end);
- }
- static getVariableNameAfter(str, index) {
- if (index < 0) return null;
- let start = index, end = start + 1;
- while (PatcherUtils.isVarCharacter(str[end]))
- end += 1;
- return str.substring(start, end);
- }
+static indexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {
+if (startIndex < 0) return -1;
+let index = txt.indexOf(searchString, startIndex);
+if (index < 0 || maxRange && index - startIndex > maxRange) return -1;
+return after ? index + searchString.length : index;
+}
+static lastIndexOf(txt, searchString, startIndex, maxRange = 0, after = !1) {
+if (startIndex < 0) return -1;
+let index = txt.lastIndexOf(searchString, startIndex);
+if (index < 0 || maxRange && startIndex - index > maxRange) return -1;
+return after ? index + searchString.length : index;
+}
+static insertAt(txt, index, insertString) {
+return txt.substring(0, index) + insertString + txt.substring(index);
+}
+static replaceWith(txt, index, fromString, toString) {
+return txt.substring(0, index) + toString + txt.substring(index + fromString.length);
+}
+static filterPatches(patches) {
+return patches.filter((item2) => !!item2);
+}
+static patchBeforePageLoad(str, page) {
+let text = `chunkName:()=>"${page}-page",`;
+if (!str.includes(text)) return !1;
+return str = str.replace("requireAsync(e){", `requireAsync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str = str.replace("requireSync(e){", `requireSync(e){window.BX_EXPOSED.beforePageLoad("${page}");`), str;
+}
+static isVarCharacter(char) {
+let code = char.charCodeAt(0), isUppercase = code >= 65 && code <= 90, isLowercase = code >= 97 && code <= 122, isDigit = code >= 48 && code <= 57;
+return isUppercase || isLowercase || isDigit || (char === "_" || char === "$");
+}
+static getVariableNameBefore(str, index) {
+if (index < 0) return null;
+let end = index, start = end - 1;
+while (PatcherUtils.isVarCharacter(str[start]))
+start -= 1;
+return str.substring(start + 1, end);
+}
+static getVariableNameAfter(str, index) {
+if (index < 0) return null;
+let start = index, end = start + 1;
+while (PatcherUtils.isVarCharacter(str[end]))
+end += 1;
+return str.substring(start, end);
+}
}
var LOG_TAG2 = "Patcher", PATCHES = {
- disableAiTrack(str) {
- let text = ".track=function(", index = str.indexOf(text);
- if (index < 0) return !1;
- if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) return !1;
- return PatcherUtils.replaceWith(str, index, text, ".track=function(e){},!!function(");
- },
- disableTelemetry(str) {
- let text = ".disableTelemetry=function(){return!1}";
- if (!str.includes(text)) return !1;
- return str.replace(text, ".disableTelemetry=function(){return!0}");
- },
- disableTelemetryProvider(str) {
- let text = "this.enableLightweightTelemetry=!";
- if (!str.includes(text)) return !1;
- let newCode = [
- "this.trackEvent",
- "this.trackPageView",
- "this.trackHttpCompleted",
- "this.trackHttpFailed",
- "this.trackError",
- "this.trackErrorLike",
- "this.onTrackEvent",
- "()=>{}"
- ].join("=");
- return str.replace(text, newCode + ";" + text);
- },
- disableIndexDbLogging(str) {
- let text = ",this.logsDb=new";
- if (!str.includes(text)) return !1;
- let newCode = ",this.log=()=>{}";
- return str.replace(text, newCode + text);
- },
- websiteLayout(str) {
- let text = '?"tv":"default"';
- if (!str.includes(text)) return !1;
- let layout = getGlobalPref("ui.layout") === "tv" ? "tv" : "default";
- return str.replace(text, `?"${layout}":"${layout}"`);
- },
- remotePlayDirectConnectUrl(str) {
- let index = str.indexOf("/direct-connect");
- if (index < 0) return !1;
- return str.replace(str.substring(index - 9, index + 15), "https://www.xbox.com/play");
- },
- remotePlayKeepAlive(str) {
- let text = "onServerDisconnectMessage(e){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + remote_play_keep_alive_default), str;
- },
- remotePlayConnectMode(str) {
- let text = 'connectMode:"cloud-connect",';
- if (!str.includes(text)) return !1;
- let newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect",
+disableAiTrack(str) {
+let text = ".track=function(", index = str.indexOf(text);
+if (index < 0) return !1;
+if (PatcherUtils.indexOf(str, '"AppInsightsCore', index, 200) < 0) return !1;
+return PatcherUtils.replaceWith(str, index, text, ".track=function(e){},!!function(");
+},
+disableTelemetry(str) {
+let text = ".disableTelemetry=function(){return!1}";
+if (!str.includes(text)) return !1;
+return str.replace(text, ".disableTelemetry=function(){return!0}");
+},
+disableTelemetryProvider(str) {
+let text = "this.enableLightweightTelemetry=!";
+if (!str.includes(text)) return !1;
+let newCode = [
+"this.trackEvent",
+"this.trackPageView",
+"this.trackHttpCompleted",
+"this.trackHttpFailed",
+"this.trackError",
+"this.trackErrorLike",
+"this.onTrackEvent",
+"()=>{}"
+].join("=");
+return str.replace(text, newCode + ";" + text);
+},
+disableIndexDbLogging(str) {
+let text = ",this.logsDb=new";
+if (!str.includes(text)) return !1;
+let newCode = ",this.log=()=>{}";
+return str.replace(text, newCode + text);
+},
+websiteLayout(str) {
+let text = '?"tv":"default"';
+if (!str.includes(text)) return !1;
+let layout = getGlobalPref("ui.layout") === "tv" ? "tv" : "default";
+return str.replace(text, `?"${layout}":"${layout}"`);
+},
+remotePlayDirectConnectUrl(str) {
+let index = str.indexOf("/direct-connect");
+if (index < 0) return !1;
+return str.replace(str.substring(index - 9, index + 15), "https://www.xbox.com/play");
+},
+remotePlayKeepAlive(str) {
+let text = "onServerDisconnectMessage(e){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + remote_play_keep_alive_default), str;
+},
+remotePlayConnectMode(str) {
+let text = 'connectMode:"cloud-connect",';
+if (!str.includes(text)) return !1;
+let newCode = `connectMode: window.BX_REMOTE_PLAY_CONFIG ? "xhome-connect" : "cloud-connect",
remotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',`;
- return str.replace(text, newCode);
- },
- remotePlayDisableAchievementToast(str) {
- let text = ".AchievementUnlock:{";
- if (!str.includes(text)) return !1;
- let newCode = "if (!!window.BX_REMOTE_PLAY_CONFIG) return;";
- return str.replace(text, text + newCode);
- },
- remotePlayRecentlyUsedTitleIds(str) {
- let text = "(e.data.recentlyUsedTitleIds)){";
- if (!str.includes(text)) return !1;
- let newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;";
- return str.replace(text, text + newCode);
- },
- remotePlayWebTitle(str) {
- let text = "titleTemplate:void 0,title:", index = str.indexOf(text);
- if (index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t("remote-play")} - Better xCloud" :`), str;
- },
- blockWebRtcStatsCollector(str) {
- let text = "this.shouldCollectStats=!0";
- if (!str.includes(text)) return !1;
- return str.replace(text, "this.shouldCollectStats=!1");
- },
- patchPollGamepads(str) {
- let index = str.indexOf("},this.pollGamepads=()=>{");
- if (index < 0) return !1;
- let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index);
- if (setTimeoutIndex < 0) return !1;
- let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ");
- if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getGlobalPref("block.tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", "");
- let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/);
- if (!match) return !1;
- let newCode = renderString(poll_gamepad_default, {
- gamepadVar: match[1]
- });
- if (codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"), match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/), !match) return !1;
- let xCloudGamepadVar = match[1], inputFeedbackManager = PatcherUtils.indexOf(codeBlock, "this.inputFeedbackManager.onGamepadConnected(", 0, 1e4), backetIndex = PatcherUtils.indexOf(codeBlock, "}", inputFeedbackManager, 100);
- if (backetIndex < 0) return !1;
- let customizationCode = ";";
- return customizationCode += renderString(controller_customization_default, { xCloudGamepadVar }), codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode), str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str;
- },
- enableXcloudLogger(str) {
- let text = "this.telemetryProvider=e}log(e,t,r){";
- if (!str.includes(text)) return !1;
- let newCode = `
+return str.replace(text, newCode);
+},
+remotePlayDisableAchievementToast(str) {
+let text = ".AchievementUnlock:{";
+if (!str.includes(text)) return !1;
+let newCode = "if (!!window.BX_REMOTE_PLAY_CONFIG) return;";
+return str.replace(text, text + newCode);
+},
+remotePlayRecentlyUsedTitleIds(str) {
+let text = "(e.data.recentlyUsedTitleIds)){";
+if (!str.includes(text)) return !1;
+let newCode = "if (window.BX_REMOTE_PLAY_CONFIG) return;";
+return str.replace(text, text + newCode);
+},
+remotePlayWebTitle(str) {
+let text = "titleTemplate:void 0,title:", index = str.indexOf(text);
+if (index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index + text.length, `!!window.BX_REMOTE_PLAY_CONFIG ? "${t("remote-play")} - Better xCloud" :`), str;
+},
+blockWebRtcStatsCollector(str) {
+let text = "this.shouldCollectStats=!0";
+if (!str.includes(text)) return !1;
+return str.replace(text, "this.shouldCollectStats=!1");
+},
+patchPollGamepads(str) {
+let index = str.indexOf("},this.pollGamepads=()=>{");
+if (index < 0) return !1;
+let setTimeoutIndex = str.indexOf("setTimeout(this.pollGamepads", index);
+if (setTimeoutIndex < 0) return !1;
+let codeBlock = str.substring(index, setTimeoutIndex), tmp = str.substring(setTimeoutIndex, setTimeoutIndex + 150), tmpPatched = tmp.replaceAll("Math.max(0,4-", "Math.max(0,window.BX_STREAM_SETTINGS.controllerPollingRate - ");
+if (str = PatcherUtils.replaceWith(str, setTimeoutIndex, tmp, tmpPatched), getGlobalPref("block.tracking")) codeBlock = codeBlock.replace("this.inputPollingIntervalStats.addValue", ""), codeBlock = codeBlock.replace("this.inputPollingDurationStats.addValue", "");
+let match = codeBlock.match(/this\.gamepadTimestamps\.set\(([A-Za-z0-9_$]+)\.index/);
+if (!match) return !1;
+let newCode = renderString(poll_gamepad_default, {
+gamepadVar: match[1]
+});
+if (codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set"), match = codeBlock.match(/let ([A-Za-z0-9_$]+)=this\.gamepadMappings\.find/), !match) return !1;
+let xCloudGamepadVar = match[1], inputFeedbackManager = PatcherUtils.indexOf(codeBlock, "this.inputFeedbackManager.onGamepadConnected(", 0, 1e4), backetIndex = PatcherUtils.indexOf(codeBlock, "}", inputFeedbackManager, 100);
+if (backetIndex < 0) return !1;
+let customizationCode = ";";
+return customizationCode += renderString(controller_customization_default, { xCloudGamepadVar }), codeBlock = PatcherUtils.insertAt(codeBlock, backetIndex, customizationCode), str = str.substring(0, index) + codeBlock + str.substring(setTimeoutIndex), str;
+},
+enableXcloudLogger(str) {
+let text = "this.telemetryProvider=e}log(e,t,r){";
+if (!str.includes(text)) return !1;
+let newCode = `
const [logTag, logLevel, logMessage] = Array.from(arguments);
const logFunc = [console.debug, console.log, console.warn, console.error][logLevel];
logFunc(logTag, '//', logMessage);
`;
- return str = str.replaceAll(text, text + newCode), str;
- },
- enableConsoleLogging(str) {
- let text = "static isConsoleLoggingAllowed(){";
- if (!str.includes(text)) return !1;
- return str = str.replaceAll(text, text + "return true;"), str;
- },
- playVibration(str) {
- let text = "}playVibration(e){";
- if (!str.includes(text)) return !1;
- return str = str.replaceAll(text, text + vibration_adjust_default), str;
- },
- disableGamepadDisconnectedScreen(str) {
- let index = str.indexOf('"GamepadDisconnected_Title",');
- if (index < 0) return !1;
- let constIndex = str.indexOf("const", index - 30);
- return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str;
- },
- patchUpdateInputConfigurationAsync(str) {
- let text = "async updateInputConfigurationAsync(e){";
- if (!str.includes(text)) return !1;
- let newCode = "e.enableTouchInput = true;";
- return str = str.replace(text, text + newCode), str;
- },
- disableStreamGate(str) {
- let index = str.indexOf('case"partially-ready":');
- if (index < 0) return !1;
- let bracketIndex = str.indexOf("=>{", index - 150) + 3;
- return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str;
- },
- exposeTouchLayoutManager(str) {
- let text = "this._perScopeLayoutsStream=new";
- if (!str.includes(text)) return !1;
- let newCode = `
+return str = str.replaceAll(text, text + newCode), str;
+},
+enableConsoleLogging(str) {
+let text = "static isConsoleLoggingAllowed(){";
+if (!str.includes(text)) return !1;
+return str = str.replaceAll(text, text + "return true;"), str;
+},
+playVibration(str) {
+let text = "}playVibration(e){";
+if (!str.includes(text)) return !1;
+return str = str.replaceAll(text, text + vibration_adjust_default), str;
+},
+disableGamepadDisconnectedScreen(str) {
+let index = str.indexOf('"GamepadDisconnected_Title",');
+if (index < 0) return !1;
+let constIndex = str.indexOf("const", index - 30);
+return str = str.substring(0, constIndex) + "e.onClose();return null;" + str.substring(constIndex), str;
+},
+patchUpdateInputConfigurationAsync(str) {
+let text = "async updateInputConfigurationAsync(e){";
+if (!str.includes(text)) return !1;
+let newCode = "e.enableTouchInput = true;";
+return str = str.replace(text, text + newCode), str;
+},
+disableStreamGate(str) {
+let index = str.indexOf('case"partially-ready":');
+if (index < 0) return !1;
+let bracketIndex = str.indexOf("=>{", index - 150) + 3;
+return str = str.substring(0, bracketIndex) + "return 0;" + str.substring(bracketIndex), str;
+},
+exposeTouchLayoutManager(str) {
+let text = "this._perScopeLayoutsStream=new";
+if (!str.includes(text)) return !1;
+let newCode = `
true;
window.BX_EXPOSED["touchLayoutManager"] = this;
window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}"));
`;
- return str = str.replace(text, newCode + text), str;
- },
- patchBabylonRendererClass(str) {
- let index = str.indexOf(".current.render(),");
- if (index < 0) return !1;
- index -= 1;
- let newCode = `
+return str = str.replace(text, newCode + text), str;
+},
+patchBabylonRendererClass(str) {
+let index = str.indexOf(".current.render(),");
+if (index < 0) return !1;
+index -= 1;
+let newCode = `
if (window.BX_EXPOSED.stopTakRendering) {
- try {
- document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen');
- ${str[index]}.current.dispose();
- } catch (e) {}
- window.BX_EXPOSED.stopTakRendering = false;
- return;
+try {
+document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen');
+${str[index]}.current.dispose();
+} catch (e) {}
+window.BX_EXPOSED.stopTakRendering = false;
+return;
}
`;
- return str = str.substring(0, index) + newCode + str.substring(index), str;
- },
- supportLocalCoOp(str) {
- let text = "this.gamepadMappingsToSend=[],";
- if (!str.includes(text)) return !1;
- let newCode = `true; ${local_co_op_enable_default}; true,`;
- return str = str.replace(text, text + newCode), str;
- },
- forceFortniteConsole(str) {
- let text = "sendTouchInputEnabledMessage(e){";
- if (!str.includes(text)) return !1;
- let newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);";
- return str = str.replace(text, text + newCode), str;
- },
- disableTakRenderer(str) {
- let text = "const{TakRenderer:";
- if (!str.includes(text)) return !1;
- let autoOffCode = "";
- if (getGlobalPref("touchController.mode") === "off") autoOffCode = "return;";
- else if (getGlobalPref("touchController.autoOff")) autoOffCode = `
+return str = str.substring(0, index) + newCode + str.substring(index), str;
+},
+supportLocalCoOp(str) {
+let text = "this.gamepadMappingsToSend=[],";
+if (!str.includes(text)) return !1;
+let newCode = `true; ${local_co_op_enable_default}; true,`;
+return str = str.replace(text, text + newCode), str;
+},
+forceFortniteConsole(str) {
+let text = "sendTouchInputEnabledMessage(e){";
+if (!str.includes(text)) return !1;
+let newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);";
+return str = str.replace(text, text + newCode), str;
+},
+disableTakRenderer(str) {
+let text = "const{TakRenderer:";
+if (!str.includes(text)) return !1;
+let autoOffCode = "";
+if (getGlobalPref("touchController.mode") === "off") autoOffCode = "return;";
+else if (getGlobalPref("touchController.autoOff")) autoOffCode = `
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
for (let gamepad of gamepads) {
- if (gamepad && gamepad.connected) {
- gamepadFound = true;
- break;
- }
+if (gamepad && gamepad.connected) {
+gamepadFound = true;
+break;
+}
}
if (gamepadFound) {
- return;
+return;
}
`;
- let newCode = `
+let 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 (getGlobalPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;";
- return str = str.replace(text, newCode + text), str;
- },
- broadcastPollingMode(str) {
- let text = ".setPollingMode=e=>{";
- if (!str.includes(text)) return !1;
- let newCode = `
+if (getGlobalPref("touchController.mode") === "off") newCode += "e.canShowTakHUD = false;";
+return str = str.replace(text, newCode + text), str;
+},
+broadcastPollingMode(str) {
+let text = ".setPollingMode=e=>{";
+if (!str.includes(text)) return !1;
+let newCode = `
window.BX_STREAM_SETTINGS.xCloudPollingMode = e.toLowerCase();
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED);
`;
- return str = str.replace(text, text + newCode), str;
- },
- patchGamepadPolling(str) {
- let index = str.indexOf(".shouldHandleGamepadInput)())return void");
- if (index < 0) return !1;
- return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str;
- },
- patchXcloudTitleInfo(str) {
- let text = "async cloudConnect", index = str.indexOf(text);
- if (index < 0) return !1;
- let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
- if (!params) return !1;
- let titleInfoVar = params.split(",")[0], newCode = `
+return str = str.replace(text, text + newCode), str;
+},
+patchGamepadPolling(str) {
+let index = str.indexOf(".shouldHandleGamepadInput)())return void");
+if (index < 0) return !1;
+return index = str.indexOf("{", index - 20) + 1, str = str.substring(0, index) + "if (window.BX_EXPOSED.disableGamepadPolling) return;" + str.substring(index), str;
+},
+patchXcloudTitleInfo(str) {
+let text = "async cloudConnect", index = str.indexOf(text);
+if (index < 0) return !1;
+let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
+if (!params) return !1;
+let titleInfoVar = params.split(",")[0], newCode = `
${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar});
BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
`;
- return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;
- },
- patchRemotePlayMkb(str) {
- let text = "async homeConsoleConnect", index = str.indexOf(text);
- if (index < 0) return !1;
- let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
- if (!params) return !1;
- let configsVar = params.split(",")[1], newCode = `
+return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;
+},
+patchRemotePlayMkb(str) {
+let text = "async homeConsoleConnect", index = str.indexOf(text);
+if (index < 0) return !1;
+let backetIndex = str.indexOf("{", index), params = str.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
+if (!params) return !1;
+let configsVar = params.split(",")[1], newCode = `
Object.assign(${configsVar}.inputConfiguration, {
- enableMouseInput: false,
- enableKeyboardInput: false,
- enableAbsoluteMouse: false,
+enableMouseInput: false,
+enableKeyboardInput: false,
+enableAbsoluteMouse: false,
});
BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
- return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;
- },
- patchAudioMediaStream(str) {
- let text = ".srcObject=this.audioMediaStream,";
- if (!str.includes(text)) return !1;
- let newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),";
- return str = str.replace(text, text + newCode), str;
- },
- patchCombinedAudioVideoMediaStream(str) {
- let text = ".srcObject=this.combinedAudioVideoStream";
- if (!str.includes(text)) return !1;
- let newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)";
- return str = str.replace(text, text + newCode), str;
- },
- patchTouchControlDefaultOpacity(str) {
- let text = "opacityMultiplier:1";
- if (!str.includes(text)) return !1;
- let newCode = `opacityMultiplier: ${(getGlobalPref("touchController.opacity.default") / 100).toFixed(1)}`;
- return str = str.replace(text, newCode), str;
- },
- patchShowSensorControls(str) {
- let text = ",{shouldShowSensorControls:";
- if (!str.includes(text)) return !1;
- let newCode = ",{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||";
- return str = str.replace(text, newCode), str;
- },
- exposeStreamSession(str) {
- let text = ",this._connectionType=";
- if (!str.includes(text)) return !1;
- let newCode = `;
+return str = str.substring(0, backetIndex + 1) + newCode + str.substring(backetIndex + 1), str;
+},
+patchAudioMediaStream(str) {
+let text = ".srcObject=this.audioMediaStream,";
+if (!str.includes(text)) return !1;
+let newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),";
+return str = str.replace(text, text + newCode), str;
+},
+patchCombinedAudioVideoMediaStream(str) {
+let text = ".srcObject=this.combinedAudioVideoStream";
+if (!str.includes(text)) return !1;
+let newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)";
+return str = str.replace(text, text + newCode), str;
+},
+patchTouchControlDefaultOpacity(str) {
+let text = "opacityMultiplier:1";
+if (!str.includes(text)) return !1;
+let newCode = `opacityMultiplier: ${(getGlobalPref("touchController.opacity.default") / 100).toFixed(1)}`;
+return str = str.replace(text, newCode), str;
+},
+patchShowSensorControls(str) {
+let text = ",{shouldShowSensorControls:";
+if (!str.includes(text)) return !1;
+let newCode = ",{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||";
+return str = str.replace(text, newCode), str;
+},
+exposeStreamSession(str) {
+let text = ",this._connectionType=";
+if (!str.includes(text)) return !1;
+let newCode = `;
${expose_stream_session_default}
true` + text;
- return str = str.replace(text, newCode), str;
- },
- skipFeedbackDialog(str) {
- let text = "shouldTransitionToFeedback(e){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + "return !1;"), str;
- },
- enableNativeMkb(str) {
- let index = str.indexOf(".mouseSupported&&");
- if (index < 0) return !1;
- let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`;
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + "return true;"), str;
- },
- patchMouseAndKeyboardEnabled(str) {
- let text = "get mouseAndKeyboardEnabled(){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + "return true;"), str;
- },
- exposeInputChannel(str) {
- let index = str.indexOf("this.flushData=");
- if (index < 0) return !1;
- let newCode = "window.BX_EXPOSED.inputChannel = this,";
- return str = PatcherUtils.insertAt(str, index, newCode), str;
- },
- disableNativeRequestPointerLock(str) {
- let text = "async requestPointerLock(){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + "return;"), str;
- },
- patchRequestInfoCrash(str) {
- let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");';
- if (!str.includes(text)) return !1;
- return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str;
- },
- exposeDialogRoutes(str) {
- let text = "return{goBack:function(){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str;
- },
- enableTvRoutes(str) {
- let index = str.indexOf(".LoginDeviceCode.path,");
- if (index < 0) return !1;
- let match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100));
- if (!match) return !1;
- let funcName = match[1];
- if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1;
- return str = str.substring(0, index) + "|| true" + str.substring(index), str;
- },
- ignoreNewsSection(str) {
- let index = str.indexOf('Logger("CarouselRow")');
- if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const ", index, 200)), index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index, "return null;"), str;
- },
- ignorePlayWithFriendsSection(str) {
- let index = str.indexOf('location:"PlayWithFriendsRow",');
- if (index < 0) return !1;
- if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1;
- return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str;
- },
- ignoreAllGamesSection(str) {
- let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer');
- if (index < 0) return !1;
- if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1;
- if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index, "true ? null :"), str;
- },
- ignorePlayWithTouchSection(str) {
- let index = str.indexOf('("Play_With_Touch"),');
- if (index < 0) return !1;
- if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index, "return null;"), str;
- },
- ignoreSiglSections(str) {
- let index = str.indexOf("SiglRow-module__heroCard___");
- if (index < 0) return !1;
- if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1;
- let PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), siglIds = [], sections = {
- "native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486",
- "most-popular": "e7590b22-e299-44db-ae22-25c61405454c"
- };
- for (let section of PREF_HIDE_SECTIONS) {
- let galleryId = sections[section];
- galleryId && siglIds.push(galleryId);
- }
- let newCode = `
+return str = str.replace(text, newCode), str;
+},
+skipFeedbackDialog(str) {
+let text = "shouldTransitionToFeedback(e){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + "return !1;"), str;
+},
+enableNativeMkb(str) {
+let index = str.indexOf(".mouseSupported&&");
+if (index < 0) return !1;
+let varName = str.charAt(index - 1), text = `${varName}.mouseSupported&&${varName}.keyboardSupported&&${varName}.fullscreenSupported;`;
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + "return true;"), str;
+},
+patchMouseAndKeyboardEnabled(str) {
+let text = "get mouseAndKeyboardEnabled(){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + "return true;"), str;
+},
+exposeInputChannel(str) {
+let index = str.indexOf("this.flushData=");
+if (index < 0) return !1;
+let newCode = "window.BX_EXPOSED.inputChannel = this,";
+return str = PatcherUtils.insertAt(str, index, newCode), str;
+},
+disableNativeRequestPointerLock(str) {
+let text = "async requestPointerLock(){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + "return;"), str;
+},
+patchRequestInfoCrash(str) {
+let text = 'if(!e)throw new Error("RequestInfo.origin is falsy");';
+if (!str.includes(text)) return !1;
+return str = str.replace(text, 'if (!e) e = "https://www.xbox.com";'), str;
+},
+exposeDialogRoutes(str) {
+let text = "return{goBack:function(){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, "return window.BX_EXPOSED.dialogRoutes = {goBack:function(){"), str;
+},
+enableTvRoutes(str) {
+let index = str.indexOf(".LoginDeviceCode.path,");
+if (index < 0) return !1;
+let match = /render:.*?jsx\)\(([^,]+),/.exec(str.substring(index, index + 100));
+if (!match) return !1;
+let funcName = match[1];
+if (index = str.indexOf(`const ${funcName}=e=>{`), index > -1 && (index = str.indexOf("return ", index)), index > -1 && (index = str.indexOf("?", index)), index < 0) return !1;
+return str = str.substring(0, index) + "|| true" + str.substring(index), str;
+},
+ignoreNewsSection(str) {
+let index = str.indexOf('Logger("CarouselRow")');
+if (index > -1 && (index = PatcherUtils.lastIndexOf(str, "const ", index, 200)), index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index, "return null;"), str;
+},
+ignorePlayWithFriendsSection(str) {
+let index = str.indexOf('location:"PlayWithFriendsRow",');
+if (index < 0) return !1;
+if (index = PatcherUtils.lastIndexOf(str, "return", index, 50), index < 0) return !1;
+return str = PatcherUtils.replaceWith(str, index, "return", "return null;"), str;
+},
+ignoreAllGamesSection(str) {
+let index = str.indexOf('className:"AllGamesRow-module__allGamesRowContainer');
+if (index < 0) return !1;
+if (index = PatcherUtils.indexOf(str, "grid:!0,", index, 1500), index < 0) return !1;
+if (index = PatcherUtils.lastIndexOf(str, "(0,", index, 70), index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index, "true ? null :"), str;
+},
+ignorePlayWithTouchSection(str) {
+let index = str.indexOf('("Play_With_Touch"),');
+if (index < 0) return !1;
+if (index = PatcherUtils.lastIndexOf(str, "const ", index, 30), index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index, "return null;"), str;
+},
+ignoreSiglSections(str) {
+let index = str.indexOf("SiglRow-module__heroCard___");
+if (index < 0) return !1;
+if (index = PatcherUtils.lastIndexOf(str, "const[", index, 300), index < 0) return !1;
+let PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), siglIds = [], sections = {
+"native-mkb": "8fa264dd-124f-4af3-97e8-596fcdf4b486",
+"most-popular": "e7590b22-e299-44db-ae22-25c61405454c"
+};
+for (let section of PREF_HIDE_SECTIONS) {
+let galleryId = sections[section];
+galleryId && siglIds.push(galleryId);
+}
+let newCode = `
if (e && e.id) {
- const siglId = e.id;
- if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) {
- return null;
- }
+const siglId = e.id;
+if (${siglIds.map((item2) => `siglId === "${item2}"`).join(" || ")}) {
+return null;
+}
}
`;
- return str = PatcherUtils.insertAt(str, index, newCode), str;
- },
- overrideStorageGetSettings(str) {
- let text = "}getSetting(e){";
- if (!str.includes(text)) return !1;
- let newCode = `
+return str = PatcherUtils.insertAt(str, index, newCode), str;
+},
+overrideStorageGetSettings(str) {
+let text = "}getSetting(e){";
+if (!str.includes(text)) return !1;
+let newCode = `
// console.log('setting', this.baseStorageKey, e);
if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
- const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey];
- if (e in settings) {
- return settings[e];
- }
+const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey];
+if (e in settings) {
+return settings[e];
+}
}
`;
- return str = str.replace(text, text + newCode), str;
- },
- alwaysShowStreamHud(str) {
- let index = str.indexOf(",{onShowStreamMenu:");
- if (index < 0) return !1;
- if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1;
- let commaIndex = str.indexOf(",", index - 10);
- return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str;
- },
- patchSetCurrentFocus(str) {
- let index = str.indexOf(".setCurrentFocus=(");
- if (index < 0) return !1;
- return index = str.indexOf("{", index) + 1, str = PatcherUtils.insertAt(str, index, "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });"), str;
- },
- detectProductDetailPage(str) {
- let index = str.indexOf('{location:"ProductDetailPage",');
- if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;
- return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str;
- },
- detectBrowserRouterReady(str) {
- let index = str.indexOf("{history:this.history,");
- if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 100)), index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str;
- },
- guideAchievementsDefaultLocked(str) {
- let index = str.indexOf("FilterButton-module__container");
- if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1;
- if (str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250)), index < 0) return !1;
- return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str;
- },
- disableTouchContextMenu(str) {
- let index = str.indexOf('"ContextualCardActions-module__container');
- if (index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index)), index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return ", index, 50)), index < 0) return !1;
- return str = PatcherUtils.replaceWith(str, index, "return", "return () => {};"), str;
- },
- optimizeGameSlugGenerator(str) {
- let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, "window.BX_EXPOSED.GameSlugRegexes[0]"), str = str.replace("/ {2,}/g", "window.BX_EXPOSED.GameSlugRegexes[1]"), str = str.replace("/ /g", "window.BX_EXPOSED.GameSlugRegexes[2]"), str;
- },
- modifyPreloadedState(str) {
- let text = "=window.__PRELOADED_STATE__;";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str;
- },
- homePageBeforeLoad(str) {
- return PatcherUtils.patchBeforePageLoad(str, "home");
- },
- productDetailPageBeforeLoad(str) {
- return PatcherUtils.patchBeforePageLoad(str, "product-detail");
- },
- streamPageBeforeLoad(str) {
- return PatcherUtils.patchBeforePageLoad(str, "stream");
- },
- disableAbsoluteMouse(str) {
- let text = "sendAbsoluteMouseCapableMessage(e){";
- if (!str.includes(text)) return !1;
- return str = str.replace(text, text + "return;"), str;
- },
- changeNotificationsSubscription(str) {
- let text = ";buildSubscriptionQueryParamsForNotifications(", index = str.indexOf(text);
- if (index < 0) return !1;
- index += text.length;
- let subsVar = str[index];
- index = str.indexOf("{", index) + 1;
- let blockFeatures = getGlobalPref("block.features"), filters = [];
- if (blockFeatures.includes("notifications-invites")) filters.push("GameInvite", "PartyInvite");
- if (blockFeatures.includes("friends")) filters.push("Follower");
- if (blockFeatures.includes("notifications-achievements")) filters.push("AchievementUnlock");
- let newCode = `
+return str = str.replace(text, text + newCode), str;
+},
+alwaysShowStreamHud(str) {
+let index = str.indexOf(",{onShowStreamMenu:");
+if (index < 0) return !1;
+if (index = str.indexOf("&&(0,", index - 100), index < 0) return !1;
+let commaIndex = str.indexOf(",", index - 10);
+return str = str.substring(0, commaIndex) + ",true" + str.substring(index), str;
+},
+patchSetCurrentFocus(str) {
+let index = str.indexOf(".setCurrentFocus=(");
+if (index < 0) return !1;
+return index = str.indexOf("{", index) + 1, str = PatcherUtils.insertAt(str, index, "e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, { element: e });"), str;
+},
+detectProductDetailPage(str) {
+let index = str.indexOf('{location:"ProductDetailPage",');
+if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 200)), index < 0) return !1;
+return str = str.substring(0, index) + 'BxEvent.dispatch(window, BxEvent.XCLOUD_RENDERING_COMPONENT, { component: "product-detail" });' + str.substring(index), str;
+},
+detectBrowserRouterReady(str) {
+let index = str.indexOf("{history:this.history,");
+if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return", index, 100)), index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index, "window.BxEvent.dispatch(window, window.BxEvent.XCLOUD_ROUTER_HISTORY_READY, {history: this.history});"), str;
+},
+guideAchievementsDefaultLocked(str) {
+let index = str.indexOf("FilterButton-module__container");
+if (index >= 0 && (index = PatcherUtils.lastIndexOf(str, '"All"', index, 150)), index < 0) return !1;
+if (str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), index = str.indexOf('"Guide_Achievements_Unlocked_Empty","Guide_Achievements_Locked_Empty"'), index >= 0 && (index = PatcherUtils.indexOf(str, '"All"', index, 250)), index < 0) return !1;
+return str = PatcherUtils.replaceWith(str, index, '"All"', '"Locked"'), str;
+},
+disableTouchContextMenu(str) {
+let index = str.indexOf('"ContextualCardActions-module__container');
+if (index >= 0 && (index = str.indexOf('addEventListener("touchstart"', index)), index >= 0 && (index = PatcherUtils.lastIndexOf(str, "return ", index, 50)), index < 0) return !1;
+return str = PatcherUtils.replaceWith(str, index, "return", "return () => {};"), str;
+},
+optimizeGameSlugGenerator(str) {
+let text = "/[;,/?:@&=+_`~$%#^*()!^\\u2122\\xae\\xa9]/g";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, "window.BX_EXPOSED.GameSlugRegexes[0]"), str = str.replace("/ {2,}/g", "window.BX_EXPOSED.GameSlugRegexes[1]"), str = str.replace("/ /g", "window.BX_EXPOSED.GameSlugRegexes[2]"), str;
+},
+modifyPreloadedState(str) {
+let text = "=window.__PRELOADED_STATE__;";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, "=window.BX_EXPOSED.modifyPreloadedState(window.__PRELOADED_STATE__);"), str;
+},
+homePageBeforeLoad(str) {
+return PatcherUtils.patchBeforePageLoad(str, "home");
+},
+productDetailPageBeforeLoad(str) {
+return PatcherUtils.patchBeforePageLoad(str, "product-detail");
+},
+streamPageBeforeLoad(str) {
+return PatcherUtils.patchBeforePageLoad(str, "stream");
+},
+disableAbsoluteMouse(str) {
+let text = "sendAbsoluteMouseCapableMessage(e){";
+if (!str.includes(text)) return !1;
+return str = str.replace(text, text + "return;"), str;
+},
+changeNotificationsSubscription(str) {
+let text = ";buildSubscriptionQueryParamsForNotifications(", index = str.indexOf(text);
+if (index < 0) return !1;
+index += text.length;
+let subsVar = str[index];
+index = str.indexOf("{", index) + 1;
+let blockFeatures = getGlobalPref("block.features"), filters = [];
+if (blockFeatures.includes("notifications-invites")) filters.push("GameInvite", "PartyInvite");
+if (blockFeatures.includes("friends")) filters.push("Follower");
+if (blockFeatures.includes("notifications-achievements")) filters.push("AchievementUnlock");
+let newCode = `
let subs = ${subsVar};
subs = subs.filter(val => !${JSON.stringify(filters)}.includes(val));
${subsVar} = subs;
`;
- return str = PatcherUtils.insertAt(str, index, newCode), str;
- },
- exposeReactCreateComponent(str) {
- let index = str.indexOf(".prototype.isReactComponent={}");
- if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;
- let newCode = "window.BX_EXPOSED.reactCreateElement=";
- return str = PatcherUtils.insertAt(str, index - 1, newCode), str;
- },
- gameCardCustomIcons(str) {
- let initialIndex = str.indexOf("const{supportedInputIcons:");
- if (initialIndex < 0) return !1;
- let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));
- if (returnIndex < 0) return !1;
- let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);
- if (arrowIndex < 0) return !1;
- let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));
- if (!paramVar || !supportedInputIconsVar) return !1;
- let newCode = renderString(game_card_icons_default, {
- param: paramVar,
- supportedInputIcons: supportedInputIconsVar
- });
- return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;
- },
- setImageQuality(str) {
- let index = str.indexOf("const{size:{width:");
- if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;
- let paramVar = PatcherUtils.getVariableNameBefore(str, index);
- if (!paramVar) return !1;
- index = PatcherUtils.indexOf(str, "return", index, 200);
- let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;
- return str = PatcherUtils.insertAt(str, index, newCode), str;
- },
- setBackgroundImageQuality(str) {
- let index = str.indexOf("}?w=${");
- if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;
- return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;
- }
+return str = PatcherUtils.insertAt(str, index, newCode), str;
+},
+exposeReactCreateComponent(str) {
+let index = str.indexOf(".prototype.isReactComponent={}");
+if (index > -1 && (index = PatcherUtils.indexOf(str, ".createElement=", index)), index < 0) return !1;
+let newCode = "window.BX_EXPOSED.reactCreateElement=";
+return str = PatcherUtils.insertAt(str, index - 1, newCode), str;
+},
+gameCardCustomIcons(str) {
+let initialIndex = str.indexOf("const{supportedInputIcons:");
+if (initialIndex < 0) return !1;
+let returnIndex = PatcherUtils.lastIndexOf(str, "return ", str.indexOf("SupportedInputsBadge"));
+if (returnIndex < 0) return !1;
+let arrowIndex = PatcherUtils.lastIndexOf(str, "=>{", initialIndex, 300);
+if (arrowIndex < 0) return !1;
+let paramVar = PatcherUtils.getVariableNameBefore(str, arrowIndex), supportedInputIconsVar = PatcherUtils.getVariableNameAfter(str, PatcherUtils.indexOf(str, "supportedInputIcons:", initialIndex, 100, !0));
+if (!paramVar || !supportedInputIconsVar) return !1;
+let newCode = renderString(game_card_icons_default, {
+param: paramVar,
+supportedInputIcons: supportedInputIconsVar
+});
+return str = PatcherUtils.insertAt(str, returnIndex, newCode), str;
+},
+setImageQuality(str) {
+let index = str.indexOf("const{size:{width:");
+if (index > -1 && (index = PatcherUtils.indexOf(str, "=new URLSearchParams", index, 500)), index < 0) return !1;
+let paramVar = PatcherUtils.getVariableNameBefore(str, index);
+if (!paramVar) return !1;
+index = PatcherUtils.indexOf(str, "return", index, 200);
+let newCode = `${paramVar}.set('q', ${getGlobalPref("ui.imageQuality")});`;
+return str = PatcherUtils.insertAt(str, index, newCode), str;
+},
+setBackgroundImageQuality(str) {
+let index = str.indexOf("}?w=${");
+if (index > -1 && (index = PatcherUtils.indexOf(str, "}", index + 1, 10, !0)), index < 0) return !1;
+return str = PatcherUtils.insertAt(str, index, `&q=${getGlobalPref("ui.imageQuality")}`), str;
+}
}, PATCH_ORDERS = PatcherUtils.filterPatches([
- ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [
- "enableNativeMkb",
- "disableAbsoluteMouse"
- ] : [],
- "exposeReactCreateComponent",
- "gameCardCustomIcons",
- ...getGlobalPref("ui.imageQuality") < 90 ? [
- "setImageQuality"
- ] : [],
- "modifyPreloadedState",
- "optimizeGameSlugGenerator",
- "detectBrowserRouterReady",
- "patchRequestInfoCrash",
- "disableStreamGate",
- "broadcastPollingMode",
- "patchGamepadPolling",
- "exposeStreamSession",
- "exposeDialogRoutes",
- "homePageBeforeLoad",
- "productDetailPageBeforeLoad",
- "streamPageBeforeLoad",
- "guideAchievementsDefaultLocked",
- "enableTvRoutes",
- "supportLocalCoOp",
- "overrideStorageGetSettings",
- getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",
- getGlobalPref("ui.layout") !== "default" && "websiteLayout",
- getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",
- ...STATES.userAgent.capabilities.touch ? [
- "disableTouchContextMenu"
- ] : [],
- ...getGlobalPref("block.tracking") ? [
- "disableAiTrack",
- "disableTelemetry",
- "blockWebRtcStatsCollector",
- "disableIndexDbLogging",
- "disableTelemetryProvider"
- ] : [],
- ...getGlobalPref("xhome.enabled") ? [
- "remotePlayKeepAlive",
- "remotePlayDirectConnectUrl",
- "remotePlayDisableAchievementToast",
- "remotePlayRecentlyUsedTitleIds",
- "remotePlayWebTitle",
- STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"
- ] : [],
- ...BX_FLAGS.EnableXcloudLogging ? [
- "enableConsoleLogging",
- "enableXcloudLogger"
- ] : []
+...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [
+"enableNativeMkb",
+"disableAbsoluteMouse"
+] : [],
+"exposeReactCreateComponent",
+"gameCardCustomIcons",
+...getGlobalPref("ui.imageQuality") < 90 ? [
+"setImageQuality"
+] : [],
+"modifyPreloadedState",
+"optimizeGameSlugGenerator",
+"detectBrowserRouterReady",
+"patchRequestInfoCrash",
+"disableStreamGate",
+"broadcastPollingMode",
+"patchGamepadPolling",
+"exposeStreamSession",
+"exposeDialogRoutes",
+"homePageBeforeLoad",
+"productDetailPageBeforeLoad",
+"streamPageBeforeLoad",
+"guideAchievementsDefaultLocked",
+"enableTvRoutes",
+"supportLocalCoOp",
+"overrideStorageGetSettings",
+getGlobalPref("ui.gameCard.waitTime.show") && "patchSetCurrentFocus",
+getGlobalPref("ui.layout") !== "default" && "websiteLayout",
+getGlobalPref("game.fortnite.forceConsole") && "forceFortniteConsole",
+...STATES.userAgent.capabilities.touch ? [
+"disableTouchContextMenu"
+] : [],
+...getGlobalPref("block.tracking") ? [
+"disableAiTrack",
+"disableTelemetry",
+"blockWebRtcStatsCollector",
+"disableIndexDbLogging",
+"disableTelemetryProvider"
+] : [],
+...getGlobalPref("xhome.enabled") ? [
+"remotePlayKeepAlive",
+"remotePlayDirectConnectUrl",
+"remotePlayDisableAchievementToast",
+"remotePlayRecentlyUsedTitleIds",
+"remotePlayWebTitle",
+STATES.userAgent.capabilities.touch && "patchUpdateInputConfigurationAsync"
+] : [],
+...BX_FLAGS.EnableXcloudLogging ? [
+"enableConsoleLogging",
+"enableXcloudLogger"
+] : []
]), hideSections = getGlobalPref("ui.hideSections"), HOME_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
- hideSections.includes("news") && "ignoreNewsSection",
- hideSections.includes("friends") && "ignorePlayWithFriendsSection",
- hideSections.includes("all-games") && "ignoreAllGamesSection",
- STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",
- hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",
- ...getGlobalPref("ui.imageQuality") < 90 ? [
- "setBackgroundImageQuality"
- ] : [],
- ...blockSomeNotifications() ? [
- "changeNotificationsSubscription"
- ] : []
+hideSections.includes("news") && "ignoreNewsSection",
+hideSections.includes("friends") && "ignorePlayWithFriendsSection",
+hideSections.includes("all-games") && "ignoreAllGamesSection",
+STATES.browser.capabilities.touch && hideSections.includes("touch") && "ignorePlayWithTouchSection",
+hideSections.some((value) => ["native-mkb", "most-popular"].includes(value)) && "ignoreSiglSections",
+...getGlobalPref("ui.imageQuality") < 90 ? [
+"setBackgroundImageQuality"
+] : [],
+...blockSomeNotifications() ? [
+"changeNotificationsSubscription"
+] : []
]), STREAM_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
- "exposeInputChannel",
- "patchXcloudTitleInfo",
- "disableGamepadDisconnectedScreen",
- "patchStreamHud",
- "playVibration",
- "alwaysShowStreamHud",
- getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",
- getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",
- getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",
- ...STATES.userAgent.capabilities.touch ? [
- getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",
- getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",
- (getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",
- getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",
- getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"
- ] : [],
- BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
- "patchPollGamepads",
- getGlobalPref("stream.video.combineAudio") && "streamCombineSources",
- ...getGlobalPref("xhome.enabled") ? [
- "patchRemotePlayMkb",
- "remotePlayConnectMode"
- ] : [],
- ...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [
- "patchMouseAndKeyboardEnabled",
- "disableNativeRequestPointerLock"
- ] : []
+"exposeInputChannel",
+"patchXcloudTitleInfo",
+"disableGamepadDisconnectedScreen",
+"patchStreamHud",
+"playVibration",
+"alwaysShowStreamHud",
+getGlobalPref("audio.volume.booster.enabled") && !getGlobalPref("stream.video.combineAudio") && "patchAudioMediaStream",
+getGlobalPref("audio.volume.booster.enabled") && getGlobalPref("stream.video.combineAudio") && "patchCombinedAudioVideoMediaStream",
+getGlobalPref("ui.feedbackDialog.disabled") && "skipFeedbackDialog",
+...STATES.userAgent.capabilities.touch ? [
+getGlobalPref("touchController.mode") === "all" && "patchShowSensorControls",
+getGlobalPref("touchController.mode") === "all" && "exposeTouchLayoutManager",
+(getGlobalPref("touchController.mode") === "off" || getGlobalPref("touchController.autoOff")) && "disableTakRenderer",
+getGlobalPref("touchController.opacity.default") !== 100 && "patchTouchControlDefaultOpacity",
+getGlobalPref("touchController.mode") !== "off" && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on") && "patchBabylonRendererClass"
+] : [],
+BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
+"patchPollGamepads",
+getGlobalPref("stream.video.combineAudio") && "streamCombineSources",
+...getGlobalPref("xhome.enabled") ? [
+"patchRemotePlayMkb",
+"remotePlayConnectMode"
+] : [],
+...AppInterface && getGlobalPref("nativeMkb.mode") === "on" ? [
+"patchMouseAndKeyboardEnabled",
+"disableNativeRequestPointerLock"
+] : []
]), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = PatcherUtils.filterPatches([
- "detectProductDetailPage"
+"detectProductDetailPage"
]), ALL_PATCHES = [...PATCH_ORDERS, ...HOME_PAGE_PATCH_ORDERS, ...STREAM_PAGE_PATCH_ORDERS, ...PRODUCT_DETAIL_PAGE_PATCH_ORDERS];
class Patcher {
- static remainingPatches = {
- home: HOME_PAGE_PATCH_ORDERS,
- stream: STREAM_PAGE_PATCH_ORDERS,
- "product-detail": PRODUCT_DETAIL_PAGE_PATCH_ORDERS
- };
- static patchPage(page) {
- let remaining = Patcher.remainingPatches[page];
- if (!remaining) return;
- PATCH_ORDERS = PATCH_ORDERS.concat(remaining), delete Patcher.remainingPatches[page];
- }
- static patchNativeBind() {
- let nativeBind = Function.prototype.bind;
- Function.prototype.bind = function() {
- let valid = !1;
- if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
- if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;
- }
- if (!valid) return nativeBind.apply(this, arguments);
- if (typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;
- let orgFunc = this, newFunc = (a, item2) => {
- Patcher.checkChunks(item2), orgFunc(a, item2);
- };
- return nativeBind.apply(newFunc, arguments);
- };
- }
- static checkChunks(item) {
- let patchesToCheck, appliedPatches, chunkData = item[1], patchesMap = {}, patcherCache = PatcherCache.getInstance();
- for (let chunkId in chunkData) {
- appliedPatches = [];
- let cachedPatches = patcherCache.getPatches(chunkId);
- if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);
- else patchesToCheck = PATCH_ORDERS.slice(0);
- if (!patchesToCheck.length) continue;
- let func = chunkData[chunkId], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1;
- for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) {
- let patchName = patchesToCheck[patchIndex];
- if (appliedPatches.indexOf(patchName) > -1) continue;
- if (!PATCHES[patchName]) continue;
- let tmpStr = PATCHES[patchName].call(null, patchedFuncStr);
- if (!tmpStr) continue;
- modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS);
- }
- if (modified) {
- BX_FLAGS.Debug && console.time(LOG_TAG2);
- try {
- chunkData[chunkId] = eval(patchedFuncStr);
- } catch (e) {
- if (e instanceof Error) BxLogger.error(LOG_TAG2, "Error", appliedPatches, e.message, patchedFuncStr);
- }
- BX_FLAGS.Debug && console.timeEnd(LOG_TAG2);
- }
- if (appliedPatches.length) patchesMap[chunkId] = appliedPatches;
- }
- if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);
- }
- static init() {
- Patcher.patchNativeBind();
- }
+static remainingPatches = {
+home: HOME_PAGE_PATCH_ORDERS,
+stream: STREAM_PAGE_PATCH_ORDERS,
+"product-detail": PRODUCT_DETAIL_PAGE_PATCH_ORDERS
+};
+static patchPage(page) {
+let remaining = Patcher.remainingPatches[page];
+if (!remaining) return;
+PATCH_ORDERS = PATCH_ORDERS.concat(remaining), delete Patcher.remainingPatches[page];
+}
+static patchNativeBind() {
+let nativeBind = Function.prototype.bind;
+Function.prototype.bind = function() {
+let valid = !1;
+if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
+if (arguments[1] === 0 || typeof arguments[1] === "function") valid = !0;
+}
+if (!valid) return nativeBind.apply(this, arguments);
+if (typeof arguments[1] === "function") BxLogger.info(LOG_TAG2, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;
+let orgFunc = this, newFunc = (a, item2) => {
+Patcher.checkChunks(item2), orgFunc(a, item2);
+};
+return nativeBind.apply(newFunc, arguments);
+};
+}
+static checkChunks(item) {
+let patchesToCheck, appliedPatches, chunkData = item[1], patchesMap = {}, patcherCache = PatcherCache.getInstance();
+for (let chunkId in chunkData) {
+appliedPatches = [];
+let cachedPatches = patcherCache.getPatches(chunkId);
+if (cachedPatches) patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);
+else patchesToCheck = PATCH_ORDERS.slice(0);
+if (!patchesToCheck.length) continue;
+let func = chunkData[chunkId], funcStr = func.toString(), patchedFuncStr = funcStr, modified = !1;
+for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) {
+let patchName = patchesToCheck[patchIndex];
+if (appliedPatches.indexOf(patchName) > -1) continue;
+if (!PATCHES[patchName]) continue;
+let tmpStr = PATCHES[patchName].call(null, patchedFuncStr);
+if (!tmpStr) continue;
+modified = !0, patchedFuncStr = tmpStr, BxLogger.info(LOG_TAG2, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName), BxLogger.info(LOG_TAG2, "Remaining patches", PATCH_ORDERS);
+}
+if (modified) {
+BX_FLAGS.Debug && console.time(LOG_TAG2);
+try {
+chunkData[chunkId] = eval(patchedFuncStr);
+} catch (e) {
+if (e instanceof Error) BxLogger.error(LOG_TAG2, "Error", appliedPatches, e.message, patchedFuncStr);
+}
+BX_FLAGS.Debug && console.timeEnd(LOG_TAG2);
+}
+if (appliedPatches.length) patchesMap[chunkId] = appliedPatches;
+}
+if (Object.keys(patchesMap).length) patcherCache.saveToCache(patchesMap);
+}
+static init() {
+Patcher.patchNativeBind();
+}
}
class PatcherCache {
- static instance;
- static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache);
- KEY_CACHE = "BetterXcloud.Patches.Cache";
- KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature";
- CACHE;
- constructor() {
- this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE);
- let pathName = window.location.pathname;
- if (pathName.includes("/play/launch/")) Patcher.patchPage("stream");
- else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail");
- else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home");
- PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0));
- }
- getSignature() {
- let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]');
- if ($link) {
- let match = /\/client\.([^\.]+)\.js/.exec($link.href);
- match && (webVersion = match[1]);
- } else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? "";
- return hashCode(scriptVersion + webVersion + patches);
- }
- clear() {
- window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {};
- }
- checkSignature() {
- let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature();
- if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear();
- else BxLogger.info(LOG_TAG2, "Signature unchanged");
- }
- cleanupPatches(patches) {
- return patches.filter((item2) => {
- for (let id in this.CACHE)
- if (this.CACHE[id].includes(item2)) return !1;
- return !0;
- });
- }
- getPatches(id) {
- return this.CACHE[id];
- }
- saveToCache(subCache) {
- for (let id in subCache) {
- let patchNames = subCache[id], data = this.CACHE[id];
- if (!data) this.CACHE[id] = patchNames;
- else for (let patchName of patchNames)
- if (!data.includes(patchName)) data.push(patchName);
- }
- window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));
- }
+static instance;
+static getInstance = () => PatcherCache.instance ?? (PatcherCache.instance = new PatcherCache);
+KEY_CACHE = "BetterXcloud.Patches.Cache";
+KEY_SIGNATURE = "BetterXcloud.Patches.Cache.Signature";
+CACHE;
+constructor() {
+this.checkSignature(), this.CACHE = JSON.parse(window.localStorage.getItem(this.KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG2, "Cache", this.CACHE);
+let pathName = window.location.pathname;
+if (pathName.includes("/play/launch/")) Patcher.patchPage("stream");
+else if (pathName.includes("/play/games/")) Patcher.patchPage("product-detail");
+else if (pathName.endsWith("/play") || pathName.endsWith("/play/")) Patcher.patchPage("home");
+PATCH_ORDERS = this.cleanupPatches(PATCH_ORDERS), STREAM_PAGE_PATCH_ORDERS = this.cleanupPatches(STREAM_PAGE_PATCH_ORDERS), PRODUCT_DETAIL_PAGE_PATCH_ORDERS = this.cleanupPatches(PRODUCT_DETAIL_PAGE_PATCH_ORDERS), BxLogger.info(LOG_TAG2, "PATCH_ORDERS", PATCH_ORDERS.slice(0));
}
-class XboxApi {
- static CACHED_TITLES = {};
- static async getProductTitle(xboxTitleId) {
- if (xboxTitleId = xboxTitleId.toString(), XboxApi.CACHED_TITLES[xboxTitleId]) return XboxApi.CACHED_TITLES[xboxTitleId];
- try {
- let url = `https://displaycatalog.mp.microsoft.com/v7.0/products/lookup?market=US&languages=en&value=${xboxTitleId}&alternateId=XboxTitleId&fieldsTemplate=browse`, productTitle = (await (await NATIVE_FETCH(url)).json()).Products[0].LocalizedProperties[0].ProductTitle;
- return XboxApi.CACHED_TITLES[xboxTitleId] = productTitle, productTitle;
- } catch (e) {}
- return;
- }
+getSignature() {
+let scriptVersion = SCRIPT_VERSION, patches = JSON.stringify(ALL_PATCHES), webVersion = "", $link = document.querySelector('link[data-chunk="client"][href*="/client."]');
+if ($link) {
+let match = /\/client\.([^\.]+)\.js/.exec($link.href);
+match && (webVersion = match[1]);
+} else webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content ?? "";
+return hashCode(scriptVersion + webVersion + patches);
}
-class SettingsManager {
- static instance;
- static getInstance = () => SettingsManager.instance ?? (SettingsManager.instance = new SettingsManager);
- $streamSettingsSelection;
- $tips;
- playingGameId = -1;
- targetGameId = -1;
- SETTINGS = {
- "localCoOp.enabled": {
- onChange: () => {
- BxExposed.toggleLocalCoOp(getStreamPref("localCoOp.enabled"));
- }
- },
- "deviceVibration.mode": {
- onChange: StreamSettings.refreshControllerSettings
- },
- "deviceVibration.intensity": {
- onChange: StreamSettings.refreshControllerSettings
- },
- "controller.pollingRate": {
- onChange: StreamSettings.refreshControllerSettings
- },
- "controller.settings": {
- onChange: StreamSettings.refreshControllerSettings
- },
- "nativeMkb.scroll.sensitivityX": {
- onChange: () => {
- let value = getStreamPref("nativeMkb.scroll.sensitivityX");
- NativeMkbHandler.getInstance()?.setHorizontalScrollMultiplier(value / 100);
- }
- },
- "nativeMkb.scroll.sensitivityY": {
- onChange: () => {
- let value = getStreamPref("nativeMkb.scroll.sensitivityY");
- NativeMkbHandler.getInstance()?.setVerticalScrollMultiplier(value / 100);
- }
- },
- "video.player.type": {
- onChange: onChangeVideoPlayerType,
- alwaysTriggerOnChange: !0
- },
- "video.player.powerPreference": {
- onChange: () => {
- let streamPlayer = STATES.currentStream.streamPlayer;
- if (!streamPlayer) return;
- streamPlayer.reloadPlayer(), updateVideoPlayer();
- }
- },
- "video.processing": {
- onChange: updateVideoPlayer
- },
- "video.processing.sharpness": {
- onChange: updateVideoPlayer
- },
- "video.maxFps": {
- onChange: () => {
- let value = getStreamPref("video.maxFps");
- limitVideoPlayerFps(value);
- }
- },
- "video.ratio": {
- onChange: updateVideoPlayer
- },
- "video.brightness": {
- onChange: updateVideoPlayer
- },
- "video.contrast": {
- onChange: updateVideoPlayer
- },
- "video.saturation": {
- onChange: updateVideoPlayer
- },
- "video.position": {
- onChange: updateVideoPlayer
- },
- "audio.volume": {
- onChange: () => {
- let value = getStreamPref("audio.volume");
- SoundShortcut.setGainNodeVolume(value);
- }
- },
- "stats.items": {
- onChange: StreamStats.refreshStyles
- },
- "stats.quickGlance.enabled": {
- onChange: () => {
- let value = getStreamPref("stats.quickGlance.enabled"), streamStats = StreamStats.getInstance();
- value ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
- }
- },
- "stats.position": {
- onChange: StreamStats.refreshStyles
- },
- "stats.textSize": {
- onChange: StreamStats.refreshStyles
- },
- "stats.opacity.all": {
- onChange: StreamStats.refreshStyles
- },
- "stats.opacity.background": {
- onChange: StreamStats.refreshStyles
- },
- "stats.colors": {
- onChange: StreamStats.refreshStyles
- },
- "mkb.p1.preset.mappingId": {
- onChange: StreamSettings.refreshMkbSettings
- },
- "mkb.p1.slot": {
- onChange: () => {
- EmulatedMkbHandler.getInstance()?.resetXcloudGamepads();
- }
- },
- "keyboardShortcuts.preset.inGameId": {
- onChange: StreamSettings.refreshKeyboardShortcuts
- }
- };
- constructor() {
- BxEventBus.Stream.on("setting.changed", (data) => {
- if (isStreamPref(data.settingKey)) this.updateStreamElement(data.settingKey);
- }), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
- this.switchGameSettings(id);
- }), this.renderStreamSettingsSelection();
- }
- updateStreamElement(key, onChanges) {
- let info = this.SETTINGS[key];
- if (info.onChange && (STATES.isPlaying || info.alwaysTriggerOnChange)) if (onChanges) onChanges.add(info.onChange);
- else info.onChange();
- let $elm = info.$element;
- if (!$elm) return;
- let value = getGamePref(this.targetGameId, key, !0);
- if ("setValue" in $elm) $elm.setValue(value);
- else $elm.value = value.toString();
- this.updateDataset($elm, key);
- }
- switchGameSettings(id) {
- if (setGameIdPref(id), this.targetGameId === id) return;
- let onChanges = new Set, oldGameId = this.targetGameId;
- this.targetGameId = id;
- let key;
- for (key in this.SETTINGS) {
- if (!isStreamPref(key)) continue;
- let oldValue = getGamePref(oldGameId, key, !0, !0), newValue = getGamePref(this.targetGameId, key, !0, !0);
- if (oldValue === newValue) continue;
- this.updateStreamElement(key, onChanges);
- }
- onChanges.forEach((onChange) => {
- onChange && onChange();
- }), this.$tips.classList.toggle("bx-gone", id < 0);
- }
- setElement(pref, $elm) {
- if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
- this.updateDataset($elm, pref), this.SETTINGS[pref].$element = $elm;
- }
- getElement(pref, params) {
- if (!this.SETTINGS[pref]) this.SETTINGS[pref] = {};
- let $elm = this.SETTINGS[pref].$element;
- if (!$elm) $elm = SettingElement.fromPref(pref, null, params), this.SETTINGS[pref].$element = $elm;
- return this.updateDataset($elm, pref), $elm;
- }
- updateDataset($elm, pref) {
- if (this.targetGameId === this.playingGameId && hasGamePref(this.playingGameId, pref)) $elm.dataset.override = "true";
- else delete $elm.dataset.override;
- }
- renderStreamSettingsSelection() {
- this.$tips = CE("p", { class: "bx-gone" }, "⇐ Q ⟶: Reset highlighted setting");
- let $select = BxSelectElement.create(CE("select", !1, CE("optgroup", { label: ut("Settings for") }, CE("option", { value: -1 }, t("tc-all-games")))), !0);
- $select.addEventListener("input", (e) => {
- let id = parseInt($select.value);
- BxEventBus.Stream.emit("gameSettings.switched", { id });
- }), this.$streamSettingsSelection = CE("div", {
- class: "bx-stream-settings-selection bx-gone",
- _nearby: { orientation: "vertical" }
- }, CE("div", !1, $select), this.$tips), BxEventBus.Stream.on("xboxTitleId.changed", async ({ id }) => {
- this.playingGameId = id, setGameIdPref(id);
- let $optGroup = $select.querySelector("optgroup");
- while ($optGroup.childElementCount > 1)
- $optGroup.lastElementChild?.remove();
- if (id >= 0) {
- let title = id === 0 ? ut("Xbox") : await XboxApi.getProductTitle(id);
- $optGroup.appendChild(CE("option", {
- value: id
- }, title)), $select.value = id.toString();
- } else $select.value = "-1";
- BxEventBus.Stream.emit("gameSettings.switched", { id });
- });
- }
- getStreamSettingsSelection() {
- return this.$streamSettingsSelection;
- }
- getTargetGameId() {
- return this.targetGameId;
- }
+clear() {
+window.localStorage.removeItem(this.KEY_CACHE), this.CACHE = {};
+}
+checkSignature() {
+let storedSig = window.localStorage.getItem(this.KEY_SIGNATURE) || 0, currentSig = this.getSignature();
+if (currentSig !== parseInt(storedSig)) BxLogger.warning(LOG_TAG2, "Signature changed"), window.localStorage.setItem(this.KEY_SIGNATURE, currentSig.toString()), this.clear();
+else BxLogger.info(LOG_TAG2, "Signature unchanged");
+}
+cleanupPatches(patches) {
+return patches.filter((item2) => {
+for (let id in this.CACHE)
+if (this.CACHE[id].includes(item2)) return !1;
+return !0;
+});
+}
+getPatches(id) {
+return this.CACHE[id];
+}
+saveToCache(subCache) {
+for (let id in subCache) {
+let patchNames = subCache[id], data = this.CACHE[id];
+if (!data) this.CACHE[id] = patchNames;
+else for (let patchName of patchNames)
+if (!data.includes(patchName)) data.push(patchName);
+}
+window.localStorage.setItem(this.KEY_CACHE, JSON.stringify(this.CACHE));
}
-class SettingElement {
- static renderOptions(key, setting, currentValue, onChange) {
- let $control = CE("select", {
- tabindex: 0
- }), $parent;
- if (setting.optionsGroup) $parent = CE("optgroup", {
- label: setting.optionsGroup
- }), $control.appendChild($parent);
- else $parent = $control;
- for (let value in setting.options) {
- let label = setting.options[value], $option = CE("option", { value }, label);
- $parent.appendChild($option);
- }
- return $control.value = currentValue, onChange && $control.addEventListener("input", (e) => {
- let target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
- !e.ignoreOnChange && onChange(e, value);
- }), $control.setValue = (value) => {
- $control.value = value;
- }, $control;
- }
- static renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
- let $control = CE("select", {
- multiple: !0,
- tabindex: 0
- }), totalOptions = Object.keys(setting.multipleOptions).length, size = params.size ? Math.min(params.size, totalOptions) : totalOptions;
- $control.setAttribute("size", size.toString());
- for (let value in setting.multipleOptions) {
- let label = setting.multipleOptions[value], $option = CE("option", { value }, label);
- $option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {
- e.preventDefault();
- let target = e.target;
- target.selected = !target.selected;
- let $parent = target.parentElement;
- $parent.focus(), BxEvent.dispatch($parent, "input");
- }), $control.appendChild($option);
- }
- return $control.addEventListener("mousedown", function(e) {
- let self = this, orgScrollTop = self.scrollTop;
- window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
- }), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("input", (e) => {
- let target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);
- !e.ignoreOnChange && onChange(e, values);
- }), Object.defineProperty($control, "value", {
- get() {
- return Array.from($control.options).filter((option) => option.selected).map((option) => option.value);
- },
- set(value) {
- let values = value.split(",");
- Array.from($control.options).forEach((option) => {
- option.selected = values.includes(option.value);
- });
- }
- }), $control;
- }
- static renderCheckbox(key, setting, currentValue, onChange) {
- let $control = CE("input", { type: "checkbox", tabindex: 0 });
- return $control.checked = currentValue, onChange && $control.addEventListener("input", (e) => {
- !e.ignoreOnChange && onChange(e, e.target.checked);
- }), $control.setValue = (value) => {
- $control.checked = !!value;
- }, $control;
- }
- static renderNumberStepper(key, setting, value, onChange, options = {}) {
- return BxNumberStepper.create(key, value, setting.min, setting.max, options, onChange);
- }
- static METHOD_MAP = {
- options: SettingElement.renderOptions,
- "multiple-options": SettingElement.renderMultipleOptions,
- "number-stepper": SettingElement.renderNumberStepper,
- checkbox: SettingElement.renderCheckbox
- };
- static render(type, key, setting, currentValue, onChange, options) {
- let method = SettingElement.METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));
- if (type !== "number-stepper") $control.id = `bx_setting_${escapeCssSelector(key)}`;
- if (type === "options" || type === "multiple-options") $control.name = $control.id;
- return $control;
- }
- static fromPref(key, onChange, overrideParams = {}) {
- let { definition, storage } = getPrefInfo(key);
- if (!definition) return null;
- let currentValue = storage.getSetting(key), type;
- if ("options" in definition) type = "options";
- else if ("multipleOptions" in definition) type = "multiple-options";
- else if (typeof definition.default === "number") type = "number-stepper";
- else type = "checkbox";
- let params = {};
- if ("params" in definition) params = Object.assign(overrideParams, definition.params || {});
- if (params.disabled) currentValue = definition.default;
- return SettingElement.render(type, key, definition, currentValue, (e, value) => {
- if (isGlobalPref(key)) setGlobalPref(key, value, "ui");
- else {
- let id = SettingsManager.getInstance().getTargetGameId();
- setGamePref(id, key, value, "ui");
- }
- onChange && onChange(e, value);
- }, params);
- }
}
class FullscreenText {
- static instance;
- static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
- LOG_TAG = "FullscreenText";
- $text;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {
- class: "bx-fullscreen-text bx-gone"
- }), document.documentElement.appendChild(this.$text);
- }
- show(msg) {
- document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg;
- }
- hide() {
- document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone");
- }
+static instance;
+static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
+LOG_TAG = "FullscreenText";
+$text;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$text = CE("div", {
+class: "bx-fullscreen-text bx-gone"
+}), document.documentElement.appendChild(this.$text);
+}
+show(msg) {
+document.body.classList.add("bx-no-scroll"), this.$text.classList.remove("bx-gone"), this.$text.textContent = msg;
+}
+hide() {
+document.body.classList.remove("bx-no-scroll"), this.$text.classList.add("bx-gone");
+}
}
class BaseProfileManagerDialog extends NavigationDialog {
- $container;
- title;
- presetsDb;
- allPresets;
- currentPresetId = null;
- activatedPresetId = null;
- $presets;
- $header;
- $defaultNote;
- $content;
- $btnRename;
- $btnDelete;
- constructor(title, presetsDb) {
- super();
- this.title = title, this.presetsDb = presetsDb;
- }
- async renderSummary(presetId) {
- return null;
- }
- updateButtonStates() {
- let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0;
- this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset);
- }
- async renderPresetsList() {
- if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0];
- renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 });
- }
- promptNewName(action, value = "") {
- let newName = "";
- while (!newName) {
- if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1;
- newName = newName.trim();
- }
- return newName ? newName : !1;
- }
- async renderDialog() {
- this.$presets = CE("select", {
- class: "bx-full-width",
- tabindex: -1
- });
- let $select = BxSelectElement.create(this.$presets);
- $select.addEventListener("input", (e) => {
- this.switchPreset(parseInt($select.value));
- });
- let $header = CE("div", {
- class: "bx-dialog-preset-tools",
- _nearby: {
- orientation: "horizontal",
- focus: $select
- }
- }, $select, this.$btnRename = createButton({
- title: t("rename"),
- icon: BxIcon.CURSOR_TEXT,
- style: 64,
- onClick: async () => {
- let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name);
- if (!newName) return;
- preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh();
- }
- }), this.$btnDelete = createButton({
- icon: BxIcon.TRASH,
- title: t("delete"),
- style: 4 | 64,
- onClick: async (e) => {
- if (!confirm(t("confirm-delete-preset"))) return;
- await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh();
- }
- }), createButton({
- icon: BxIcon.NEW,
- title: t("new"),
- style: 64 | 1,
- onClick: async (e) => {
- let newName = this.promptNewName(t("new"));
- if (!newName) return;
- let newId = await this.presetsDb.newPreset(newName, this.presetsDb.BLANK_PRESET_DATA);
- this.currentPresetId = newId, await this.refresh();
- }
- }), createButton({
- icon: BxIcon.COPY,
- title: t("copy"),
- style: 64 | 1,
- onClick: async (e) => {
- let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`);
- if (!newName) return;
- let newId = await this.presetsDb.newPreset(newName, preset.data);
- this.currentPresetId = newId, await this.refresh();
- }
- }));
- this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, this.title), createButton({
- icon: BxIcon.CLOSE,
- style: 64 | 2048 | 8,
- onClick: (e) => this.hide()
- })), CE("div", !1, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content));
- }
- async refresh() {
- await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 });
- }
- async onBeforeMount(configs = {}) {
- await this.renderPresetsList();
- let valid = !1;
- if (typeof configs?.id === "number") {
- if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0;
- }
- if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null;
- this.refresh();
- }
- getDialog() {
- return this;
- }
- getContent() {
- if (!this.$container) this.renderDialog();
- return this.$container;
- }
- focusIfNeeded() {
- this.dialogManager.focus(this.$header);
- }
+$container;
+title;
+presetsDb;
+allPresets;
+currentPresetId = null;
+activatedPresetId = null;
+$presets;
+$header;
+$defaultNote;
+$content;
+$btnRename;
+$btnDelete;
+constructor(title, presetsDb) {
+super();
+this.title = title, this.presetsDb = presetsDb;
+}
+async renderSummary(presetId) {
+return null;
+}
+updateButtonStates() {
+let isDefaultPreset = this.currentPresetId === null || this.currentPresetId <= 0;
+this.$btnRename.disabled = isDefaultPreset, this.$btnDelete.disabled = isDefaultPreset, this.$defaultNote.classList.toggle("bx-gone", !isDefaultPreset);
+}
+async renderPresetsList() {
+if (this.allPresets = await this.presetsDb.getPresets(), this.currentPresetId === null) this.currentPresetId = this.allPresets.default[0];
+renderPresetsList(this.$presets, this.allPresets, this.activatedPresetId, { selectedIndicator: !0 });
+}
+promptNewName(action, value = "") {
+let newName = "";
+while (!newName) {
+if (newName = prompt(`[${action}] ${t("prompt-preset-name")}`, value), newName === null) return !1;
+newName = newName.trim();
+}
+return newName ? newName : !1;
+}
+async renderDialog() {
+this.$presets = CE("select", {
+class: "bx-full-width",
+tabindex: -1
+});
+let $select = BxSelectElement.create(this.$presets);
+$select.addEventListener("input", (e) => {
+this.switchPreset(parseInt($select.value));
+});
+let $header = CE("div", {
+class: "bx-dialog-preset-tools",
+_nearby: {
+orientation: "horizontal",
+focus: $select
+}
+}, $select, this.$btnRename = createButton({
+title: t("rename"),
+icon: BxIcon.CURSOR_TEXT,
+style: 64,
+onClick: async () => {
+let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("rename"), preset.name);
+if (!newName) return;
+preset.name = newName, await this.presetsDb.updatePreset(preset), await this.refresh();
+}
+}), this.$btnDelete = createButton({
+icon: BxIcon.TRASH,
+title: t("delete"),
+style: 4 | 64,
+onClick: async (e) => {
+if (!confirm(t("confirm-delete-preset"))) return;
+await this.presetsDb.deletePreset(this.currentPresetId), delete this.allPresets.data[this.currentPresetId], this.currentPresetId = parseInt(Object.keys(this.allPresets.data)[0]), await this.refresh();
+}
+}), createButton({
+icon: BxIcon.NEW,
+title: t("new"),
+style: 64 | 1,
+onClick: async (e) => {
+let newName = this.promptNewName(t("new"));
+if (!newName) return;
+let newId = await this.presetsDb.newPreset(newName, this.presetsDb.BLANK_PRESET_DATA);
+this.currentPresetId = newId, await this.refresh();
+}
+}), createButton({
+icon: BxIcon.COPY,
+title: t("copy"),
+style: 64 | 1,
+onClick: async (e) => {
+let preset = this.allPresets.data[this.currentPresetId], newName = this.promptNewName(t("copy"), `${preset.name} (2)`);
+if (!newName) return;
+let newId = await this.presetsDb.newPreset(newName, preset.data);
+this.currentPresetId = newId, await this.refresh();
+}
+}));
+this.$header = $header, this.$container = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, this.title), createButton({
+icon: BxIcon.CLOSE,
+style: 64 | 2048 | 8,
+onClick: (e) => this.hide()
+})), CE("div", !1, $header, this.$defaultNote = CE("div", { class: "bx-default-preset-note bx-gone" }, t("default-preset-note"))), CE("div", { class: "bx-dialog-content" }, this.$content));
+}
+async refresh() {
+await this.renderPresetsList(), this.$presets.value = this.currentPresetId.toString(), BxEvent.dispatch(this.$presets, "input", { manualTrigger: !0 });
+}
+async onBeforeMount(configs = {}) {
+await this.renderPresetsList();
+let valid = !1;
+if (typeof configs?.id === "number") {
+if (configs.id in this.allPresets.data) this.currentPresetId = configs.id, this.activatedPresetId = configs.id, valid = !0;
+}
+if (!valid) this.currentPresetId = this.allPresets.default[0], this.activatedPresetId = null;
+this.refresh();
+}
+getDialog() {
+return this;
+}
+getContent() {
+if (!this.$container) this.renderDialog();
+return this.$container;
+}
+focusIfNeeded() {
+this.dialogManager.focus(this.$header);
+}
}
var SHORTCUT_ACTIONS = {
- [t("better-xcloud")]: {
- "bx.settings.show": [t("settings"), t("show")]
- },
- ...STATES.browser.capabilities.mkb ? {
- [t("mouse-and-keyboard")]: {
- "mkb.toggle": [t("toggle")]
- }
- } : {},
- [t("controller")]: {
- "controller.xbox.press": [t("button-xbox"), t("press")]
- },
- ...AppInterface ? {
- [t("device")]: {
- "device.sound.toggle": [t("sound"), t("toggle")],
- "device.volume.inc": [t("volume"), t("increase")],
- "device.volume.dec": [t("volume"), t("decrease")],
- "device.brightness.inc": [t("brightness"), t("increase")],
- "device.brightness.dec": [t("brightness"), t("decrease")]
- }
- } : {},
- [t("stream")]: {
- "stream.screenshot.capture": [t("take-screenshot")],
- "stream.video.toggle": [t("video"), t("toggle")],
- "stream.sound.toggle": [t("sound"), t("toggle")],
- ...getGlobalPref("audio.volume.booster.enabled") ? {
- "stream.volume.inc": [t("volume"), t("increase")],
- "stream.volume.dec": [t("volume"), t("decrease")]
- } : {},
- "stream.menu.show": [t("menu"), t("show")],
- "stream.stats.toggle": [t("stats"), t("show-hide")],
- "stream.microphone.toggle": [t("microphone"), t("toggle")]
- },
- [t("other")]: {
- "ta.open": [t("true-achievements"), t("show")]
- }
+[t("better-xcloud")]: {
+"bx.settings.show": [t("settings"), t("show")]
+},
+...STATES.browser.capabilities.mkb ? {
+[t("mouse-and-keyboard")]: {
+"mkb.toggle": [t("toggle")]
+}
+} : {},
+[t("controller")]: {
+"controller.xbox.press": [t("button-xbox"), t("press")]
+},
+...AppInterface ? {
+[t("device")]: {
+"device.sound.toggle": [t("sound"), t("toggle")],
+"device.volume.inc": [t("volume"), t("increase")],
+"device.volume.dec": [t("volume"), t("decrease")],
+"device.brightness.inc": [t("brightness"), t("increase")],
+"device.brightness.dec": [t("brightness"), t("decrease")]
+}
+} : {},
+[t("stream")]: {
+"stream.screenshot.capture": [t("take-screenshot")],
+"stream.video.toggle": [t("video"), t("toggle")],
+"stream.sound.toggle": [t("sound"), t("toggle")],
+...getGlobalPref("audio.volume.booster.enabled") ? {
+"stream.volume.inc": [t("volume"), t("increase")],
+"stream.volume.dec": [t("volume"), t("decrease")]
+} : {},
+"stream.menu.show": [t("menu"), t("show")],
+"stream.stats.toggle": [t("stats"), t("show-hide")],
+"stream.microphone.toggle": [t("microphone"), t("toggle")]
+},
+[t("other")]: {
+"ta.open": [t("true-achievements"), t("show")]
+}
};
class ControllerShortcutsManagerDialog extends BaseProfileManagerDialog {
- static instance;
- static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts")));
- $content;
- selectActions = {};
- BUTTONS_ORDER = [
- 3,
- 0,
- 2,
- 1,
- 12,
- 13,
- 14,
- 15,
- 8,
- 9,
- 4,
- 5,
- 6,
- 7,
- 10,
- 11
- ];
- constructor(title) {
- super(title, ControllerShortcutsTable.getInstance());
- let $baseSelect = CE("select", {
- class: "bx-full-width",
- autocomplete: "off"
- }, CE("option", { value: "" }, "---"));
- for (let groupLabel in SHORTCUT_ACTIONS) {
- let items = SHORTCUT_ACTIONS[groupLabel];
- if (!items) continue;
- let $optGroup = CE("optgroup", { label: groupLabel });
- for (let action in items) {
- let crumbs = items[action];
- if (!crumbs) continue;
- let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label);
- $optGroup.appendChild($option);
- }
- $baseSelect.appendChild($optGroup);
- }
- let $content = CE("div", {
- class: "bx-controller-shortcuts-manager-container"
- }), onActionChanged = (e) => {
- if (!e.ignoreOnChange) this.updatePreset();
- }, fragment = document.createDocumentFragment();
- fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")));
- for (let button of this.BUTTONS_ORDER) {
- let prompt2 = GamepadKeyName[button][1], $row = CE("div", {
- class: "bx-shortcut-row",
- _nearby: {
- orientation: "horizontal"
- }
- }), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0));
- $select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, {
- focus: $select
- }), $row.append($label, $select), fragment.appendChild($row);
- }
- $content.appendChild(fragment), this.$content = $content;
- }
- switchPreset(id) {
- let preset = this.allPresets.data[id];
- if (!preset) {
- this.currentPresetId = 0;
- return;
- }
- this.currentPresetId = id;
- let isDefaultPreset = id <= 0, actions = preset.data, button;
- for (button in this.selectActions) {
- let $select = this.selectActions[button];
- $select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {
- ignoreOnChange: !0,
- manualTrigger: !0
- });
- }
- super.updateButtonStates();
- }
- updatePreset() {
- let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), button;
- for (button in this.selectActions) {
- let action = this.selectActions[button].value;
- if (!action) continue;
- newData.mapping[button] = action;
- }
- let preset = this.allPresets.data[this.currentPresetId];
- preset.data = newData, this.presetsDb.updatePreset(preset);
- }
- onBeforeUnmount() {
- StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();
- }
+static instance;
+static getInstance = () => ControllerShortcutsManagerDialog.instance ?? (ControllerShortcutsManagerDialog.instance = new ControllerShortcutsManagerDialog(t("controller-shortcuts")));
+$content;
+selectActions = {};
+BUTTONS_ORDER = [
+3,
+0,
+2,
+1,
+12,
+13,
+14,
+15,
+8,
+9,
+4,
+5,
+6,
+7,
+10,
+11
+];
+constructor(title) {
+super(title, ControllerShortcutsTable.getInstance());
+let $baseSelect = CE("select", {
+class: "bx-full-width",
+autocomplete: "off"
+}, CE("option", { value: "" }, "---"));
+for (let groupLabel in SHORTCUT_ACTIONS) {
+let items = SHORTCUT_ACTIONS[groupLabel];
+if (!items) continue;
+let $optGroup = CE("optgroup", { label: groupLabel });
+for (let action in items) {
+let crumbs = items[action];
+if (!crumbs) continue;
+let label = crumbs.join(" ❯ "), $option = CE("option", { value: action }, label);
+$optGroup.appendChild($option);
+}
+$baseSelect.appendChild($optGroup);
+}
+let $content = CE("div", {
+class: "bx-controller-shortcuts-manager-container"
+}), onActionChanged = (e) => {
+if (!e.ignoreOnChange) this.updatePreset();
+}, fragment = document.createDocumentFragment();
+fragment.appendChild(CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, ""), ": " + t("controller-shortcuts-xbox-note")));
+for (let button of this.BUTTONS_ORDER) {
+let prompt2 = GamepadKeyName[button][1], $row = CE("div", {
+class: "bx-shortcut-row",
+_nearby: {
+orientation: "horizontal"
+}
+}), $label = CE("label", { class: "bx-prompt" }, `${""}${prompt2}`), $select = BxSelectElement.create($baseSelect.cloneNode(!0));
+$select.dataset.button = button.toString(), $select.addEventListener("input", onActionChanged), this.selectActions[button] = $select, setNearby($row, {
+focus: $select
+}), $row.append($label, $select), fragment.appendChild($row);
+}
+$content.appendChild(fragment), this.$content = $content;
+}
+switchPreset(id) {
+let preset = this.allPresets.data[id];
+if (!preset) {
+this.currentPresetId = 0;
+return;
+}
+this.currentPresetId = id;
+let isDefaultPreset = id <= 0, actions = preset.data, button;
+for (button in this.selectActions) {
+let $select = this.selectActions[button];
+$select.value = actions.mapping[button] || "", $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {
+ignoreOnChange: !0,
+manualTrigger: !0
+});
+}
+super.updateButtonStates();
+}
+updatePreset() {
+let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), button;
+for (button in this.selectActions) {
+let action = this.selectActions[button].value;
+if (!action) continue;
+newData.mapping[button] = action;
+}
+let preset = this.allPresets.data[this.currentPresetId];
+preset.data = newData, this.presetsDb.updatePreset(preset);
+}
+onBeforeUnmount() {
+StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();
+}
}
class BxDualNumberStepper extends HTMLInputElement {
- controlValues;
- controlMin;
- controlMinDiff;
- controlMax;
- steps;
- options;
- onChange;
- $text;
- $rangeFrom;
- $rangeTo;
- $activeRange;
- onRangeInput;
- setValue;
- getValue;
- normalizeValue;
- static create(key, values, options, onChange) {
- options.suffix = options.suffix || "", options.disabled = !!options.disabled;
- let $text, $rangeFrom, $rangeTo, self = CE("div", {
- class: "bx-dual-number-stepper",
- id: `bx_setting_${escapeCssSelector(key)}`
- }, $text = CE("span"));
- if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self;
- return $rangeFrom = CE("input", {
- type: "range",
- min: self.controlMin,
- max: self.controlMax,
- step: self.steps,
- tabindex: 0
- }), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", !1, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, {
- focus: $rangeFrom,
- orientation: "vertical"
- }), Object.defineProperty(self, "value", {
- get() {
- return self.controlValues;
- },
- set(value) {
- let from, to;
- if (typeof value === "string") {
- let tmp = value.split(",");
- from = parseInt(tmp[0]), to = parseInt(tmp[1]);
- } else if (Array.isArray(value)) [from, to] = value;
- if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]);
- }
- }), self;
- }
- static setValues(values) {
- let from, to;
- if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values);
- else from = this.controlMin, to = this.controlMax, values = [from, to];
- this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString();
- let ratio = 100 / (this.controlMax - this.controlMin);
- this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%");
- }
- static getValues() {
- return this.controlValues || [this.controlMin, this.controlMax];
- }
- static normalizeValues(values) {
- let [from, to] = values;
- if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from);
- else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to);
- return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to];
- }
- static onRangeInput(e) {
- this.$activeRange = e.target;
- let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]);
- if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values);
- }
- static onContextMenu(e) {
- e.preventDefault();
- }
- static updateTextValue() {
- let values = this.controlValues, textContent = null;
- if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax);
- if (textContent === null) {
- let [from, to] = values;
- if (from === this.controlMin && to === this.controlMax) textContent = t("default");
- else {
- let pad = to.toString().length;
- textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`;
- }
- }
- return textContent;
- }
+controlValues;
+controlMin;
+controlMinDiff;
+controlMax;
+steps;
+options;
+onChange;
+$text;
+$rangeFrom;
+$rangeTo;
+$activeRange;
+onRangeInput;
+setValue;
+getValue;
+normalizeValue;
+static create(key, values, options, onChange) {
+options.suffix = options.suffix || "", options.disabled = !!options.disabled;
+let $text, $rangeFrom, $rangeTo, self = CE("div", {
+class: "bx-dual-number-stepper",
+id: `bx_setting_${escapeCssSelector(key)}`
+}, $text = CE("span"));
+if (self.$text = $text, self.onChange = onChange, self.onRangeInput = BxDualNumberStepper.onRangeInput.bind(self), self.controlMin = options.min, self.controlMax = options.max, self.controlMinDiff = options.minDiff, self.options = options, self.steps = Math.max(options.steps || 1, 1), options.disabled) return self.disabled = !0, self;
+return $rangeFrom = CE("input", {
+type: "range",
+min: self.controlMin,
+max: self.controlMax,
+step: self.steps,
+tabindex: 0
+}), $rangeTo = $rangeFrom.cloneNode(), self.$rangeFrom = $rangeFrom, self.$rangeTo = $rangeTo, self.$activeRange = $rangeFrom, self.getValue = BxDualNumberStepper.getValues.bind(self), self.setValue = BxDualNumberStepper.setValues.bind(self), $rangeFrom.addEventListener("input", self.onRangeInput), $rangeTo.addEventListener("input", self.onRangeInput), self.addEventListener("input", self.onRangeInput), self.append(CE("div", !1, $rangeFrom, $rangeTo)), BxDualNumberStepper.setValues.call(self, values), self.addEventListener("contextmenu", BxDualNumberStepper.onContextMenu), setNearby(self, {
+focus: $rangeFrom,
+orientation: "vertical"
+}), Object.defineProperty(self, "value", {
+get() {
+return self.controlValues;
+},
+set(value) {
+let from, to;
+if (typeof value === "string") {
+let tmp = value.split(",");
+from = parseInt(tmp[0]), to = parseInt(tmp[1]);
+} else if (Array.isArray(value)) [from, to] = value;
+if (typeof from !== "undefined" && typeof to !== "undefined") BxDualNumberStepper.setValues.call(self, [from, to]);
+}
+}), self;
+}
+static setValues(values) {
+let from, to;
+if (values) [from, to] = BxDualNumberStepper.normalizeValues.call(this, values);
+else from = this.controlMin, to = this.controlMax, values = [from, to];
+this.controlValues = [from, to], this.$text.textContent = BxDualNumberStepper.updateTextValue.call(this), this.$rangeFrom.value = from.toString(), this.$rangeTo.value = to.toString();
+let ratio = 100 / (this.controlMax - this.controlMin);
+this.style.setProperty("--from", ratio * (from - this.controlMin) + "%"), this.style.setProperty("--to", ratio * (to - this.controlMin) + "%");
+}
+static getValues() {
+return this.controlValues || [this.controlMin, this.controlMax];
+}
+static normalizeValues(values) {
+let [from, to] = values;
+if (this.$activeRange === this.$rangeFrom) to = Math.min(this.controlMax, to), from = Math.min(from, to), from = Math.min(to - this.controlMinDiff, from);
+else from = Math.max(this.controlMin, from), to = Math.max(from, to), to = Math.max(this.controlMinDiff + from, to);
+return to = Math.min(this.controlMax, to), from = Math.min(from, to), [from, to];
+}
+static onRangeInput(e) {
+this.$activeRange = e.target;
+let values = BxDualNumberStepper.normalizeValues.call(this, [parseInt(this.$rangeFrom.value), parseInt(this.$rangeTo.value)]);
+if (BxDualNumberStepper.setValues.call(this, values), !e.ignoreOnChange && this.onChange) this.onChange(e, values);
+}
+static onContextMenu(e) {
+e.preventDefault();
+}
+static updateTextValue() {
+let values = this.controlValues, textContent = null;
+if (this.options.customTextValue) textContent = this.options.customTextValue(values, this.controlMin, this.controlMax);
+if (textContent === null) {
+let [from, to] = values;
+if (from === this.controlMin && to === this.controlMax) textContent = t("default");
+else {
+let pad = to.toString().length;
+textContent = `${from.toString().padStart(pad)} - ${to.toString().padEnd(pad)}${this.options.suffix}`;
+}
+}
+return textContent;
+}
}
class ControllerCustomizationsManagerDialog extends BaseProfileManagerDialog {
- static instance;
- static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization")));
- $vibrationIntensity;
- $leftTriggerRange;
- $rightTriggerRange;
- $leftStickDeadzone;
- $rightStickDeadzone;
- $btnDetect;
- selectsMap = {};
- selectsOrder = [];
- isDetectingButton = !1;
- detectIntervalId = null;
- static BUTTONS_ORDER = [
- 0,
- 1,
- 2,
- 3,
- 12,
- 15,
- 13,
- 14,
- 4,
- 5,
- 6,
- 7,
- 10,
- 11,
- 104,
- 204,
- 8,
- 9,
- 17
- ];
- constructor(title) {
- super(title, ControllerCustomizationsTable.getInstance());
- this.render();
- }
- render() {
- let isControllerFriendly = getGlobalPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => {
- if (!e.ignoreOnChange) this.updatePreset();
- }, boundUpdatePreset = this.updatePreset.bind(this);
- for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
- if (gamepadKey === 17) continue;
- let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0];
- (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", {
- value: gamepadKey,
- _dataset: { label: GamepadKeyName[gamepadKey][1] }
- }, name));
- }
- for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
- let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0);
- $clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove();
- let $select = BxSelectElement.create($clonedSelect);
- $select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey);
- let $row = CE("div", {
- class: "bx-controller-key-row",
- _nearby: { orientation: "horizontal" }
- }, CE("label", { title: buttonName }, buttonPrompt), $select);
- $rows.append($row);
- }
- if (getGlobalPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) {
- let $select = this.selectsMap[this.selectsOrder[i]], directions = {
- 1: i - 2,
- 3: i + 2,
- 4: i - 1,
- 2: i + 1
- };
- for (let dir in directions) {
- let idx = directions[dir];
- if (typeof this.selectsOrder[idx] === "undefined") continue;
- let $targetSelect = this.selectsMap[this.selectsOrder[idx]];
- setNearby($select, {
- [dir]: $targetSelect
- });
- }
- }
- let blankSettings = this.presetsDb.BLANK_PRESET_DATA.settings, params = {
- min: 0,
- minDiff: 1,
- max: 100,
- steps: 1
- };
- this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({
- label: t("detect-controller-button"),
- classes: ["bx-btn-detect"],
- style: 4096 | 64 | 128,
- onClick: () => {
- this.startDetectingButton();
- }
- }), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, {
- steps: 10,
- suffix: "%",
- exactTicks: 20,
- customTextValue: (value) => {
- return value = parseInt(value), value === 0 ? t("off") : value + "%";
- }
- }, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", blankSettings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", blankSettings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", blankSettings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", blankSettings.rightStickDeadzone, params, boundUpdatePreset)));
- }
- startDetectingButton() {
- this.isDetectingButton = !0;
- let { $btnDetect } = this;
- $btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0;
- let count = 4;
- $btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => {
- if (count -= 1, count === 0) {
- this.stopDetectingButton(), $btnDetect.focus();
- return;
- }
- $btnDetect.textContent = `[${count}] ${t("press-any-button")}`;
- }, 1000);
- }
- stopDetectingButton() {
- let { $btnDetect } = this;
- $btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null;
- }
- async onBeforeMount() {
- this.stopDetectingButton(), super.onBeforeMount(...arguments);
- }
- onBeforeUnmount() {
- this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();
- }
- handleGamepad(button) {
- if (!this.isDetectingButton) return super.handleGamepad(button);
- if (button in ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
- this.stopDetectingButton();
- let $select = this.selectsMap[button], $label = $select.previousElementSibling;
- if ($label.addEventListener("animationend", () => {
- $label.classList.remove("bx-horizontal-shaking");
- }, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getGlobalPref("ui.controllerFriendly"))
- this.dialogManager.focus($select);
- }
- return !0;
- }
- switchPreset(id) {
- let preset = this.allPresets.data[id];
- if (!preset) {
- this.currentPresetId = 0;
- return;
- }
- let {
- $btnDetect,
- $vibrationIntensity,
- $leftStickDeadzone,
- $rightStickDeadzone,
- $leftTriggerRange,
- $rightTriggerRange,
- selectsMap
- } = this, presetData = preset.data;
- this.currentPresetId = id;
- let isDefaultPreset = id <= 0;
- this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset);
- let buttonIndex;
- for (buttonIndex in selectsMap) {
- buttonIndex = buttonIndex;
- let $select = selectsMap[buttonIndex];
- if (!$select) continue;
- let mappedButton = presetData.mapping[buttonIndex];
- $select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {
- ignoreOnChange: !0,
- manualTrigger: !0
- });
- }
- presetData.settings = Object.assign({}, this.presetsDb.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange);
- }
- updatePreset() {
- let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), gamepadKey;
- for (gamepadKey in this.selectsMap) {
- let value = this.selectsMap[gamepadKey].value;
- if (!value) continue;
- let mapTo = value === "false" ? !1 : parseInt(value);
- newData.mapping[gamepadKey] = mapTo;
- }
- Object.assign(newData.settings, {
- vibrationIntensity: parseInt(this.$vibrationIntensity.value),
- leftStickDeadzone: this.$leftStickDeadzone.getValue(),
- rightStickDeadzone: this.$rightStickDeadzone.getValue(),
- leftTriggerRange: this.$leftTriggerRange.getValue(),
- rightTriggerRange: this.$rightTriggerRange.getValue()
- });
- let preset = this.allPresets.data[this.currentPresetId];
- preset.data = newData, this.presetsDb.updatePreset(preset);
- }
- async renderSummary(presetId) {
- let preset = await this.presetsDb.getPreset(presetId);
- if (!preset) return null;
- let presetData = preset.data, $content, showNote = !1;
- if (Object.keys(presetData.mapping).length > 0) {
- $content = CE("div", { class: "bx-controller-customization-summary" });
- for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
- if (!(gamepadKey in presetData.mapping)) continue;
- let mappedKey = presetData.mapping[gamepadKey];
- $content.append(CE("span", { class: "bx-prompt" }, getGamepadPrompt(gamepadKey) + " > " + (mappedKey === !1 ? "🚫" : getGamepadPrompt(mappedKey))));
- }
- showNote = !0;
- }
- let key;
- for (key in presetData.settings) {
- if (key === "vibrationIntensity") continue;
- let value = presetData.settings[key];
- if (Array.isArray(value) && (value[0] !== 0 || value[1] !== 100)) {
- showNote = !0;
- break;
- }
- }
- let fragment = document.createDocumentFragment();
- if (showNote) {
- let $note = CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("controller-customization-input-latency-note"));
- fragment.appendChild($note);
- }
- if ($content) fragment.appendChild($content);
- return fragment.childElementCount ? fragment : null;
- }
+static instance;
+static getInstance = () => ControllerCustomizationsManagerDialog.instance ?? (ControllerCustomizationsManagerDialog.instance = new ControllerCustomizationsManagerDialog(t("controller-customization")));
+$vibrationIntensity;
+$leftTriggerRange;
+$rightTriggerRange;
+$leftStickDeadzone;
+$rightStickDeadzone;
+$btnDetect;
+selectsMap = {};
+selectsOrder = [];
+isDetectingButton = !1;
+detectIntervalId = null;
+static BUTTONS_ORDER = [
+0,
+1,
+2,
+3,
+12,
+15,
+13,
+14,
+4,
+5,
+6,
+7,
+10,
+11,
+104,
+204,
+8,
+9,
+17
+];
+constructor(title) {
+super(title, ControllerCustomizationsTable.getInstance());
+this.render();
+}
+render() {
+let isControllerFriendly = getGlobalPref("ui.controllerFriendly"), $rows = CE("div", { class: "bx-buttons-grid" }), $baseSelect = CE("select", { class: "bx-full-width" }, CE("option", { value: "" }, "---"), CE("option", { value: "false", _dataset: { label: "🚫" } }, isControllerFriendly ? "🚫" : t("off"))), $baseButtonSelect = $baseSelect.cloneNode(!0), $baseStickSelect = $baseSelect.cloneNode(!0), onButtonChanged = (e) => {
+if (!e.ignoreOnChange) this.updatePreset();
+}, boundUpdatePreset = this.updatePreset.bind(this);
+for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
+if (gamepadKey === 17) continue;
+let name = GamepadKeyName[gamepadKey][isControllerFriendly ? 1 : 0];
+(gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).appendChild(CE("option", {
+value: gamepadKey,
+_dataset: { label: GamepadKeyName[gamepadKey][1] }
+}, name));
+}
+for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
+let [buttonName, buttonPrompt] = GamepadKeyName[gamepadKey], $clonedSelect = (gamepadKey === 104 || gamepadKey === 204 ? $baseStickSelect : $baseButtonSelect).cloneNode(!0);
+$clonedSelect.querySelector(`option[value="${gamepadKey}"]`)?.remove();
+let $select = BxSelectElement.create($clonedSelect);
+$select.dataset.index = gamepadKey.toString(), $select.addEventListener("input", onButtonChanged), this.selectsMap[gamepadKey] = $select, this.selectsOrder.push(gamepadKey);
+let $row = CE("div", {
+class: "bx-controller-key-row",
+_nearby: { orientation: "horizontal" }
+}, CE("label", { title: buttonName }, buttonPrompt), $select);
+$rows.append($row);
+}
+if (getGlobalPref("ui.controllerFriendly")) for (let i = 0;i < this.selectsOrder.length; i++) {
+let $select = this.selectsMap[this.selectsOrder[i]], directions = {
+1: i - 2,
+3: i + 2,
+4: i - 1,
+2: i + 1
+};
+for (let dir in directions) {
+let idx = directions[dir];
+if (typeof this.selectsOrder[idx] === "undefined") continue;
+let $targetSelect = this.selectsMap[this.selectsOrder[idx]];
+setNearby($select, {
+[dir]: $targetSelect
+});
+}
+}
+let blankSettings = this.presetsDb.BLANK_PRESET_DATA.settings, params = {
+min: 0,
+minDiff: 1,
+max: 100,
+steps: 1
+};
+this.$content = CE("div", { class: "bx-controller-customizations-container" }, this.$btnDetect = createButton({
+label: t("detect-controller-button"),
+classes: ["bx-btn-detect"],
+style: 4096 | 64 | 128,
+onClick: () => {
+this.startDetectingButton();
+}
+}), $rows, createSettingRow(t("vibration-intensity"), this.$vibrationIntensity = BxNumberStepper.create("controller_vibration_intensity", 50, 0, 100, {
+steps: 10,
+suffix: "%",
+exactTicks: 20,
+customTextValue: (value) => {
+return value = parseInt(value), value === 0 ? t("off") : value + "%";
+}
+}, boundUpdatePreset)), createSettingRow(t("left-trigger-range"), this.$leftTriggerRange = BxDualNumberStepper.create("left-trigger-range", blankSettings.leftTriggerRange, params, boundUpdatePreset)), createSettingRow(t("right-trigger-range"), this.$rightTriggerRange = BxDualNumberStepper.create("right-trigger-range", blankSettings.rightTriggerRange, params, boundUpdatePreset)), createSettingRow(t("left-stick-deadzone"), this.$leftStickDeadzone = BxDualNumberStepper.create("left-stick-deadzone", blankSettings.leftStickDeadzone, params, boundUpdatePreset)), createSettingRow(t("right-stick-deadzone"), this.$rightStickDeadzone = BxDualNumberStepper.create("right-stick-deadzone", blankSettings.rightStickDeadzone, params, boundUpdatePreset)));
+}
+startDetectingButton() {
+this.isDetectingButton = !0;
+let { $btnDetect } = this;
+$btnDetect.classList.add("bx-monospaced", "bx-blink-me"), $btnDetect.disabled = !0;
+let count = 4;
+$btnDetect.textContent = `[${count}] ${t("press-any-button")}`, this.detectIntervalId = window.setInterval(() => {
+if (count -= 1, count === 0) {
+this.stopDetectingButton(), $btnDetect.focus();
+return;
+}
+$btnDetect.textContent = `[${count}] ${t("press-any-button")}`;
+}, 1000);
+}
+stopDetectingButton() {
+let { $btnDetect } = this;
+$btnDetect.classList.remove("bx-monospaced", "bx-blink-me"), $btnDetect.textContent = t("detect-controller-button"), $btnDetect.disabled = !1, this.isDetectingButton = !1, this.detectIntervalId && window.clearInterval(this.detectIntervalId), this.detectIntervalId = null;
+}
+async onBeforeMount() {
+this.stopDetectingButton(), super.onBeforeMount(...arguments);
+}
+onBeforeUnmount() {
+this.stopDetectingButton(), StreamSettings.refreshControllerSettings(), super.onBeforeUnmount();
+}
+handleGamepad(button) {
+if (!this.isDetectingButton) return super.handleGamepad(button);
+if (button in ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
+this.stopDetectingButton();
+let $select = this.selectsMap[button], $label = $select.previousElementSibling;
+if ($label.addEventListener("animationend", () => {
+$label.classList.remove("bx-horizontal-shaking");
+}, { once: !0 }), $label.classList.add("bx-horizontal-shaking"), getGlobalPref("ui.controllerFriendly"))
+this.dialogManager.focus($select);
+}
+return !0;
+}
+switchPreset(id) {
+let preset = this.allPresets.data[id];
+if (!preset) {
+this.currentPresetId = 0;
+return;
+}
+let {
+$btnDetect,
+$vibrationIntensity,
+$leftStickDeadzone,
+$rightStickDeadzone,
+$leftTriggerRange,
+$rightTriggerRange,
+selectsMap
+} = this, presetData = preset.data;
+this.currentPresetId = id;
+let isDefaultPreset = id <= 0;
+this.updateButtonStates(), $btnDetect.classList.toggle("bx-gone", isDefaultPreset);
+let buttonIndex;
+for (buttonIndex in selectsMap) {
+buttonIndex = buttonIndex;
+let $select = selectsMap[buttonIndex];
+if (!$select) continue;
+let mappedButton = presetData.mapping[buttonIndex];
+$select.value = typeof mappedButton === "undefined" ? "" : mappedButton.toString(), $select.disabled = isDefaultPreset, BxEvent.dispatch($select, "input", {
+ignoreOnChange: !0,
+manualTrigger: !0
+});
+}
+presetData.settings = Object.assign({}, this.presetsDb.BLANK_PRESET_DATA.settings, presetData.settings), $vibrationIntensity.value = presetData.settings.vibrationIntensity.toString(), $vibrationIntensity.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.dataset.disabled = $rightStickDeadzone.dataset.disabled = $leftTriggerRange.dataset.disabled = $rightTriggerRange.dataset.disabled = isDefaultPreset.toString(), $leftStickDeadzone.setValue(presetData.settings.leftStickDeadzone), $rightStickDeadzone.setValue(presetData.settings.rightStickDeadzone), $leftTriggerRange.setValue(presetData.settings.leftTriggerRange), $rightTriggerRange.setValue(presetData.settings.rightTriggerRange);
+}
+updatePreset() {
+let newData = deepClone(this.presetsDb.BLANK_PRESET_DATA), gamepadKey;
+for (gamepadKey in this.selectsMap) {
+let value = this.selectsMap[gamepadKey].value;
+if (!value) continue;
+let mapTo = value === "false" ? !1 : parseInt(value);
+newData.mapping[gamepadKey] = mapTo;
+}
+Object.assign(newData.settings, {
+vibrationIntensity: parseInt(this.$vibrationIntensity.value),
+leftStickDeadzone: this.$leftStickDeadzone.getValue(),
+rightStickDeadzone: this.$rightStickDeadzone.getValue(),
+leftTriggerRange: this.$leftTriggerRange.getValue(),
+rightTriggerRange: this.$rightTriggerRange.getValue()
+});
+let preset = this.allPresets.data[this.currentPresetId];
+preset.data = newData, this.presetsDb.updatePreset(preset);
+}
+async renderSummary(presetId) {
+let preset = await this.presetsDb.getPreset(presetId);
+if (!preset) return null;
+let presetData = preset.data, $content, showNote = !1;
+if (Object.keys(presetData.mapping).length > 0) {
+$content = CE("div", { class: "bx-controller-customization-summary" });
+for (let gamepadKey of ControllerCustomizationsManagerDialog.BUTTONS_ORDER) {
+if (!(gamepadKey in presetData.mapping)) continue;
+let mappedKey = presetData.mapping[gamepadKey];
+$content.append(CE("span", { class: "bx-prompt" }, getGamepadPrompt(gamepadKey) + " > " + (mappedKey === !1 ? "🚫" : getGamepadPrompt(mappedKey))));
+}
+showNote = !0;
+}
+let key;
+for (key in presetData.settings) {
+if (key === "vibrationIntensity") continue;
+let value = presetData.settings[key];
+if (Array.isArray(value) && (value[0] !== 0 || value[1] !== 100)) {
+showNote = !0;
+break;
+}
+}
+let fragment = document.createDocumentFragment();
+if (showNote) {
+let $note = CE("div", { class: "bx-settings-dialog-note" }, "ⓘ " + t("controller-customization-input-latency-note"));
+fragment.appendChild($note);
+}
+if ($content) fragment.appendChild($content);
+return fragment.childElementCount ? fragment : null;
+}
}
class ControllerExtraSettings extends HTMLElement {
- currentControllerId;
- controllerIds;
- $selectControllers;
- $selectShortcuts;
- $selectCustomization;
- $summaryCustomization;
- updateLayout;
- switchController;
- getCurrentControllerId;
- saveSettings;
- updateCustomizationSummary;
- setValue;
- static renderSettings() {
- let $container = CE("label", {
- class: "bx-settings-row bx-controller-extra-settings"
- });
- $container.prefKey = "controller.settings", $container.addEventListener("contextmenu", this.boundOnContextMenu), this.settingsManager.setElement("controller.settings", $container), $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container), $container.setValue = ControllerExtraSettings.setValue.bind($container);
- let $selectControllers = BxSelectElement.create(CE("select", {
- class: "bx-full-width",
- autocomplete: "off",
- _on: {
- input: (e) => {
- $container.switchController($selectControllers.value);
- }
- }
- })), $selectShortcuts = BxSelectElement.create(CE("select", {
- autocomplete: "off",
- _on: { input: $container.saveSettings }
- })), $selectCustomization = BxSelectElement.create(CE("select", {
- autocomplete: "off",
- _on: {
- input: async () => {
- ControllerExtraSettings.updateCustomizationSummary.call($container), $container.saveSettings();
- }
- }
- })), $rowCustomization = createSettingRow(t("in-game-controller-customization"), CE("div", {
- class: "bx-preset-row",
- _nearby: { orientation: "horizontal" }
- }, $selectCustomization, createButton({
- title: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64 | 1 | 512,
- onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({
- id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null
- })
- })), { multiLines: !0 });
- return $rowCustomization.appendChild($container.$summaryCustomization = CE("div")), $container.append(CE("span", !1, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", {
- class: "bx-preset-row",
- _nearby: { orientation: "horizontal" }
- }, $selectShortcuts, createButton({
- title: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64 | 1 | 512,
- onClick: () => ControllerShortcutsManagerDialog.getInstance().show({
- id: parseInt($container.$selectShortcuts.value)
- })
- })), { multiLines: !0 }), $rowCustomization))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => {
- $container.updateLayout();
- }), $container;
- }
- static async updateCustomizationSummary() {
- let presetId = parseInt(this.$selectCustomization.value), $summaryContent = await ControllerCustomizationsManagerDialog.getInstance().renderSummary(presetId);
- if (removeChildElements(this.$summaryCustomization), $summaryContent) this.$summaryCustomization.appendChild($summaryContent);
- }
- static async updateLayout() {
- if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return;
- let $fragment = document.createDocumentFragment();
- removeChildElements(this.$selectControllers);
- for (let name of this.controllerIds) {
- let $option = CE("option", { value: name }, simplifyGamepadName(name));
- $fragment.appendChild($option);
- }
- this.$selectControllers.appendChild($fragment);
- let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
- renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 });
- let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets();
- renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 });
- for (let name of this.controllerIds) {
- let $option = CE("option", { value: name }, name);
- $fragment.appendChild($option);
- }
- BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this);
- }
- static async switchController(id) {
- if (this.currentControllerId = id, !this.getCurrentControllerId()) return;
- let controllerSetting = STORAGE.Stream.getControllerSetting(this.currentControllerId);
- ControllerExtraSettings.updateElements.call(this, controllerSetting);
- }
- static getCurrentControllerId() {
- if (this.currentControllerId) {
- if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId;
- this.currentControllerId = "";
- }
- if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0];
- if (this.currentControllerId) return this.currentControllerId;
- return null;
- }
- static async saveSettings() {
- if (!this.getCurrentControllerId()) return;
- let controllerSettings = getStreamPref("controller.settings");
- controllerSettings[this.currentControllerId] = {
- shortcutPresetId: parseInt(this.$selectShortcuts.value),
- customizationPresetId: parseInt(this.$selectCustomization.value)
- }, setStreamPref("controller.settings", controllerSettings, "ui"), StreamSettings.refreshControllerSettings();
- }
- static setValue(value) {
- ControllerExtraSettings.updateElements.call(this, value[this.currentControllerId]);
- }
- static updateElements(controllerSetting) {
- if (!controllerSetting) return;
- this.$selectShortcuts.value = controllerSetting.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSetting.customizationPresetId.toString(), ControllerExtraSettings.updateCustomizationSummary.call(this);
- }
+currentControllerId;
+controllerIds;
+$selectControllers;
+$selectShortcuts;
+$selectCustomization;
+$summaryCustomization;
+updateLayout;
+switchController;
+getCurrentControllerId;
+saveSettings;
+updateCustomizationSummary;
+setValue;
+static renderSettings() {
+let $container = CE("label", {
+class: "bx-settings-row bx-controller-extra-settings"
+});
+$container.prefKey = "controller.settings", $container.addEventListener("contextmenu", this.boundOnContextMenu), this.settingsManager.setElement("controller.settings", $container), $container.updateLayout = ControllerExtraSettings.updateLayout.bind($container), $container.switchController = ControllerExtraSettings.switchController.bind($container), $container.getCurrentControllerId = ControllerExtraSettings.getCurrentControllerId.bind($container), $container.saveSettings = ControllerExtraSettings.saveSettings.bind($container), $container.setValue = ControllerExtraSettings.setValue.bind($container);
+let $selectControllers = BxSelectElement.create(CE("select", {
+class: "bx-full-width",
+autocomplete: "off",
+_on: {
+input: (e) => {
+$container.switchController($selectControllers.value);
+}
+}
+})), $selectShortcuts = BxSelectElement.create(CE("select", {
+autocomplete: "off",
+_on: { input: $container.saveSettings }
+})), $selectCustomization = BxSelectElement.create(CE("select", {
+autocomplete: "off",
+_on: {
+input: async () => {
+ControllerExtraSettings.updateCustomizationSummary.call($container), $container.saveSettings();
+}
+}
+})), $rowCustomization = createSettingRow(t("in-game-controller-customization"), CE("div", {
+class: "bx-preset-row",
+_nearby: { orientation: "horizontal" }
+}, $selectCustomization, createButton({
+title: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64 | 1 | 512,
+onClick: () => ControllerCustomizationsManagerDialog.getInstance().show({
+id: $container.$selectCustomization.value ? parseInt($container.$selectCustomization.value) : null
+})
+})), { multiLines: !0 });
+return $rowCustomization.appendChild($container.$summaryCustomization = CE("div")), $container.append(CE("span", !1, t("no-controllers-connected")), CE("div", { class: "bx-controller-extra-wrapper" }, $selectControllers, CE("div", { class: "bx-sub-content-box" }, createSettingRow(t("in-game-controller-shortcuts"), CE("div", {
+class: "bx-preset-row",
+_nearby: { orientation: "horizontal" }
+}, $selectShortcuts, createButton({
+title: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64 | 1 | 512,
+onClick: () => ControllerShortcutsManagerDialog.getInstance().show({
+id: parseInt($container.$selectShortcuts.value)
+})
+})), { multiLines: !0 }), $rowCustomization))), $container.$selectControllers = $selectControllers, $container.$selectShortcuts = $selectShortcuts, $container.$selectCustomization = $selectCustomization, $container.updateLayout(), window.addEventListener("gamepadconnected", $container.updateLayout), window.addEventListener("gamepaddisconnected", $container.updateLayout), this.onMountedCallbacks.push(() => {
+$container.updateLayout();
+}), $container;
+}
+static async updateCustomizationSummary() {
+let presetId = parseInt(this.$selectCustomization.value), $summaryContent = await ControllerCustomizationsManagerDialog.getInstance().renderSummary(presetId);
+if (removeChildElements(this.$summaryCustomization), $summaryContent) this.$summaryCustomization.appendChild($summaryContent);
+}
+static async updateLayout() {
+if (this.controllerIds = getUniqueGamepadNames(), this.dataset.hasGamepad = (this.controllerIds.length > 0).toString(), this.controllerIds.length === 0) return;
+let $fragment = document.createDocumentFragment();
+removeChildElements(this.$selectControllers);
+for (let name of this.controllerIds) {
+let $option = CE("option", { value: name }, simplifyGamepadName(name));
+$fragment.appendChild($option);
+}
+this.$selectControllers.appendChild($fragment);
+let allShortcutPresets = await ControllerShortcutsTable.getInstance().getPresets();
+renderPresetsList(this.$selectShortcuts, allShortcutPresets, null, { addOffValue: !0 });
+let allCustomizationPresets = await ControllerCustomizationsTable.getInstance().getPresets();
+renderPresetsList(this.$selectCustomization, allCustomizationPresets, null, { addOffValue: !0 });
+for (let name of this.controllerIds) {
+let $option = CE("option", { value: name }, name);
+$fragment.appendChild($option);
+}
+BxEvent.dispatch(this.$selectControllers, "input"), calculateSelectBoxes(this);
+}
+static async switchController(id) {
+if (this.currentControllerId = id, !this.getCurrentControllerId()) return;
+let controllerSetting = STORAGE.Stream.getControllerSetting(this.currentControllerId);
+ControllerExtraSettings.updateElements.call(this, controllerSetting);
+}
+static getCurrentControllerId() {
+if (this.currentControllerId) {
+if (this.controllerIds.includes(this.currentControllerId)) return this.currentControllerId;
+this.currentControllerId = "";
+}
+if (!this.currentControllerId) this.currentControllerId = this.controllerIds[0];
+if (this.currentControllerId) return this.currentControllerId;
+return null;
+}
+static async saveSettings() {
+if (!this.getCurrentControllerId()) return;
+let controllerSettings = getStreamPref("controller.settings");
+controllerSettings[this.currentControllerId] = {
+shortcutPresetId: parseInt(this.$selectShortcuts.value),
+customizationPresetId: parseInt(this.$selectCustomization.value)
+}, setStreamPref("controller.settings", controllerSettings, "ui"), StreamSettings.refreshControllerSettings();
+}
+static setValue(value) {
+ControllerExtraSettings.updateElements.call(this, value[this.currentControllerId]);
+}
+static updateElements(controllerSetting) {
+if (!controllerSetting) return;
+this.$selectShortcuts.value = controllerSetting.shortcutPresetId.toString(), this.$selectCustomization.value = controllerSetting.customizationPresetId.toString(), ControllerExtraSettings.updateCustomizationSummary.call(this);
+}
}
class SuggestionsSetting {
- static async renderSuggestions(e) {
- let $btnSuggest = e.target.closest("div");
- $btnSuggest.toggleAttribute("bx-open");
- let $content = $btnSuggest.nextElementSibling;
- if ($content) {
- BxEvent.dispatch($content.querySelector("select"), "input");
- return;
- }
- let settingTabGroup;
- for (settingTabGroup in this.SETTINGS_UI) {
- let settingTab = this.SETTINGS_UI[settingTabGroup];
- if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;
- for (let settingTabContent of settingTab.items) {
- if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue;
- for (let setting of settingTabContent.items) {
- let prefKey;
- if (typeof setting === "string") prefKey = setting;
- else if (typeof setting === "object") prefKey = setting.pref;
- if (prefKey) this.settingLabels[prefKey] = settingTabContent.label;
- }
- }
- }
- let recommendedDevice = "";
- if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
- if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
- }
- let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
- if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on");
- else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto");
- else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off");
- SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
- let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality")));
- $select.addEventListener("input", (e2) => {
- let profile = $select.value;
- removeChildElements($suggestedSettings);
- let fragment = document.createDocumentFragment(), note;
- if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice });
- else if (profile === "highest") note = "⚠️ " + t("highest-quality-note");
- note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note));
- let settings = this.suggestedSettings[profile];
- for (let key in settings) {
- let { storage, definition } = getPrefInfo(key), prefKey;
- if (storage === STORAGE.Stream) prefKey = key;
- else prefKey = key;
- let suggestedValue;
- if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
- else suggestedValue = settings[prefKey];
- let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value;
- if (isSameValue) $value = currentValueText;
- else {
- let suggestedValueText = storage.getValueText(prefKey, suggestedValue);
- $value = currentValueText + " ➔ " + suggestedValueText;
- }
- let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`);
- if ($child = CE("div", {
- class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}`
- }, $checkbox = CE("input", {
- type: "checkbox",
- tabindex: 0,
- checked: !0,
- id
- }), CE("label", {
- for: id
- }, CE("div", {
- class: "bx-suggest-label"
- }, breadcrumb), CE("div", {
- class: "bx-suggest-value"
- }, $value))), isSameValue)
- $checkbox.disabled = !0, $checkbox.checked = !0;
- fragment.appendChild($child);
- }
- $suggestedSettings.appendChild(fragment);
- }), BxEvent.dispatch($select, "input");
- let onClickApply = () => {
- let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance();
- for (prefKey in settings) {
- let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`);
- if (!$checkBox.checked || $checkBox.disabled) continue;
- let $control = settingsManager.getElement(prefKey);
- if (!$control) {
- setPref(prefKey, suggestedValue, "direct");
- continue;
- }
- let { definition: settingDefinition } = getPrefInfo(prefKey);
- if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
- if ("setValue" in $control) $control.setValue(suggestedValue);
- else $control.value = suggestedValue;
- BxEvent.dispatch($control, "input", {
- manualTrigger: !0
- });
- }
- BxEvent.dispatch($select, "input");
- }, $btnApply = createButton({
- label: t("apply"),
- style: 128 | 64,
- onClick: onClickApply
- });
- $content = CE("div", {
- class: "bx-sub-content-box bx-suggest-box",
- _nearby: {
- orientation: "vertical"
- }
- }, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", {
- class: "bx-suggest-link bx-focusable",
- href: "https://better-xcloud.github.io/guide/android-webview-tweaks/",
- target: "_blank",
- tabindex: 0
- }, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", {
- class: "bx-suggest-link bx-focusable",
- href: "https://github.com/redphx/better-xcloud-devices",
- target: "_blank",
- tabindex: 0
- }, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);
- }
- static async getRecommendedSettings(androidInfo) {
- function normalize(str) {
- return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
- }
- try {
- let { brand, board, model } = androidInfo;
- brand = normalize(brand), board = normalize(board), model = normalize(model);
- let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {};
- if (json.schema_version !== 2) return null;
- let scriptSettings = json.settings.script;
- if (scriptSettings._base) {
- let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base;
- for (let profile of base)
- Object.assign(recommended, this.suggestedSettings[profile]);
- delete scriptSettings._base;
- }
- let key;
- for (key in scriptSettings)
- recommended[key] = scriptSettings[key];
- return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name;
- } catch (e) {}
- return null;
- }
- static addDefaultSuggestedSetting(prefKey, value) {
- let key;
- for (key in this.suggestedSettings)
- if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value;
- }
- static generateDefaultSuggestedSettings() {
- let key;
- for (key in this.suggestedSettings) {
- if (key === "default") continue;
- let prefKey;
- for (prefKey in this.suggestedSettings[key])
- if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default;
- }
- }
+static async renderSuggestions(e) {
+let $btnSuggest = e.target.closest("div");
+$btnSuggest.toggleAttribute("bx-open");
+let $content = $btnSuggest.nextElementSibling;
+if ($content) {
+BxEvent.dispatch($content.querySelector("select"), "input");
+return;
+}
+let settingTabGroup;
+for (settingTabGroup in this.SETTINGS_UI) {
+let settingTab = this.SETTINGS_UI[settingTabGroup];
+if (!settingTab || !settingTab.items || typeof settingTab.items === "function") continue;
+for (let settingTabContent of settingTab.items) {
+if (!settingTabContent || settingTabContent instanceof HTMLElement || !settingTabContent.items) continue;
+for (let setting of settingTabContent.items) {
+let prefKey;
+if (typeof setting === "string") prefKey = setting;
+else if (typeof setting === "object") prefKey = setting.pref;
+if (prefKey) this.settingLabels[prefKey] = settingTabContent.label;
+}
+}
+}
+let recommendedDevice = "";
+if (BX_FLAGS.DeviceInfo.deviceType.includes("android")) {
+if (BX_FLAGS.DeviceInfo.androidInfo) recommendedDevice = await SuggestionsSetting.getRecommendedSettings.call(this, BX_FLAGS.DeviceInfo.androidInfo);
+}
+let hasRecommendedSettings = Object.keys(this.suggestedSettings.recommended).length > 0, deviceType = BX_FLAGS.DeviceInfo.deviceType;
+if (deviceType === "android-handheld") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off"), SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "on");
+else if (deviceType === "android") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "deviceVibration.mode", "auto");
+else if (deviceType === "android-tv") SuggestionsSetting.addDefaultSuggestedSetting.call(this, "touchController.mode", "off");
+SuggestionsSetting.generateDefaultSuggestedSettings.call(this);
+let $suggestedSettings = CE("div", { class: "bx-suggest-wrapper" }), $select = CE("select", !1, hasRecommendedSettings && CE("option", { value: "recommended" }, t("recommended")), !hasRecommendedSettings && CE("option", { value: "highest" }, t("highest-quality")), CE("option", { value: "default" }, t("default")), CE("option", { value: "lowest" }, t("lowest-quality")));
+$select.addEventListener("input", (e2) => {
+let profile = $select.value;
+removeChildElements($suggestedSettings);
+let fragment = document.createDocumentFragment(), note;
+if (profile === "recommended") note = t("recommended-settings-for-device", { device: recommendedDevice });
+else if (profile === "highest") note = "⚠️ " + t("highest-quality-note");
+note && fragment.appendChild(CE("div", { class: "bx-suggest-note" }, note));
+let settings = this.suggestedSettings[profile];
+for (let key in settings) {
+let { storage, definition } = getPrefInfo(key), prefKey;
+if (storage === STORAGE.Stream) prefKey = key;
+else prefKey = key;
+let suggestedValue;
+if (definition && definition.transformValue) suggestedValue = definition.transformValue.get.call(definition, settings[prefKey]);
+else suggestedValue = settings[prefKey];
+let currentValue = storage.getSetting(prefKey, !1), currentValueText = storage.getValueText(prefKey, currentValue), isSameValue = currentValue === suggestedValue, $child, $value;
+if (isSameValue) $value = currentValueText;
+else {
+let suggestedValueText = storage.getValueText(prefKey, suggestedValue);
+$value = currentValueText + " ➔ " + suggestedValueText;
+}
+let $checkbox, breadcrumb = this.settingLabels[prefKey] + " ❯ " + storage.getLabel(prefKey), id = escapeCssSelector(`bx_suggest_${prefKey}`);
+if ($child = CE("div", {
+class: `bx-suggest-row ${isSameValue ? "bx-suggest-ok" : "bx-suggest-change"}`
+}, $checkbox = CE("input", {
+type: "checkbox",
+tabindex: 0,
+checked: !0,
+id
+}), CE("label", {
+for: id
+}, CE("div", {
+class: "bx-suggest-label"
+}, breadcrumb), CE("div", {
+class: "bx-suggest-value"
+}, $value))), isSameValue)
+$checkbox.disabled = !0, $checkbox.checked = !0;
+fragment.appendChild($child);
+}
+$suggestedSettings.appendChild(fragment);
+}), BxEvent.dispatch($select, "input");
+let onClickApply = () => {
+let profile = $select.value, settings = this.suggestedSettings[profile], prefKey, settingsManager = SettingsManager.getInstance();
+for (prefKey in settings) {
+let suggestedValue = settings[prefKey], $checkBox = $content.querySelector(`#bx_suggest_${escapeCssSelector(prefKey)}`);
+if (!$checkBox.checked || $checkBox.disabled) continue;
+let $control = settingsManager.getElement(prefKey);
+if (!$control) {
+setPref(prefKey, suggestedValue, "direct");
+continue;
+}
+let { definition: settingDefinition } = getPrefInfo(prefKey);
+if (settingDefinition?.transformValue) suggestedValue = settingDefinition.transformValue.get.call(settingDefinition, suggestedValue);
+if ("setValue" in $control) $control.setValue(suggestedValue);
+else $control.value = suggestedValue;
+BxEvent.dispatch($control, "input", {
+manualTrigger: !0
+});
+}
+BxEvent.dispatch($select, "input");
+}, $btnApply = createButton({
+label: t("apply"),
+style: 128 | 64,
+onClick: onClickApply
+});
+$content = CE("div", {
+class: "bx-sub-content-box bx-suggest-box",
+_nearby: {
+orientation: "vertical"
+}
+}, BxSelectElement.create($select), $suggestedSettings, $btnApply, BX_FLAGS.DeviceInfo.deviceType.includes("android") && CE("a", {
+class: "bx-suggest-link bx-focusable",
+href: "https://better-xcloud.github.io/guide/android-webview-tweaks/",
+target: "_blank",
+tabindex: 0
+}, "🤓 " + t("how-to-improve-app-performance")), BX_FLAGS.DeviceInfo.deviceType.includes("android") && !hasRecommendedSettings && CE("a", {
+class: "bx-suggest-link bx-focusable",
+href: "https://github.com/redphx/better-xcloud-devices",
+target: "_blank",
+tabindex: 0
+}, t("suggest-settings-link"))), $btnSuggest.insertAdjacentElement("afterend", $content);
+}
+static async getRecommendedSettings(androidInfo) {
+function normalize(str) {
+return str.toLowerCase().trim().replaceAll(/\s+/g, "-").replaceAll(/-+/g, "-");
+}
+try {
+let { brand, board, model } = androidInfo;
+brand = normalize(brand), board = normalize(board), model = normalize(model);
+let url = GhPagesUtils.getUrl(`devices/${brand}/${board}-${model}.json`), json = await (await NATIVE_FETCH(url)).json(), recommended = {};
+if (json.schema_version !== 2) return null;
+let scriptSettings = json.settings.script;
+if (scriptSettings._base) {
+let base = typeof scriptSettings._base === "string" ? [scriptSettings._base] : scriptSettings._base;
+for (let profile of base)
+Object.assign(recommended, this.suggestedSettings[profile]);
+delete scriptSettings._base;
+}
+let key;
+for (key in scriptSettings)
+recommended[key] = scriptSettings[key];
+return BX_FLAGS.DeviceInfo.deviceType = json.device_type, this.suggestedSettings.recommended = recommended, json.device_name;
+} catch (e) {}
+return null;
+}
+static addDefaultSuggestedSetting(prefKey, value) {
+let key;
+for (key in this.suggestedSettings)
+if (key !== "default" && !(prefKey in this.suggestedSettings)) this.suggestedSettings[key][prefKey] = value;
+}
+static generateDefaultSuggestedSettings() {
+let key;
+for (key in this.suggestedSettings) {
+if (key === "default") continue;
+let prefKey;
+for (prefKey in this.suggestedSettings[key])
+if (!(prefKey in this.suggestedSettings.default)) this.suggestedSettings.default[prefKey] = getPrefInfo(prefKey).definition.default;
+}
+}
}
class BxKeyBindingButton extends HTMLButtonElement {
- title;
- isPrompt = !1;
- allowedFlags;
- keyInfo = null;
- bindKey;
- unbindKey;
- static create(options) {
- let $btn = CE("button", {
- class: "bx-binding-button bx-focusable",
- type: "button"
- });
- return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn;
- }
- static onClick(e) {
- KeyBindingDialog.getInstance().show({
- $elm: this
- });
- }
- static onContextMenu = (e) => {
- e.preventDefault();
- let $btn = e.target;
- if (!$btn.disabled) $btn.unbindKey.apply($btn);
- };
- static bindKey(key, force = !1) {
- if (!key) return;
- if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) {
- if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change");
- }
- }
- static unbindKey(force = !1) {
- this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change");
- }
- constructor() {
- super();
- }
+title;
+isPrompt = !1;
+allowedFlags;
+keyInfo = null;
+bindKey;
+unbindKey;
+static create(options) {
+let $btn = CE("button", {
+class: "bx-binding-button bx-focusable",
+type: "button"
+});
+return $btn.title = options.title, $btn.isPrompt = !!options.isPrompt, $btn.allowedFlags = options.allowedFlags, $btn.bindKey = BxKeyBindingButton.bindKey.bind($btn), $btn.unbindKey = BxKeyBindingButton.unbindKey.bind($btn), $btn.addEventListener("click", BxKeyBindingButton.onClick.bind($btn)), $btn.addEventListener("contextmenu", BxKeyBindingButton.onContextMenu), $btn.addEventListener("change", options.onChanged), $btn;
+}
+static onClick(e) {
+KeyBindingDialog.getInstance().show({
+$elm: this
+});
+}
+static onContextMenu = (e) => {
+e.preventDefault();
+let $btn = e.target;
+if (!$btn.disabled) $btn.unbindKey.apply($btn);
+};
+static bindKey(key, force = !1) {
+if (!key) return;
+if (force || this.keyInfo === null || key.code !== this.keyInfo?.code || key.modifiers !== this.keyInfo?.modifiers) {
+if (this.textContent = KeyHelper.codeToKeyName(key), this.keyInfo = key, !force) BxEvent.dispatch(this, "change");
+}
+}
+static unbindKey(force = !1) {
+this.textContent = "", this.keyInfo = null, !force && BxEvent.dispatch(this, "change");
+}
+constructor() {
+super();
+}
}
class KeyBindingDialog {
- static instance;
- static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog);
- $dialog;
- $wait;
- $title;
- $inputList;
- $overlay;
- $currentElm;
- countdownIntervalId;
- constructor() {
- this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", !1, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", !1, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", !1, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog);
- }
- show(options) {
- this.$currentElm = options.$elm, this.addEventListeners();
- let allowedFlags = this.$currentElm.allowedFlags;
- this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown();
- }
- startCountdown() {
- this.stopCountdown();
- let count = 9;
- this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => {
- if (count -= 1, count === 0) {
- this.stopCountdown(), this.hide();
- return;
- }
- this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`;
- }, 1000);
- }
- stopCountdown() {
- this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null;
- }
- hide = () => {
- this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone");
- };
- addEventListeners() {
- let allowedFlags = this.$currentElm.allowedFlags;
- if (allowedFlags.includes(1)) window.addEventListener("keyup", this);
- if (allowedFlags.includes(4)) window.addEventListener("mousedown", this);
- if (allowedFlags.includes(8)) window.addEventListener("wheel", this);
- }
- clearEventListeners() {
- window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this);
- }
- handleEvent(e) {
- let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1;
- switch (e.type) {
- case "wheel":
- if (handled = !0, allowedFlags.includes(8)) valid = !0;
- break;
- case "mousedown":
- if (handled = !0, allowedFlags.includes(4)) valid = !0;
- break;
- case "keyup":
- if (handled = !0, allowedFlags.includes(1)) {
- let keyboardEvent = e;
- if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) {
- let key = keyboardEvent.key;
- valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid;
- }
- }
- break;
- }
- if (handled) {
- if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown();
- else this.startCountdown();
- window.setTimeout(this.hide, 200);
- }
- }
+static instance;
+static getInstance = () => KeyBindingDialog.instance ?? (KeyBindingDialog.instance = new KeyBindingDialog);
+$dialog;
+$wait;
+$title;
+$inputList;
+$overlay;
+$currentElm;
+countdownIntervalId;
+constructor() {
+this.$overlay = CE("div", { class: "bx-key-binding-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay), this.$dialog = CE("div", { class: "bx-key-binding-dialog bx-gone" }, this.$title = CE("h2", {}), CE("div", { class: "bx-key-binding-dialog-content" }, CE("div", !1, this.$wait = CE("p", { class: "bx-blink-me" }), this.$inputList = CE("ul", !1, CE("li", { _dataset: { flag: 1 } }, t("keyboard-key")), CE("li", { _dataset: { flag: 2 } }, t("modifiers-note")), CE("li", { _dataset: { flag: 4 } }, t("mouse-click")), CE("li", { _dataset: { flag: 8 } }, t("mouse-wheel"))), CE("i", !1, t("press-esc-to-cancel"))))), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog);
+}
+show(options) {
+this.$currentElm = options.$elm, this.addEventListeners();
+let allowedFlags = this.$currentElm.allowedFlags;
+this.$inputList.dataset.flags = "[" + allowedFlags.join("][") + "]", document.activeElement && document.activeElement.blur(), this.$title.textContent = this.$currentElm.title, this.$title.classList.toggle("bx-prompt", this.$currentElm.isPrompt), this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), this.startCountdown();
+}
+startCountdown() {
+this.stopCountdown();
+let count = 9;
+this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`, this.countdownIntervalId = window.setInterval(() => {
+if (count -= 1, count === 0) {
+this.stopCountdown(), this.hide();
+return;
+}
+this.$wait.textContent = `[${count}] ${t("waiting-for-input")}`;
+}, 1000);
+}
+stopCountdown() {
+this.countdownIntervalId && clearInterval(this.countdownIntervalId), this.countdownIntervalId = null;
+}
+hide = () => {
+this.clearEventListeners(), this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone");
+};
+addEventListeners() {
+let allowedFlags = this.$currentElm.allowedFlags;
+if (allowedFlags.includes(1)) window.addEventListener("keyup", this);
+if (allowedFlags.includes(4)) window.addEventListener("mousedown", this);
+if (allowedFlags.includes(8)) window.addEventListener("wheel", this);
+}
+clearEventListeners() {
+window.removeEventListener("keyup", this), window.removeEventListener("mousedown", this), window.removeEventListener("wheel", this);
+}
+handleEvent(e) {
+let allowedFlags = this.$currentElm.allowedFlags, handled = !1, valid = !1;
+switch (e.type) {
+case "wheel":
+if (handled = !0, allowedFlags.includes(8)) valid = !0;
+break;
+case "mousedown":
+if (handled = !0, allowedFlags.includes(4)) valid = !0;
+break;
+case "keyup":
+if (handled = !0, allowedFlags.includes(1)) {
+let keyboardEvent = e;
+if (valid = keyboardEvent.code !== "Escape", valid && allowedFlags.includes(2)) {
+let key = keyboardEvent.key;
+valid = key !== "Control" && key !== "Shift" && key !== "Alt", handled = valid;
+}
+}
+break;
+}
+if (handled) {
+if (e.preventDefault(), e.stopPropagation(), valid) this.$currentElm.bindKey(KeyHelper.getKeyFromEvent(e)), this.stopCountdown();
+else this.startCountdown();
+window.setTimeout(this.hide, 200);
+}
+}
}
class MkbMappingManagerDialog extends BaseProfileManagerDialog {
- static instance;
- static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller")));
- KEYS_PER_BUTTON = 2;
- BUTTONS_ORDER = [
- 16,
- 12,
- 13,
- 14,
- 15,
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8,
- 9,
- 10,
- 100,
- 101,
- 102,
- 103,
- 11,
- 200,
- 201,
- 202,
- 203
- ];
- allKeyElements = [];
- $mouseMapTo;
- $mouseSensitivityX;
- $mouseSensitivityY;
- $mouseDeadzone;
- $unbindNote;
- constructor(title) {
- super(title, MkbMappingPresetsTable.getInstance());
- this.render();
- }
- onBindingKey = (e) => {
- if (e.target.disabled) return;
- if (e.button !== 0) return;
- };
- parseDataset($btn) {
- let dataset = $btn.dataset;
- return {
- keySlot: parseInt(dataset.keySlot),
- buttonIndex: parseInt(dataset.buttonIndex)
- };
- }
- onKeyChanged = (e) => {
- let $current = e.target, keyInfo = $current.keyInfo;
- if (keyInfo) {
- for (let $elm of this.allKeyElements)
- if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0);
- }
- this.savePreset();
- };
- render() {
- let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")));
- for (let buttonIndex of this.BUTTONS_ORDER) {
- let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment();
- for (let i = 0;i < this.KEYS_PER_BUTTON; i++)
- $elm = BxKeyBindingButton.create({
- title: buttonPrompt,
- isPrompt: !0,
- allowedFlags: [1, 4, 8],
- onChanged: this.onKeyChanged
- }), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm);
- let $keyRow = CE("div", {
- class: "bx-mkb-key-row",
- _nearby: { orientation: "horizontal" }
- }, CE("label", { title: buttonName }, buttonPrompt), $fragment);
- $rows.appendChild($keyRow);
- }
- let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, {
- suffix: "%",
- exactTicks: 50
- }, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, {
- suffix: "%",
- exactTicks: 50
- }, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, {
- suffix: "%",
- exactTicks: 10
- }, savePreset)));
- this.$content = CE("div", !1, $rows, $extraSettings);
- }
- switchPreset(id) {
- let preset = this.allPresets.data[id];
- if (!preset) {
- this.currentPresetId = 0;
- return;
- }
- let presetData = preset.data;
- this.currentPresetId = id;
- let isDefaultPreset = id <= 0;
- this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);
- for (let $elm of this.allKeyElements) {
- let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex];
- if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({
- code: buttonKeys[keySlot]
- }, !0);
- else $elm.unbindKey(!0);
- $elm.disabled = isDefaultPreset;
- }
- let mouse = presetData.mouse;
- this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();
- }
- savePreset() {
- let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);
- for (let $elm of this.allKeyElements) {
- let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping;
- if (!mapping[buttonIndex]) mapping[buttonIndex] = [];
- if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot];
- else mapping[buttonIndex][keySlot] = $elm.keyInfo.code;
- }
- let mouse = presetData.mouse;
- mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
- let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {
- id: this.currentPresetId,
- name: oldPreset.name,
- data: presetData
- };
- this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;
- }
- onBeforeUnmount() {
- StreamSettings.refreshMkbSettings(), super.onBeforeUnmount();
- }
+static instance;
+static getInstance = () => MkbMappingManagerDialog.instance ?? (MkbMappingManagerDialog.instance = new MkbMappingManagerDialog(t("virtual-controller")));
+KEYS_PER_BUTTON = 2;
+BUTTONS_ORDER = [
+16,
+12,
+13,
+14,
+15,
+0,
+1,
+2,
+3,
+4,
+5,
+6,
+7,
+8,
+9,
+10,
+100,
+101,
+102,
+103,
+11,
+200,
+201,
+202,
+203
+];
+allKeyElements = [];
+$mouseMapTo;
+$mouseSensitivityX;
+$mouseSensitivityY;
+$mouseDeadzone;
+$unbindNote;
+constructor(title) {
+super(title, MkbMappingPresetsTable.getInstance());
+this.render();
+}
+onBindingKey = (e) => {
+if (e.target.disabled) return;
+if (e.button !== 0) return;
+};
+parseDataset($btn) {
+let dataset = $btn.dataset;
+return {
+keySlot: parseInt(dataset.keySlot),
+buttonIndex: parseInt(dataset.buttonIndex)
+};
+}
+onKeyChanged = (e) => {
+let $current = e.target, keyInfo = $current.keyInfo;
+if (keyInfo) {
+for (let $elm of this.allKeyElements)
+if ($elm !== $current && $elm.keyInfo?.code === keyInfo.code) $elm.unbindKey(!0);
+}
+this.savePreset();
+};
+render() {
+let $rows = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")));
+for (let buttonIndex of this.BUTTONS_ORDER) {
+let [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex], $elm, $fragment = document.createDocumentFragment();
+for (let i = 0;i < this.KEYS_PER_BUTTON; i++)
+$elm = BxKeyBindingButton.create({
+title: buttonPrompt,
+isPrompt: !0,
+allowedFlags: [1, 4, 8],
+onChanged: this.onKeyChanged
+}), $elm.dataset.buttonIndex = buttonIndex.toString(), $elm.dataset.keySlot = i.toString(), $elm.addEventListener("mouseup", this.onBindingKey), $fragment.appendChild($elm), this.allKeyElements.push($elm);
+let $keyRow = CE("div", {
+class: "bx-mkb-key-row",
+_nearby: { orientation: "horizontal" }
+}, CE("label", { title: buttonName }, buttonPrompt), $fragment);
+$rows.appendChild($keyRow);
+}
+let savePreset = () => this.savePreset(), $extraSettings = CE("div", !1, createSettingRow(t("map-mouse-to"), this.$mouseMapTo = BxSelectElement.create(CE("select", { _on: { input: savePreset } }, CE("option", { value: 2 }, t("right-stick")), CE("option", { value: 1 }, t("left-stick")), CE("option", { value: 0 }, t("off"))))), createSettingRow(t("horizontal-sensitivity"), this.$mouseSensitivityX = BxNumberStepper.create("hor_sensitivity", 0, 1, 300, {
+suffix: "%",
+exactTicks: 50
+}, savePreset)), createSettingRow(t("vertical-sensitivity"), this.$mouseSensitivityY = BxNumberStepper.create("ver_sensitivity", 0, 1, 300, {
+suffix: "%",
+exactTicks: 50
+}, savePreset)), createSettingRow(t("deadzone-counterweight"), this.$mouseDeadzone = BxNumberStepper.create("deadzone_counterweight", 0, 1, 50, {
+suffix: "%",
+exactTicks: 10
+}, savePreset)));
+this.$content = CE("div", !1, $rows, $extraSettings);
+}
+switchPreset(id) {
+let preset = this.allPresets.data[id];
+if (!preset) {
+this.currentPresetId = 0;
+return;
+}
+let presetData = preset.data;
+this.currentPresetId = id;
+let isDefaultPreset = id <= 0;
+this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);
+for (let $elm of this.allKeyElements) {
+let { buttonIndex, keySlot } = this.parseDataset($elm), buttonKeys = presetData.mapping[buttonIndex];
+if (buttonKeys && buttonKeys[keySlot]) $elm.bindKey({
+code: buttonKeys[keySlot]
+}, !0);
+else $elm.unbindKey(!0);
+$elm.disabled = isDefaultPreset;
+}
+let mouse = presetData.mouse;
+this.$mouseMapTo.value = mouse.mapTo.toString(), this.$mouseSensitivityX.value = mouse.sensitivityX.toString(), this.$mouseSensitivityY.value = mouse.sensitivityY.toString(), this.$mouseDeadzone.value = mouse.deadzoneCounterweight.toString(), this.$mouseMapTo.disabled = isDefaultPreset, this.$mouseSensitivityX.dataset.disabled = isDefaultPreset.toString(), this.$mouseSensitivityY.dataset.disabled = isDefaultPreset.toString(), this.$mouseDeadzone.dataset.disabled = isDefaultPreset.toString();
+}
+savePreset() {
+let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);
+for (let $elm of this.allKeyElements) {
+let { buttonIndex, keySlot } = this.parseDataset($elm), mapping = presetData.mapping;
+if (!mapping[buttonIndex]) mapping[buttonIndex] = [];
+if (!$elm.keyInfo) delete mapping[buttonIndex][keySlot];
+else mapping[buttonIndex][keySlot] = $elm.keyInfo.code;
+}
+let mouse = presetData.mouse;
+mouse.mapTo = parseInt(this.$mouseMapTo.value), mouse.sensitivityX = parseInt(this.$mouseSensitivityX.value), mouse.sensitivityY = parseInt(this.$mouseSensitivityY.value), mouse.deadzoneCounterweight = parseInt(this.$mouseDeadzone.value);
+let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {
+id: this.currentPresetId,
+name: oldPreset.name,
+data: presetData
+};
+this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;
+}
+onBeforeUnmount() {
+StreamSettings.refreshMkbSettings(), super.onBeforeUnmount();
+}
}
class KeyboardShortcutsManagerDialog extends BaseProfileManagerDialog {
- static instance;
- static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts")));
- $content;
- $unbindNote;
- allKeyElements = [];
- constructor(title) {
- super(title, KeyboardShortcutsTable.getInstance());
- let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" });
- for (let groupLabel in SHORTCUT_ACTIONS) {
- let items = SHORTCUT_ACTIONS[groupLabel];
- if (!items) continue;
- let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel));
- for (let action in items) {
- let crumbs = items[action];
- if (!crumbs) continue;
- let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({
- title: label,
- isPrompt: !1,
- onChanged: this.onKeyChanged,
- allowedFlags: [1, 2]
- });
- $btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn);
- let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn));
- $fieldSet.appendChild($row);
- }
- if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet);
- }
- this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows);
- }
- onKeyChanged = (e) => {
- let $current = e.target, keyInfo = $current.keyInfo;
- if (keyInfo) for (let $elm of this.allKeyElements) {
- if ($elm === $current) continue;
- if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0);
- }
- this.savePreset();
- };
- parseDataset($btn) {
- return {
- action: $btn.dataset.action
- };
- }
- switchPreset(id) {
- let preset = this.allPresets.data[id];
- if (!preset) {
- this.currentPresetId = 0;
- return;
- }
- let presetData = preset.data;
- this.currentPresetId = id;
- let isDefaultPreset = id <= 0;
- this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);
- for (let $elm of this.allKeyElements) {
- let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action];
- if (keyInfo) $elm.bindKey(keyInfo, !0);
- else $elm.unbindKey(!0);
- $elm.disabled = isDefaultPreset;
- }
- }
- savePreset() {
- let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);
- for (let $elm of this.allKeyElements) {
- let { action } = this.parseDataset($elm), mapping = presetData.mapping;
- if ($elm.keyInfo) mapping[action] = $elm.keyInfo;
- }
- let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {
- id: this.currentPresetId,
- name: oldPreset.name,
- data: presetData
- };
- this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;
- }
- onBeforeUnmount() {
- StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount();
- }
+static instance;
+static getInstance = () => KeyboardShortcutsManagerDialog.instance ?? (KeyboardShortcutsManagerDialog.instance = new KeyboardShortcutsManagerDialog(t("keyboard-shortcuts")));
+$content;
+$unbindNote;
+allKeyElements = [];
+constructor(title) {
+super(title, KeyboardShortcutsTable.getInstance());
+let $rows = CE("div", { class: "bx-keyboard-shortcuts-manager-container" });
+for (let groupLabel in SHORTCUT_ACTIONS) {
+let items = SHORTCUT_ACTIONS[groupLabel];
+if (!items) continue;
+let $fieldSet = CE("fieldset", !1, CE("legend", !1, groupLabel));
+for (let action in items) {
+let crumbs = items[action];
+if (!crumbs) continue;
+let label = crumbs.join(" ❯ "), $btn = BxKeyBindingButton.create({
+title: label,
+isPrompt: !1,
+onChanged: this.onKeyChanged,
+allowedFlags: [1, 2]
+});
+$btn.classList.add("bx-full-width"), $btn.dataset.action = action, this.allKeyElements.push($btn);
+let $row = createSettingRow(label, CE("div", { class: "bx-binding-button-wrapper" }, $btn));
+$fieldSet.appendChild($row);
+}
+if ($fieldSet.childElementCount > 1) $rows.appendChild($fieldSet);
+}
+this.$content = CE("div", !1, this.$unbindNote = CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind")), $rows);
+}
+onKeyChanged = (e) => {
+let $current = e.target, keyInfo = $current.keyInfo;
+if (keyInfo) for (let $elm of this.allKeyElements) {
+if ($elm === $current) continue;
+if ($elm.keyInfo?.code === keyInfo.code && $elm.keyInfo?.modifiers === keyInfo.modifiers) $elm.unbindKey(!0);
+}
+this.savePreset();
+};
+parseDataset($btn) {
+return {
+action: $btn.dataset.action
+};
+}
+switchPreset(id) {
+let preset = this.allPresets.data[id];
+if (!preset) {
+this.currentPresetId = 0;
+return;
+}
+let presetData = preset.data;
+this.currentPresetId = id;
+let isDefaultPreset = id <= 0;
+this.updateButtonStates(), this.$unbindNote.classList.toggle("bx-gone", isDefaultPreset);
+for (let $elm of this.allKeyElements) {
+let { action } = this.parseDataset($elm), keyInfo = presetData.mapping[action];
+if (keyInfo) $elm.bindKey(keyInfo, !0);
+else $elm.unbindKey(!0);
+$elm.disabled = isDefaultPreset;
+}
+}
+savePreset() {
+let presetData = deepClone(this.presetsDb.BLANK_PRESET_DATA);
+for (let $elm of this.allKeyElements) {
+let { action } = this.parseDataset($elm), mapping = presetData.mapping;
+if ($elm.keyInfo) mapping[action] = $elm.keyInfo;
+}
+let oldPreset = this.allPresets.data[this.currentPresetId], newPreset = {
+id: this.currentPresetId,
+name: oldPreset.name,
+data: presetData
+};
+this.presetsDb.updatePreset(newPreset), this.allPresets.data[this.currentPresetId] = newPreset;
+}
+onBeforeUnmount() {
+StreamSettings.refreshKeyboardShortcuts(), super.onBeforeUnmount();
+}
}
class MkbExtraSettings extends HTMLElement {
- $mappingPresets;
- $shortcutsPresets;
- updateLayout;
- saveMkbSettings;
- saveShortcutsSettings;
- static renderSettings() {
- let $container = document.createDocumentFragment();
- $container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);
- let $mappingPresets = BxSelectElement.create(CE("select", {
- autocomplete: "off",
- _on: {
- input: $container.saveMkbSettings
- }
- })), $shortcutsPresets = BxSelectElement.create(CE("select", {
- autocomplete: "off",
- _on: {
- input: $container.saveShortcutsSettings
- }
- }));
- return $container.append(...getGlobalPref("mkb.enabled") ? [
- createSettingRow(t("virtual-controller"), CE("div", {
- class: "bx-preset-row",
- _nearby: {
- orientation: "horizontal"
- }
- }, $mappingPresets, createButton({
- title: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64 | 1 | 512,
- onClick: () => MkbMappingManagerDialog.getInstance().show({
- id: parseInt($container.$mappingPresets.value)
- })
- })), {
- multiLines: !0,
- onContextMenu: this.boundOnContextMenu,
- pref: "mkb.p1.preset.mappingId"
- }),
- createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), {
- onContextMenu: this.boundOnContextMenu,
- pref: "mkb.p1.slot"
- })
- ] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", {
- class: "bx-preset-row",
- _nearby: {
- orientation: "horizontal"
- }
- }, $shortcutsPresets, createButton({
- title: t("manage"),
- icon: BxIcon.MANAGE,
- style: 64 | 1 | 512,
- onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({
- id: parseInt($container.$shortcutsPresets.value)
- })
- })), {
- multiLines: !0,
- onContextMenu: this.boundOnContextMenu,
- pref: "keyboardShortcuts.preset.inGameId"
- })), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => {
- $container.updateLayout();
- }), $container;
- }
- static async updateLayout() {
- let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();
- renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId"));
- let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
- renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 });
- }
- static async saveMkbSettings() {
- let presetId = parseInt(this.$mappingPresets.value);
- setStreamPref("mkb.p1.preset.mappingId", presetId, "ui");
- }
- static async saveShortcutsSettings() {
- let presetId = parseInt(this.$shortcutsPresets.value);
- setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui");
- }
+$mappingPresets;
+$shortcutsPresets;
+updateLayout;
+saveMkbSettings;
+saveShortcutsSettings;
+static renderSettings() {
+let $container = document.createDocumentFragment();
+$container.updateLayout = MkbExtraSettings.updateLayout.bind($container), $container.saveMkbSettings = MkbExtraSettings.saveMkbSettings.bind($container), $container.saveShortcutsSettings = MkbExtraSettings.saveShortcutsSettings.bind($container);
+let $mappingPresets = BxSelectElement.create(CE("select", {
+autocomplete: "off",
+_on: {
+input: $container.saveMkbSettings
+}
+})), $shortcutsPresets = BxSelectElement.create(CE("select", {
+autocomplete: "off",
+_on: {
+input: $container.saveShortcutsSettings
+}
+}));
+return $container.append(...getGlobalPref("mkb.enabled") ? [
+createSettingRow(t("virtual-controller"), CE("div", {
+class: "bx-preset-row",
+_nearby: {
+orientation: "horizontal"
+}
+}, $mappingPresets, createButton({
+title: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64 | 1 | 512,
+onClick: () => MkbMappingManagerDialog.getInstance().show({
+id: parseInt($container.$mappingPresets.value)
+})
+})), {
+multiLines: !0,
+onContextMenu: this.boundOnContextMenu,
+pref: "mkb.p1.preset.mappingId"
+}),
+createSettingRow(t("virtual-controller-slot"), this.settingsManager.getElement("mkb.p1.slot"), {
+onContextMenu: this.boundOnContextMenu,
+pref: "mkb.p1.slot"
+})
+] : [], createSettingRow(t("in-game-keyboard-shortcuts"), CE("div", {
+class: "bx-preset-row",
+_nearby: {
+orientation: "horizontal"
+}
+}, $shortcutsPresets, createButton({
+title: t("manage"),
+icon: BxIcon.MANAGE,
+style: 64 | 1 | 512,
+onClick: () => KeyboardShortcutsManagerDialog.getInstance().show({
+id: parseInt($container.$shortcutsPresets.value)
+})
+})), {
+multiLines: !0,
+onContextMenu: this.boundOnContextMenu,
+pref: "keyboardShortcuts.preset.inGameId"
+})), $container.$mappingPresets = $mappingPresets, $container.$shortcutsPresets = $shortcutsPresets, this.settingsManager.setElement("keyboardShortcuts.preset.inGameId", $shortcutsPresets), this.settingsManager.setElement("mkb.p1.preset.mappingId", $mappingPresets), $container.updateLayout(), this.onMountedCallbacks.push(() => {
+$container.updateLayout();
+}), $container;
+}
+static async updateLayout() {
+let mappingPresets = await MkbMappingPresetsTable.getInstance().getPresets();
+renderPresetsList(this.$mappingPresets, mappingPresets, getStreamPref("mkb.p1.preset.mappingId"));
+let shortcutsPresets = await KeyboardShortcutsTable.getInstance().getPresets();
+renderPresetsList(this.$shortcutsPresets, shortcutsPresets, getStreamPref("keyboardShortcuts.preset.inGameId"), { addOffValue: !0 });
+}
+static async saveMkbSettings() {
+let presetId = parseInt(this.$mappingPresets.value);
+setStreamPref("mkb.p1.preset.mappingId", presetId, "ui");
+}
+static async saveShortcutsSettings() {
+let presetId = parseInt(this.$shortcutsPresets.value);
+setStreamPref("keyboardShortcuts.preset.inGameId", presetId, "ui");
+}
}
class SettingsDialog extends NavigationDialog {
- static instance;
- static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);
- LOG_TAG = "SettingsNavigationDialog";
- $container;
- $tabs;
- $tabContents;
- $btnReload;
- $btnGlobalReload;
- $noteGlobalReload;
- $btnSuggestion;
- $streamSettingsSelection;
- renderFullSettings;
- boundOnContextMenu;
- suggestedSettings = {
- recommended: {},
- default: {},
- lowest: {},
- highest: {}
- };
- settingLabels = {};
- settingsManager;
- TAB_GLOBAL_ITEMS = [{
- group: "general",
- label: t("better-xcloud"),
- helpUrl: "https://better-xcloud.github.io/features/",
- items: [
- ($parent) => {
- let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];
- if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
- let opts = {
- label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),
- style: 1 | 64 | 128
- };
- if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();
- else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";
- topButtons.push(createButton(opts));
- }
- if (AppInterface) topButtons.push(createButton({
- label: t("app-settings"),
- icon: BxIcon.STREAM_SETTINGS,
- style: 128 | 64,
- onClick: (e) => {
- AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();
- }
- }));
- else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({
- label: "🔥 " + t("install-android"),
- style: 128 | 64,
- url: "https://better-xcloud.github.io/android"
- }));
- this.$btnGlobalReload = createButton({
- label: t("settings-reload"),
- classes: ["bx-settings-reload-button", "bx-gone"],
- style: 64 | 128,
- onClick: (e) => {
- this.reloadPage();
- }
- }), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {
- class: "bx-settings-reload-note"
- }, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {
- class: "bx-suggest-toggler bx-focusable",
- tabindex: 0
- }, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);
- let $div = CE("div", {
- class: "bx-top-buttons",
- _nearby: {
- orientation: "vertical"
- }
- }, ...topButtons);
- $parent.appendChild($div);
- },
- {
- pref: "bx.locale",
- multiLines: !0
- },
- "server.bypassRestriction",
- "ui.controllerFriendly",
- "xhome.enabled"
- ]
- }, {
- group: "server",
- label: t("server"),
- items: [
- {
- pref: "server.region",
- multiLines: !0
- },
- {
- pref: "stream.locale",
- multiLines: !0
- },
- "server.ipv6.prefer"
- ]
- }, {
- group: "stream",
- label: t("stream"),
- items: [
- "stream.video.resolution",
- "stream.video.codecProfile",
- "stream.video.maxBitrate",
- "audio.volume.booster.enabled",
- "screenshot.applyFilters",
- "audio.mic.onPlaying",
- "game.fortnite.forceConsole",
- "stream.video.combineAudio"
- ]
- }, {
- requiredVariants: "full",
- group: "mkb",
- label: t("mouse-and-keyboard"),
- items: [
- "nativeMkb.mode",
- {
- pref: "nativeMkb.forcedGames",
- multiLines: !0,
- note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))
- },
- "mkb.enabled",
- "mkb.cursor.hideIdle"
- ],
- ...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {
- unsupported: !0,
- unsupportedNote: CE("a", {
- href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",
- target: "_blank"
- }, "⚠️ " + t("browser-unsupported-feature"))
- } : {}
- }, {
- requiredVariants: "full",
- group: "touch-control",
- label: t("touch-controller"),
- items: [
- {
- pref: "touchController.mode",
- note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))
- },
- "touchController.autoOff",
- "touchController.opacity.default",
- "touchController.style.standard",
- "touchController.style.custom"
- ],
- ...!STATES.userAgent.capabilities.touch ? {
- unsupported: !0,
- unsupportedNote: "⚠️ " + t("device-unsupported-touch")
- } : {}
- }, {
- group: "ui",
- label: t("ui"),
- items: [
- "ui.layout",
- "ui.imageQuality",
- "ui.gameCard.waitTime.show",
- "ui.controllerStatus.show",
- "ui.streamMenu.simplify",
- "ui.splashVideo.skip",
- !AppInterface && "ui.hideScrollbar",
- "ui.systemMenu.hideHandle",
- "ui.feedbackDialog.disabled",
- "ui.reduceAnimations",
- {
- pref: "ui.hideSections",
- multiLines: !0
- },
- {
- pref: "block.features",
- multiLines: !0
- }
- ]
- }, {
- requiredVariants: "full",
- group: "game-bar",
- label: t("game-bar"),
- items: [
- "gameBar.position"
- ]
- }, {
- group: "loading-screen",
- label: t("loading-screen"),
- items: [
- "loadingScreen.gameArt.show",
- "loadingScreen.waitTime.show",
- "loadingScreen.rocket"
- ]
- }, {
- group: "other",
- label: t("other"),
- items: [
- "block.tracking"
- ]
- }, {
- group: "advanced",
- label: t("advanced"),
- items: [
- {
- pref: "userAgent.profile",
- multiLines: !0,
- onCreated: (setting, $control) => {
- let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {
- type: "text",
- placeholder: defaultUserAgent,
- autocomplete: "off",
- class: "bx-settings-custom-user-agent",
- tabindex: 0
- });
- $inpCustomUserAgent.addEventListener("input", (e) => {
- let profile = $control.value, custom = e.target.value.trim();
- UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);
- }), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {
- orientation: "vertical"
- });
- }
- }
- ]
- }, {
- group: "footer",
- items: [
- ($parent) => {
- try {
- let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);
- $parent.appendChild(CE("div", {
- class: "bx-settings-app-version"
- }, `xCloud website version ${appVersion} (${appDate})`));
- } catch (e) {}
- },
- ($parent) => {
- $parent.appendChild(CE("a", {
- class: "bx-donation-link",
- href: "https://ko-fi.com/redphx",
- target: "_blank",
- tabindex: 0
- }, `❤️ ${t("support-better-xcloud")}`));
- },
- ($parent) => {
- $parent.appendChild(createButton({
- label: t("clear-data"),
- style: 8 | 128 | 64,
- onClick: (e) => {
- if (confirm(t("clear-data-confirm"))) clearAllData();
- }
- }));
- },
- ($parent) => {
- $parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({
- label: "Debug info",
- style: 8 | 128 | 64,
- onClick: (e) => {
- let $button = e.target.closest("button");
- if (!$button) return;
- let $pre = $button.nextElementSibling;
- if (!$pre) {
- let debugInfo = deepClone(BX_FLAGS.DeviceInfo);
- debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {
- class: "bx-focusable bx-gone",
- tabindex: 0,
- _on: {
- click: async (e2) => {
- await copyToClipboard(e2.target.innerText);
- }
- }
- }, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);
- }
- $pre.classList.toggle("bx-gone"), $pre.scrollIntoView();
- }
- })));
- }
- ]
- }];
- TAB_DISPLAY_ITEMS = [{
- requiredVariants: "full",
- group: "audio",
- label: t("audio"),
- helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",
- items: [{
- pref: "audio.volume",
- params: {
- disabled: !getGlobalPref("audio.volume.booster.enabled")
- },
- onCreated: (setting, $elm) => {
- let $range = $elm.querySelector("input[type=range");
- BxEventBus.Stream.on("setting.changed", (payload) => {
- let { settingKey } = payload;
- if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });
- });
- }
- }]
- }, {
- group: "video",
- label: t("video"),
- helpUrl: "https://better-xcloud.github.io/ingame-features/#video",
- items: [
- "video.player.type",
- "video.maxFps",
- "video.player.powerPreference",
- "video.processing",
- "video.ratio",
- "video.position",
- "video.processing.sharpness",
- "video.saturation",
- "video.contrast",
- "video.brightness"
- ]
- }];
- TAB_CONTROLLER_ITEMS = [
- {
- group: "controller",
- label: t("controller"),
- helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",
- items: [
- "localCoOp.enabled",
- "controller.pollingRate",
- ($parent) => {
- $parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));
- }
- ]
- },
- STATES.userAgent.capabilities.touch && {
- group: "touch-control",
- label: t("touch-controller"),
- items: [{
- label: t("layout"),
- content: CE("select", {
- disabled: !0
- }, CE("option", !1, t("default"))),
- onCreated: (setting, $elm) => {
- $elm.addEventListener("input", (e) => {
- TouchController.applyCustomLayout($elm.value, 1000);
- }), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {
- let customLayouts = TouchController.getCustomLayouts();
- while ($elm.firstChild)
- $elm.removeChild($elm.firstChild);
- if ($elm.disabled = !customLayouts, !customLayouts) {
- $elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));
- return;
- }
- let $fragment = document.createDocumentFragment();
- for (let key in customLayouts.layouts) {
- let layout = customLayouts.layouts[key], name;
- if (layout.author) name = `${layout.name} (${layout.author})`;
- else name = layout.name;
- let $option = CE("option", { value: key }, name);
- $fragment.appendChild($option);
- }
- $elm.appendChild($fragment), $elm.value = customLayouts.default_layout;
- });
- }
- }]
- },
- STATES.browser.capabilities.deviceVibration && {
- group: "device",
- label: t("device"),
- items: [{
- pref: "deviceVibration.mode",
- multiLines: !0,
- unsupported: !STATES.browser.capabilities.deviceVibration
- }, {
- pref: "deviceVibration.intensity",
- unsupported: !STATES.browser.capabilities.deviceVibration
- }]
- }
- ];
- TAB_MKB_ITEMS = [
- {
- requiredVariants: "full",
- group: "mkb",
- label: t("mouse-and-keyboard"),
- helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",
- items: [
- ($parent) => {
- $parent.appendChild(MkbExtraSettings.renderSettings.apply(this));
- }
- ]
- },
- NativeMkbHandler.isAllowed() && {
- requiredVariants: "full",
- group: "native-mkb",
- label: t("native-mkb"),
- items: [
- "nativeMkb.scroll.sensitivityY",
- "nativeMkb.scroll.sensitivityX"
- ]
- }
- ];
- TAB_STATS_ITEMS = [{
- group: "stats",
- label: t("stream-stats"),
- helpUrl: "https://better-xcloud.github.io/stream-stats/",
- items: [
- "stats.showWhenPlaying",
- "stats.quickGlance.enabled",
- "stats.items",
- "stats.position",
- "stats.textSize",
- "stats.opacity.all",
- "stats.opacity.background",
- "stats.colors"
- ]
- }];
- SETTINGS_UI = {
- global: {
- group: "global",
- icon: BxIcon.HOME,
- items: this.TAB_GLOBAL_ITEMS
- },
- stream: {
- group: "stream",
- icon: BxIcon.DISPLAY,
- items: this.TAB_DISPLAY_ITEMS
- },
- controller: {
- group: "controller",
- icon: BxIcon.CONTROLLER,
- items: this.TAB_CONTROLLER_ITEMS,
- requiredVariants: "full"
- },
- mkb: {
- group: "mkb",
- icon: BxIcon.NATIVE_MKB,
- items: this.TAB_MKB_ITEMS,
- requiredVariants: "full"
- },
- stats: {
- group: "stats",
- icon: BxIcon.STREAM_STATS,
- items: this.TAB_STATS_ITEMS
- }
- };
- constructor() {
- super();
- BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {
- if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
- let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);
- if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;
- }), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
- this.$tabContents.dataset.gameId = id.toString();
- });
- }
- getDialog() {
- return this;
- }
- getContent() {
- return this.$container;
- }
- onMounted() {
- super.onMounted();
- }
- isOverlayVisible() {
- return !STATES.isPlaying;
- }
- reloadPage() {
- this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
- }
- isSupportedVariant(requiredVariants) {
- if (typeof requiredVariants === "undefined") return !0;
- return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);
- }
- onTabClicked = (e) => {
- let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);
- for ($child of children)
- if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);
- else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");
- this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");
- for (let $child2 of Array.from(this.$tabs.children))
- $child2.classList.remove("bx-active");
- $svg.classList.add("bx-active");
- };
- renderTab(settingTab) {
- let $svg = createSvgIcon(settingTab.icon);
- return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;
- }
- onGlobalSettingChanged = (e) => {
- PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
- };
- onContextMenu(e) {
- e.preventDefault();
- let $elm = e.target;
- $elm instanceof HTMLElement && this.resetHighlightedSetting($elm);
- }
- renderServerSetting(setting) {
- let selectedValue = getGlobalPref("server.region"), continents = {
- "america-north": {
- label: t("continent-north-america")
- },
- "america-south": {
- label: t("continent-south-america")
- },
- asia: {
- label: t("continent-asia")
- },
- australia: {
- label: t("continent-australia")
- },
- europe: {
- label: t("continent-europe")
- },
- other: {
- label: t("other")
- }
- }, $control = CE("select", {
- id: `bx_setting_${escapeCssSelector(setting.pref)}`,
- tabindex: 0
- });
- $control.name = $control.id, $control.addEventListener("input", (e) => {
- setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);
- }), setting.options = {};
- for (let regionName in STATES.serverRegions) {
- let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
- if (region.isDefault) {
- if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
- }
- setting.options[value] = label;
- let $option = CE("option", { value }, label), continent = continents[region.contintent];
- if (!continent.children) continent.children = [];
- continent.children.push($option);
- }
- let fragment = document.createDocumentFragment(), key;
- for (key in continents) {
- let continent = continents[key];
- if (!continent.children) continue;
- fragment.appendChild(CE("optgroup", {
- label: continent.label
- }, ...continent.children));
- }
- return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
- }
- renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
- if (typeof setting === "string") setting = {
- pref: setting
- };
- let pref = setting.pref, $control;
- if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);
- else $control = setting.content;
- else if (!setting.unsupported) {
- if (pref === "server.region") $control = this.renderServerSetting(setting);
- else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {
- let newLocale = e.target.value;
- if (getGlobalPref("ui.controllerFriendly")) {
- let timeoutId = e.target.timeoutId;
- timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {
- Translations.refreshLocale(newLocale), Translations.updateTranslations();
- }, 500);
- } else Translations.refreshLocale(newLocale), Translations.updateTranslations();
- this.onGlobalSettingChanged(e);
- });
- else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {
- let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);
- UserAgent.updateStorage(value);
- let $inp = $control.nextElementSibling;
- $inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);
- });
- else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);
- if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);
- }
- let prefDefinition = null;
- if (pref) prefDefinition = getPrefInfo(pref).definition;
- if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;
- let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;
- if (typeof note === "function") note = note();
- if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();
- if (settingTabContent.label && setting.pref) {
- if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);
- }
- if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");
- else note = `${t("experimental")}: ${note}`;
- let $note;
- if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);
- else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);
- let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
- $note,
- multiLines: setting.multiLines,
- icon: prefDefinition?.labelIcon,
- onContextMenu: this.boundOnContextMenu,
- pref
- });
- if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;
- $row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
- }
- renderSettingsSection(settingTab, sections) {
- let $tabContent = CE("div", {
- class: "bx-gone",
- _dataset: {
- tabGroup: settingTab.group
- }
- });
- for (let section of sections) {
- if (!section) continue;
- if (section instanceof HTMLElement) {
- $tabContent.appendChild(section);
- continue;
- }
- if (!this.isSupportedVariant(section.requiredVariants)) continue;
- if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;
- let label = section.label;
- if (label === t("better-xcloud")) {
- if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
- label = createButton({
- label,
- url: "https://github.com/redphx/better-xcloud/releases",
- style: 4096 | 16 | 64
- });
- }
- if (label) {
- let $title = CE("h2", {
- _nearby: {
- orientation: "horizontal"
- }
- }, CE("span", !1, label), section.helpUrl && createButton({
- icon: BxIcon.QUESTION,
- style: 8 | 64,
- url: section.helpUrl,
- title: t("help")
- }));
- $tabContent.appendChild($title);
- }
- if (section.unsupportedNote) {
- let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);
- $tabContent.appendChild($note);
- }
- if (section.unsupported) continue;
- if (section.content) {
- $tabContent.appendChild(section.content);
- continue;
- }
- section.items = section.items || [];
- for (let setting of section.items) {
- if (setting === !1) continue;
- if (typeof setting === "function") {
- setting.apply(this, [$tabContent]);
- continue;
- }
- this.renderSettingRow(settingTab, $tabContent, section, setting);
- }
- }
- return $tabContent;
- }
- setupDialog() {
- let $tabs, $tabContents, $container = CE("div", {
- class: "bx-settings-dialog",
- _nearby: {
- orientation: "horizontal"
- }
- }, CE("div", {
- class: "bx-settings-tabs-container",
- _nearby: {
- orientation: "vertical",
- focus: () => {
- return this.dialogManager.focus($tabs);
- },
- loop: (direction) => {
- if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;
- return !1;
- }
- }
- }, $tabs = CE("div", {
- class: "bx-settings-tabs bx-hide-scroll-bar",
- _nearby: {
- focus: () => this.focusActiveTab()
- }
- }), CE("div", !1, this.$btnReload = createButton({
- icon: BxIcon.REFRESH,
- style: 64 | 32,
- onClick: (e) => {
- this.reloadPage();
- }
- }), createButton({
- icon: BxIcon.CLOSE,
- style: 64 | 32,
- onClick: (e) => {
- this.dialogManager.hide();
- }
- }))), CE("div", {
- class: "bx-settings-tab-contents",
- _nearby: {
- orientation: "vertical",
- loop: (direction) => {
- if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;
- return !1;
- }
- }
- }, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {
- class: "bx-settings-tab-content",
- _nearby: {
- orientation: "vertical",
- focus: () => this.jumpToSettingGroup("next")
- }
- })));
- this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {
- if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();
- });
- let settingTabGroup;
- for (settingTabGroup in this.SETTINGS_UI) {
- let settingTab = this.SETTINGS_UI[settingTabGroup];
- if (!settingTab) continue;
- if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;
- if (settingTab.group !== "global" && !this.renderFullSettings) continue;
- let $svg = this.renderTab(settingTab);
- $tabs.appendChild($svg);
- let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);
- $tabContents.appendChild($tabContent);
- }
- $tabs.firstElementChild.dispatchEvent(new Event("click"));
- }
- focusTab(tabId) {
- let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);
- $tab && $tab.dispatchEvent(new Event("click"));
- }
- focusIfNeeded() {
- this.jumpToSettingGroup("next");
- }
- focusActiveTab() {
- let $currentTab = this.$tabs.querySelector(".bx-active");
- return $currentTab && $currentTab.focus(), !0;
- }
- focusVisibleSetting(type = "first") {
- let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
- if (!controls.length) return !1;
- if (type === "last") controls.reverse();
- for (let $control of controls) {
- if (!($control instanceof HTMLElement)) continue;
- let $focusable = this.dialogManager.findFocusableElement($control);
- if ($focusable) {
- if (this.dialogManager.focus($focusable)) return !0;
- }
- }
- return !1;
- }
- focusVisibleTab(type = "first") {
- let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));
- if (!tabs.length) return !1;
- if (type === "last") tabs.reverse();
- for (let $tab of tabs)
- if (this.dialogManager.focus($tab)) return !0;
- return !1;
- }
- jumpToSettingGroup(direction) {
- let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");
- if (!$tabContent) return !1;
- let $header, $focusing = document.activeElement;
- if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");
- else {
- let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;
- while (!0) {
- if (!$tmp) break;
- if ($tmp.tagName === "H2") {
- if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {
- if (++times, direction === "next" || times >= 2) break;
- }
- }
- $tmp = $tmp[siblingProperty];
- }
- }
- let $target;
- if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);
- if ($target) return this.dialogManager.focus($target);
- return !1;
- }
- resetHighlightedSetting($elm) {
- let targetGameId = SettingsManager.getInstance().getTargetGameId();
- if (targetGameId < 0) return;
- if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;
- let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");
- if (!$row) return;
- let pref = $row.prefKey;
- if (!pref) alert("Pref not found: " + $row.id);
- if (!isStreamPref(pref)) return;
- let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref);
- if (deleted) BxEventBus.Stream.emit("setting.changed", {
- storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,
- settingKey: pref
- });
- return deleted;
- }
- handleKeyPress(key) {
- let handled = !0;
- switch (key) {
- case "Tab":
- this.focusActiveTab();
- break;
- case "Home":
- this.focusVisibleSetting("first");
- break;
- case "End":
- this.focusVisibleSetting("last");
- break;
- case "PageUp":
- this.jumpToSettingGroup("previous");
- break;
- case "PageDown":
- this.jumpToSettingGroup("next");
- break;
- case "KeyQ":
- this.resetHighlightedSetting();
- break;
- default:
- handled = !1;
- break;
- }
- return handled;
- }
- handleGamepad(button) {
- let handled = !0;
- switch (button) {
- case 1:
- let $focusing = document.activeElement;
- if ($focusing && this.$tabs.contains($focusing)) this.hide();
- else this.focusActiveTab();
- break;
- case 4:
- case 5:
- this.focusActiveTab();
- break;
- case 6:
- this.jumpToSettingGroup("previous");
- break;
- case 7:
- this.jumpToSettingGroup("next");
- break;
- case 2:
- this.resetHighlightedSetting();
- break;
- default:
- handled = !1;
- break;
- }
- return handled;
- }
+static instance;
+static getInstance = () => SettingsDialog.instance ?? (SettingsDialog.instance = new SettingsDialog);
+LOG_TAG = "SettingsNavigationDialog";
+$container;
+$tabs;
+$tabContents;
+$btnReload;
+$btnGlobalReload;
+$noteGlobalReload;
+$btnSuggestion;
+$streamSettingsSelection;
+renderFullSettings;
+boundOnContextMenu;
+suggestedSettings = {
+recommended: {},
+default: {},
+lowest: {},
+highest: {}
+};
+settingLabels = {};
+settingsManager;
+TAB_GLOBAL_ITEMS = [{
+group: "general",
+label: t("better-xcloud"),
+helpUrl: "https://better-xcloud.github.io/features/",
+items: [
+($parent) => {
+let PREF_LATEST_VERSION = getGlobalPref("version.latest"), topButtons = [];
+if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION) {
+let opts = {
+label: "🌟 " + t("new-version-available", { version: PREF_LATEST_VERSION }),
+style: 1 | 64 | 128
+};
+if (AppInterface && AppInterface.updateLatestScript) opts.onClick = (e) => AppInterface.updateLatestScript();
+else opts.url = "https://github.com/redphx/better-xcloud/releases/latest";
+topButtons.push(createButton(opts));
+}
+if (AppInterface) topButtons.push(createButton({
+label: t("app-settings"),
+icon: BxIcon.STREAM_SETTINGS,
+style: 128 | 64,
+onClick: (e) => {
+AppInterface.openAppSettings && AppInterface.openAppSettings(), this.hide();
+}
+}));
+else if (UserAgent.getDefault().toLowerCase().includes("android")) topButtons.push(createButton({
+label: "🔥 " + t("install-android"),
+style: 128 | 64,
+url: "https://better-xcloud.github.io/android"
+}));
+this.$btnGlobalReload = createButton({
+label: t("settings-reload"),
+classes: ["bx-settings-reload-button", "bx-gone"],
+style: 64 | 128,
+onClick: (e) => {
+this.reloadPage();
+}
+}), topButtons.push(this.$btnGlobalReload), this.$noteGlobalReload = CE("span", {
+class: "bx-settings-reload-note"
+}, t("settings-reload-note")), topButtons.push(this.$noteGlobalReload), this.$btnSuggestion = CE("div", {
+class: "bx-suggest-toggler bx-focusable",
+tabindex: 0
+}, CE("label", !1, t("suggest-settings")), CE("span", !1, "❯")), this.$btnSuggestion.addEventListener("click", SuggestionsSetting.renderSuggestions.bind(this)), topButtons.push(this.$btnSuggestion);
+let $div = CE("div", {
+class: "bx-top-buttons",
+_nearby: {
+orientation: "vertical"
+}
+}, ...topButtons);
+$parent.appendChild($div);
+},
+{
+pref: "bx.locale",
+multiLines: !0
+},
+"server.bypassRestriction",
+"ui.controllerFriendly",
+"xhome.enabled"
+]
+}, {
+group: "server",
+label: t("server"),
+items: [
+{
+pref: "server.region",
+multiLines: !0
+},
+{
+pref: "stream.locale",
+multiLines: !0
+},
+"server.ipv6.prefer"
+]
+}, {
+group: "stream",
+label: t("stream"),
+items: [
+"stream.video.resolution",
+"stream.video.codecProfile",
+"stream.video.maxBitrate",
+"audio.volume.booster.enabled",
+"screenshot.applyFilters",
+"audio.mic.onPlaying",
+"game.fortnite.forceConsole",
+"stream.video.combineAudio"
+]
+}, {
+requiredVariants: "full",
+group: "mkb",
+label: t("mouse-and-keyboard"),
+items: [
+"nativeMkb.mode",
+{
+pref: "nativeMkb.forcedGames",
+multiLines: !0,
+note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/574", target: "_blank" }, t("unofficial-game-list"))
+},
+"mkb.enabled",
+"mkb.cursor.hideIdle"
+],
+...!STATES.browser.capabilities.emulatedNativeMkb && (!STATES.userAgent.capabilities.mkb || !STATES.browser.capabilities.mkb) ? {
+unsupported: !0,
+unsupportedNote: CE("a", {
+href: "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657",
+target: "_blank"
+}, "⚠️ " + t("browser-unsupported-feature"))
+} : {}
+}, {
+requiredVariants: "full",
+group: "touch-control",
+label: t("touch-controller"),
+items: [
+{
+pref: "touchController.mode",
+note: CE("a", { href: "https://github.com/redphx/better-xcloud/discussions/241", target: "_blank" }, t("unofficial-game-list"))
+},
+"touchController.autoOff",
+"touchController.opacity.default",
+"touchController.style.standard",
+"touchController.style.custom"
+],
+...!STATES.userAgent.capabilities.touch ? {
+unsupported: !0,
+unsupportedNote: "⚠️ " + t("device-unsupported-touch")
+} : {}
+}, {
+group: "ui",
+label: t("ui"),
+items: [
+"ui.layout",
+"ui.imageQuality",
+"ui.gameCard.waitTime.show",
+"ui.controllerStatus.show",
+"ui.streamMenu.simplify",
+"ui.splashVideo.skip",
+!AppInterface && "ui.hideScrollbar",
+"ui.systemMenu.hideHandle",
+"ui.feedbackDialog.disabled",
+"ui.reduceAnimations",
+{
+pref: "ui.hideSections",
+multiLines: !0
+},
+{
+pref: "block.features",
+multiLines: !0
+}
+]
+}, {
+requiredVariants: "full",
+group: "game-bar",
+label: t("game-bar"),
+items: [
+"gameBar.position"
+]
+}, {
+group: "loading-screen",
+label: t("loading-screen"),
+items: [
+"loadingScreen.gameArt.show",
+"loadingScreen.waitTime.show",
+"loadingScreen.rocket"
+]
+}, {
+group: "other",
+label: t("other"),
+items: [
+"block.tracking"
+]
+}, {
+group: "advanced",
+label: t("advanced"),
+items: [
+{
+pref: "userAgent.profile",
+multiLines: !0,
+onCreated: (setting, $control) => {
+let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent, $inpCustomUserAgent = CE("input", {
+type: "text",
+placeholder: defaultUserAgent,
+autocomplete: "off",
+class: "bx-settings-custom-user-agent",
+tabindex: 0
+});
+$inpCustomUserAgent.addEventListener("input", (e) => {
+let profile = $control.value, custom = e.target.value.trim();
+UserAgent.updateStorage(profile, custom), this.onGlobalSettingChanged(e);
+}), $control.insertAdjacentElement("afterend", $inpCustomUserAgent), setNearby($inpCustomUserAgent.parentElement, {
+orientation: "vertical"
+});
+}
+}
+]
+}, {
+group: "footer",
+items: [
+($parent) => {
+try {
+let appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);
+$parent.appendChild(CE("div", {
+class: "bx-settings-app-version"
+}, `xCloud website version ${appVersion} (${appDate})`));
+} catch (e) {}
+},
+($parent) => {
+$parent.appendChild(CE("a", {
+class: "bx-donation-link",
+href: "https://ko-fi.com/redphx",
+target: "_blank",
+tabindex: 0
+}, `❤️ ${t("support-better-xcloud")}`));
+},
+($parent) => {
+$parent.appendChild(createButton({
+label: t("clear-data"),
+style: 8 | 128 | 64,
+onClick: (e) => {
+if (confirm(t("clear-data-confirm"))) clearAllData();
+}
+}));
+},
+($parent) => {
+$parent.appendChild(CE("div", { class: "bx-debug-info" }, createButton({
+label: "Debug info",
+style: 8 | 128 | 64,
+onClick: (e) => {
+let $button = e.target.closest("button");
+if (!$button) return;
+let $pre = $button.nextElementSibling;
+if (!$pre) {
+let debugInfo = deepClone(BX_FLAGS.DeviceInfo);
+debugInfo.settings = JSON.parse(window.localStorage.getItem("BetterXcloud") || "{}"), $pre = CE("pre", {
+class: "bx-focusable bx-gone",
+tabindex: 0,
+_on: {
+click: async (e2) => {
+await copyToClipboard(e2.target.innerText);
+}
+}
+}, "```\n" + JSON.stringify(debugInfo, null, " ") + "\n```"), $button.insertAdjacentElement("afterend", $pre);
+}
+$pre.classList.toggle("bx-gone"), $pre.scrollIntoView();
+}
+})));
+}
+]
+}];
+TAB_DISPLAY_ITEMS = [{
+requiredVariants: "full",
+group: "audio",
+label: t("audio"),
+helpUrl: "https://better-xcloud.github.io/ingame-features/#audio",
+items: [{
+pref: "audio.volume",
+params: {
+disabled: !getGlobalPref("audio.volume.booster.enabled")
+},
+onCreated: (setting, $elm) => {
+let $range = $elm.querySelector("input[type=range");
+BxEventBus.Stream.on("setting.changed", (payload) => {
+let { settingKey } = payload;
+if (settingKey === "audio.volume") $range.value = getStreamPref(settingKey).toString(), BxEvent.dispatch($range, "input", { ignoreOnChange: !0 });
+});
+}
+}]
+}, {
+group: "video",
+label: t("video"),
+helpUrl: "https://better-xcloud.github.io/ingame-features/#video",
+items: [
+"video.player.type",
+"video.maxFps",
+"video.player.powerPreference",
+"video.processing",
+"video.ratio",
+"video.position",
+"video.processing.sharpness",
+"video.saturation",
+"video.contrast",
+"video.brightness"
+]
+}];
+TAB_CONTROLLER_ITEMS = [
+{
+group: "controller",
+label: t("controller"),
+helpUrl: "https://better-xcloud.github.io/ingame-features/#controller",
+items: [
+"localCoOp.enabled",
+"controller.pollingRate",
+($parent) => {
+$parent.appendChild(ControllerExtraSettings.renderSettings.apply(this));
+}
+]
+},
+STATES.userAgent.capabilities.touch && {
+group: "touch-control",
+label: t("touch-controller"),
+items: [{
+label: t("layout"),
+content: CE("select", {
+disabled: !0
+}, CE("option", !1, t("default"))),
+onCreated: (setting, $elm) => {
+$elm.addEventListener("input", (e) => {
+TouchController.applyCustomLayout($elm.value, 1000);
+}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {
+let customLayouts = TouchController.getCustomLayouts();
+while ($elm.firstChild)
+$elm.removeChild($elm.firstChild);
+if ($elm.disabled = !customLayouts, !customLayouts) {
+$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("input"));
+return;
+}
+let $fragment = document.createDocumentFragment();
+for (let key in customLayouts.layouts) {
+let layout = customLayouts.layouts[key], name;
+if (layout.author) name = `${layout.name} (${layout.author})`;
+else name = layout.name;
+let $option = CE("option", { value: key }, name);
+$fragment.appendChild($option);
+}
+$elm.appendChild($fragment), $elm.value = customLayouts.default_layout;
+});
+}
+}]
+},
+STATES.browser.capabilities.deviceVibration && {
+group: "device",
+label: t("device"),
+items: [{
+pref: "deviceVibration.mode",
+multiLines: !0,
+unsupported: !STATES.browser.capabilities.deviceVibration
+}, {
+pref: "deviceVibration.intensity",
+unsupported: !STATES.browser.capabilities.deviceVibration
+}]
+}
+];
+TAB_MKB_ITEMS = [
+{
+requiredVariants: "full",
+group: "mkb",
+label: t("mouse-and-keyboard"),
+helpUrl: "https://better-xcloud.github.io/mouse-and-keyboard/",
+items: [
+($parent) => {
+$parent.appendChild(MkbExtraSettings.renderSettings.apply(this));
+}
+]
+},
+NativeMkbHandler.isAllowed() && {
+requiredVariants: "full",
+group: "native-mkb",
+label: t("native-mkb"),
+items: [
+"nativeMkb.scroll.sensitivityY",
+"nativeMkb.scroll.sensitivityX"
+]
+}
+];
+TAB_STATS_ITEMS = [{
+group: "stats",
+label: t("stream-stats"),
+helpUrl: "https://better-xcloud.github.io/stream-stats/",
+items: [
+"stats.showWhenPlaying",
+"stats.quickGlance.enabled",
+"stats.items",
+"stats.position",
+"stats.textSize",
+"stats.opacity.all",
+"stats.opacity.background",
+"stats.colors"
+]
+}];
+SETTINGS_UI = {
+global: {
+group: "global",
+icon: BxIcon.HOME,
+items: this.TAB_GLOBAL_ITEMS
+},
+stream: {
+group: "stream",
+icon: BxIcon.DISPLAY,
+items: this.TAB_DISPLAY_ITEMS
+},
+controller: {
+group: "controller",
+icon: BxIcon.CONTROLLER,
+items: this.TAB_CONTROLLER_ITEMS,
+requiredVariants: "full"
+},
+mkb: {
+group: "mkb",
+icon: BxIcon.NATIVE_MKB,
+items: this.TAB_MKB_ITEMS,
+requiredVariants: "full"
+},
+stats: {
+group: "stats",
+icon: BxIcon.STREAM_STATS,
+items: this.TAB_STATS_ITEMS
+}
+};
+constructor() {
+super();
+BxLogger.info(this.LOG_TAG, "constructor()"), this.boundOnContextMenu = this.onContextMenu.bind(this), this.settingsManager = SettingsManager.getInstance(), this.renderFullSettings = STATES.supportedRegion && STATES.isSignedIn, this.setupDialog(), this.onMountedCallbacks.push(() => {
+if (onChangeVideoPlayerType(), STATES.userAgent.capabilities.touch) BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);
+let $selectUserAgent = document.querySelector(`#bx_setting_${escapeCssSelector("userAgent.profile")}`);
+if ($selectUserAgent) $selectUserAgent.disabled = !0, BxEvent.dispatch($selectUserAgent, "input", {}), $selectUserAgent.disabled = !1;
+}), BxEventBus.Stream.on("gameSettings.switched", ({ id }) => {
+this.$tabContents.dataset.gameId = id.toString();
+});
+}
+getDialog() {
+return this;
+}
+getContent() {
+return this.$container;
+}
+onMounted() {
+super.onMounted();
+}
+isOverlayVisible() {
+return !STATES.isPlaying;
+}
+reloadPage() {
+this.$btnGlobalReload.disabled = !0, this.$btnGlobalReload.firstElementChild.textContent = t("settings-reloading"), this.hide(), FullscreenText.getInstance().show(t("settings-reloading")), window.location.reload();
+}
+isSupportedVariant(requiredVariants) {
+if (typeof requiredVariants === "undefined") return !0;
+return requiredVariants = typeof requiredVariants === "string" ? [requiredVariants] : requiredVariants, requiredVariants.includes(SCRIPT_VARIANT);
+}
+onTabClicked = (e) => {
+let $svg = e.target.closest("svg"), $child, children = Array.from(this.$tabContents.children);
+for ($child of children)
+if ($child.dataset.tabGroup === $svg.dataset.group) $child.classList.remove("bx-gone"), calculateSelectBoxes($child);
+else if ($child.dataset.tabGroup) $child.classList.add("bx-gone");
+this.$streamSettingsSelection.classList.toggle("bx-gone", $svg.dataset.group === "global");
+for (let $child2 of Array.from(this.$tabs.children))
+$child2.classList.remove("bx-active");
+$svg.classList.add("bx-active");
+};
+renderTab(settingTab) {
+let $svg = createSvgIcon(settingTab.icon);
+return $svg.dataset.group = settingTab.group, $svg.tabIndex = 0, $svg.addEventListener("click", this.onTabClicked), $svg;
+}
+onGlobalSettingChanged = (e) => {
+PatcherCache.getInstance().clear(), this.$btnReload.classList.add("bx-danger"), this.$noteGlobalReload.classList.add("bx-gone"), this.$btnGlobalReload.classList.remove("bx-gone"), this.$btnGlobalReload.classList.add("bx-danger");
+};
+onContextMenu(e) {
+e.preventDefault();
+let $elm = e.target;
+$elm instanceof HTMLElement && this.resetHighlightedSetting($elm);
+}
+renderServerSetting(setting) {
+let selectedValue = getGlobalPref("server.region"), continents = {
+"america-north": {
+label: t("continent-north-america")
+},
+"america-south": {
+label: t("continent-south-america")
+},
+asia: {
+label: t("continent-asia")
+},
+australia: {
+label: t("continent-australia")
+},
+europe: {
+label: t("continent-europe")
+},
+other: {
+label: t("other")
+}
+}, $control = CE("select", {
+id: `bx_setting_${escapeCssSelector(setting.pref)}`,
+tabindex: 0
+});
+$control.name = $control.id, $control.addEventListener("input", (e) => {
+setGlobalPref(setting.pref, e.target.value, "ui"), this.onGlobalSettingChanged(e);
+}), setting.options = {};
+for (let regionName in STATES.serverRegions) {
+let region = STATES.serverRegions[regionName], value = regionName, label = `${region.shortName} - ${regionName}`;
+if (region.isDefault) {
+if (label += ` (${t("default")})`, value = "default", selectedValue === regionName) selectedValue = "default";
+}
+setting.options[value] = label;
+let $option = CE("option", { value }, label), continent = continents[region.contintent];
+if (!continent.children) continent.children = [];
+continent.children.push($option);
+}
+let fragment = document.createDocumentFragment(), key;
+for (key in continents) {
+let continent = continents[key];
+if (!continent.children) continue;
+fragment.appendChild(CE("optgroup", {
+label: continent.label
+}, ...continent.children));
+}
+return $control.appendChild(fragment), $control.disabled = Object.keys(STATES.serverRegions).length === 0, $control.value = selectedValue, $control;
+}
+renderSettingRow(settingTab, $tabContent, settingTabContent, setting) {
+if (typeof setting === "string") setting = {
+pref: setting
+};
+let pref = setting.pref, $control;
+if (setting.content) if (typeof setting.content === "function") $control = setting.content.apply(this);
+else $control = setting.content;
+else if (!setting.unsupported) {
+if (pref === "server.region") $control = this.renderServerSetting(setting);
+else if (pref === "bx.locale") $control = SettingElement.fromPref(pref, async (e) => {
+let newLocale = e.target.value;
+if (getGlobalPref("ui.controllerFriendly")) {
+let timeoutId = e.target.timeoutId;
+timeoutId && window.clearTimeout(timeoutId), e.target.timeoutId = window.setTimeout(() => {
+Translations.refreshLocale(newLocale), Translations.updateTranslations();
+}, 500);
+} else Translations.refreshLocale(newLocale), Translations.updateTranslations();
+this.onGlobalSettingChanged(e);
+});
+else if (pref === "userAgent.profile") $control = SettingElement.fromPref("userAgent.profile", (e) => {
+let value = e.target.value, isCustom = value === "custom", userAgent2 = UserAgent.get(value);
+UserAgent.updateStorage(value);
+let $inp = $control.nextElementSibling;
+$inp.value = userAgent2, $inp.readOnly = !isCustom, $inp.disabled = !isCustom, !e.target.disabled && this.onGlobalSettingChanged(e);
+});
+else if ($control = this.settingsManager.getElement(pref, setting.params), settingTab.group === "global") $control.addEventListener("input", this.onGlobalSettingChanged);
+if ($control instanceof HTMLSelectElement) $control = BxSelectElement.create($control);
+}
+let prefDefinition = null;
+if (pref) prefDefinition = getPrefInfo(pref).definition;
+if (prefDefinition && !this.isSupportedVariant(prefDefinition.requiredVariants)) return;
+let label = prefDefinition?.label || setting.label || "", note = prefDefinition?.note || setting.note, unsupportedNote = prefDefinition?.unsupportedNote || setting.unsupportedNote, experimental = prefDefinition?.experimental || setting.experimental;
+if (typeof note === "function") note = note();
+if (typeof unsupportedNote === "function") unsupportedNote = unsupportedNote();
+if (settingTabContent.label && setting.pref) {
+if (prefDefinition?.suggest) typeof prefDefinition.suggest.lowest !== "undefined" && (this.suggestedSettings.lowest[setting.pref] = prefDefinition.suggest.lowest), typeof prefDefinition.suggest.highest !== "undefined" && (this.suggestedSettings.highest[setting.pref] = prefDefinition.suggest.highest);
+}
+if (experimental) if (label = "🧪 " + label, !note) note = t("experimental");
+else note = `${t("experimental")}: ${note}`;
+let $note;
+if (unsupportedNote) $note = CE("div", { class: "bx-settings-dialog-note" }, unsupportedNote);
+else if (note) $note = CE("div", { class: "bx-settings-dialog-note" }, note);
+let $row = createSettingRow(label, !prefDefinition?.unsupported && $control, {
+$note,
+multiLines: setting.multiLines,
+icon: prefDefinition?.labelIcon,
+onContextMenu: this.boundOnContextMenu,
+pref
+});
+if (pref) $row.htmlFor = `bx_setting_${escapeCssSelector(pref)}`;
+$row.dataset.type = settingTabContent.group, $tabContent.appendChild($row), !prefDefinition?.unsupported && setting.onCreated && setting.onCreated(setting, $control);
+}
+renderSettingsSection(settingTab, sections) {
+let $tabContent = CE("div", {
+class: "bx-gone",
+_dataset: {
+tabGroup: settingTab.group
+}
+});
+for (let section of sections) {
+if (!section) continue;
+if (section instanceof HTMLElement) {
+$tabContent.appendChild(section);
+continue;
+}
+if (!this.isSupportedVariant(section.requiredVariants)) continue;
+if (!this.renderFullSettings && settingTab.group === "global" && section.group !== "general" && section.group !== "footer") continue;
+let label = section.label;
+if (label === t("better-xcloud")) {
+if (label += " " + SCRIPT_VERSION, SCRIPT_VARIANT === "lite") label += " (Lite)";
+label = createButton({
+label,
+url: "https://github.com/redphx/better-xcloud/releases",
+style: 4096 | 16 | 64
+});
+}
+if (label) {
+let $title = CE("h2", {
+_nearby: {
+orientation: "horizontal"
+}
+}, CE("span", !1, label), section.helpUrl && createButton({
+icon: BxIcon.QUESTION,
+style: 8 | 64,
+url: section.helpUrl,
+title: t("help")
+}));
+$tabContent.appendChild($title);
+}
+if (section.unsupportedNote) {
+let $note = CE("b", { class: "bx-note-unsupported" }, section.unsupportedNote);
+$tabContent.appendChild($note);
+}
+if (section.unsupported) continue;
+if (section.content) {
+$tabContent.appendChild(section.content);
+continue;
+}
+section.items = section.items || [];
+for (let setting of section.items) {
+if (setting === !1) continue;
+if (typeof setting === "function") {
+setting.apply(this, [$tabContent]);
+continue;
+}
+this.renderSettingRow(settingTab, $tabContent, section, setting);
+}
+}
+return $tabContent;
+}
+setupDialog() {
+let $tabs, $tabContents, $container = CE("div", {
+class: "bx-settings-dialog",
+_nearby: {
+orientation: "horizontal"
+}
+}, CE("div", {
+class: "bx-settings-tabs-container",
+_nearby: {
+orientation: "vertical",
+focus: () => {
+return this.dialogManager.focus($tabs);
+},
+loop: (direction) => {
+if (direction === 1 || direction === 3) return this.focusVisibleTab(direction === 1 ? "last" : "first"), !0;
+return !1;
+}
+}
+}, $tabs = CE("div", {
+class: "bx-settings-tabs bx-hide-scroll-bar",
+_nearby: {
+focus: () => this.focusActiveTab()
+}
+}), CE("div", !1, this.$btnReload = createButton({
+icon: BxIcon.REFRESH,
+style: 64 | 32,
+onClick: (e) => {
+this.reloadPage();
+}
+}), createButton({
+icon: BxIcon.CLOSE,
+style: 64 | 32,
+onClick: (e) => {
+this.dialogManager.hide();
+}
+}))), CE("div", {
+class: "bx-settings-tab-contents",
+_nearby: {
+orientation: "vertical",
+loop: (direction) => {
+if (direction === 1 || direction === 3) return this.focusVisibleSetting(direction === 1 ? "last" : "first"), !0;
+return !1;
+}
+}
+}, this.$streamSettingsSelection = SettingsManager.getInstance().getStreamSettingsSelection(), $tabContents = CE("div", {
+class: "bx-settings-tab-content",
+_nearby: {
+orientation: "vertical",
+focus: () => this.jumpToSettingGroup("next")
+}
+})));
+this.$container = $container, this.$tabs = $tabs, this.$tabContents = $tabContents, $container.addEventListener("click", (e) => {
+if (e.target === $container) e.preventDefault(), e.stopPropagation(), this.hide();
+});
+let settingTabGroup;
+for (settingTabGroup in this.SETTINGS_UI) {
+let settingTab = this.SETTINGS_UI[settingTabGroup];
+if (!settingTab) continue;
+if (!this.isSupportedVariant(settingTab.requiredVariants)) continue;
+if (settingTab.group !== "global" && !this.renderFullSettings) continue;
+let $svg = this.renderTab(settingTab);
+$tabs.appendChild($svg);
+let $tabContent = this.renderSettingsSection.call(this, settingTab, settingTab.items);
+$tabContents.appendChild($tabContent);
+}
+$tabs.firstElementChild.dispatchEvent(new Event("click"));
+}
+focusTab(tabId) {
+let $tab = this.$container.querySelector(`.bx-settings-tabs svg[data-group=${tabId}]`);
+$tab && $tab.dispatchEvent(new Event("click"));
+}
+focusIfNeeded() {
+this.jumpToSettingGroup("next");
+}
+focusActiveTab() {
+let $currentTab = this.$tabs.querySelector(".bx-active");
+return $currentTab && $currentTab.focus(), !0;
+}
+focusVisibleSetting(type = "first") {
+let controls = Array.from(this.$tabContents.querySelectorAll("div[data-tab-group]:not(.bx-gone) > *"));
+if (!controls.length) return !1;
+if (type === "last") controls.reverse();
+for (let $control of controls) {
+if (!($control instanceof HTMLElement)) continue;
+let $focusable = this.dialogManager.findFocusableElement($control);
+if ($focusable) {
+if (this.dialogManager.focus($focusable)) return !0;
+}
+}
+return !1;
+}
+focusVisibleTab(type = "first") {
+let tabs = Array.from(this.$tabs.querySelectorAll("svg:not(.bx-gone)"));
+if (!tabs.length) return !1;
+if (type === "last") tabs.reverse();
+for (let $tab of tabs)
+if (this.dialogManager.focus($tab)) return !0;
+return !1;
+}
+jumpToSettingGroup(direction) {
+let $tabContent = this.$tabContents.querySelector("div[data-tab-group]:not(.bx-gone)");
+if (!$tabContent) return !1;
+let $header, $focusing = document.activeElement;
+if (!$focusing || !$tabContent.contains($focusing)) $header = $tabContent.querySelector("h2");
+else {
+let $parent = $focusing.closest("[data-tab-group] > *"), siblingProperty = direction === "next" ? "nextSibling" : "previousSibling", $tmp = $parent, times = 0;
+while (!0) {
+if (!$tmp) break;
+if ($tmp.tagName === "H2") {
+if ($header = $tmp, !$tmp.nextElementSibling?.classList.contains("bx-note-unsupported")) {
+if (++times, direction === "next" || times >= 2) break;
+}
+}
+$tmp = $tmp[siblingProperty];
+}
+}
+let $target;
+if ($header) $target = this.dialogManager.findNextTarget($header, 3, !1);
+if ($target) return this.dialogManager.focus($target);
+return !1;
+}
+resetHighlightedSetting($elm) {
+let targetGameId = SettingsManager.getInstance().getTargetGameId();
+if (targetGameId < 0) return;
+if (!$elm) $elm = document.activeElement instanceof HTMLElement ? document.activeElement : void 0;
+let $row = $elm?.closest("div[data-tab-group] > .bx-settings-row");
+if (!$row) return;
+let pref = $row.prefKey;
+if (!pref) alert("Pref not found: " + $row.id);
+if (!isStreamPref(pref)) return;
+let deleted = STORAGE.Stream.getGameSettings(targetGameId)?.deleteSetting(pref);
+if (deleted) BxEventBus.Stream.emit("setting.changed", {
+storageKey: `${"BetterXcloud.Stream"}.${targetGameId}`,
+settingKey: pref
+});
+return deleted;
+}
+handleKeyPress(key) {
+let handled = !0;
+switch (key) {
+case "Tab":
+this.focusActiveTab();
+break;
+case "Home":
+this.focusVisibleSetting("first");
+break;
+case "End":
+this.focusVisibleSetting("last");
+break;
+case "PageUp":
+this.jumpToSettingGroup("previous");
+break;
+case "PageDown":
+this.jumpToSettingGroup("next");
+break;
+case "KeyQ":
+this.resetHighlightedSetting();
+break;
+default:
+handled = !1;
+break;
+}
+return handled;
+}
+handleGamepad(button) {
+let handled = !0;
+switch (button) {
+case 1:
+let $focusing = document.activeElement;
+if ($focusing && this.$tabs.contains($focusing)) this.hide();
+else this.focusActiveTab();
+break;
+case 4:
+case 5:
+this.focusActiveTab();
+break;
+case 6:
+this.jumpToSettingGroup("previous");
+break;
+case 7:
+this.jumpToSettingGroup("next");
+break;
+case 2:
+this.resetHighlightedSetting();
+break;
+default:
+handled = !1;
+break;
+}
+return handled;
+}
}
class ScreenshotManager {
- static instance;
- static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager);
- LOG_TAG = "ScreenshotManager";
- $download;
- $canvas;
- canvasContext;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", {
- alpha: !1,
- willReadFrequently: !1
- });
- }
- updateCanvasSize(width, height) {
- this.$canvas.width = width, this.$canvas.height = height;
- }
- updateCanvasFilters(filters) {
- this.canvasContext.filter = filters;
- }
- onAnimationEnd(e) {
- e.target.classList.remove("bx-taking-screenshot");
- }
- takeScreenshot(callback) {
- let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas;
- if (!streamPlayer || !$canvas) return;
- let $player;
- if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement();
- else $player = streamPlayer.getPlayerElement("default");
- if (!$player || !$player.isConnected) return;
- let $gameStream = $player.closest("#game-stream");
- if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");
- let canvasContext = this.canvasContext;
- if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();
- if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
- let data = $canvas.toDataURL("image/png").split(";base64,")[1];
- AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
- return;
- }
- $canvas.toBlob((blob) => {
- if (!blob) return;
- let now = +new Date, $download = this.$download;
- $download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
- }, "image/png");
- }
+static instance;
+static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager);
+LOG_TAG = "ScreenshotManager";
+$download;
+$canvas;
+canvasContext;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$download = CE("a"), this.$canvas = CE("canvas", { class: "bx-gone" }), this.canvasContext = this.$canvas.getContext("2d", {
+alpha: !1,
+willReadFrequently: !1
+});
+}
+updateCanvasSize(width, height) {
+this.$canvas.width = width, this.$canvas.height = height;
+}
+updateCanvasFilters(filters) {
+this.canvasContext.filter = filters;
+}
+onAnimationEnd(e) {
+e.target.classList.remove("bx-taking-screenshot");
+}
+takeScreenshot(callback) {
+let currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = this.$canvas;
+if (!streamPlayer || !$canvas) return;
+let $player;
+if (getGlobalPref("screenshot.applyFilters")) $player = streamPlayer.getPlayerElement();
+else $player = streamPlayer.getPlayerElement("default");
+if (!$player || !$player.isConnected) return;
+let $gameStream = $player.closest("#game-stream");
+if ($gameStream) $gameStream.addEventListener("animationend", this.onAnimationEnd, { once: !0 }), $gameStream.classList.add("bx-taking-screenshot");
+let canvasContext = this.canvasContext;
+if ($player instanceof HTMLCanvasElement) streamPlayer.getWebGL2Player().forceDrawFrame();
+if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
+let data = $canvas.toDataURL("image/png").split(";base64,")[1];
+AppInterface.saveScreenshot(currentStream.titleSlug, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
+return;
+}
+$canvas.toBlob((blob) => {
+if (!blob) return;
+let now = +new Date, $download = this.$download;
+$download.download = `${currentStream.titleSlug}-${now}.png`, $download.href = URL.createObjectURL(blob), $download.click(), URL.revokeObjectURL($download.href), $download.href = "", $download.download = "", canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
+}, "image/png");
+}
}
class RendererShortcut {
- static toggleVisibility() {
- let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
- if (!$mediaContainer) {
- BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 });
- return;
- }
- $mediaContainer.classList.toggle("bx-gone");
- let isVisible = !$mediaContainer.classList.contains("bx-gone");
- limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible });
- }
+static toggleVisibility() {
+let $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
+if (!$mediaContainer) {
+BxEventBus.Stream.emit("video.visibility.changed", { isVisible: !0 });
+return;
+}
+$mediaContainer.classList.toggle("bx-gone");
+let isVisible = !$mediaContainer.classList.contains("bx-gone");
+limitVideoPlayerFps(isVisible ? getStreamPref("video.maxFps") : 0), BxEventBus.Stream.emit("video.visibility.changed", { isVisible });
+}
}
class TrueAchievements {
- static instance;
- static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements);
- LOG_TAG = "TrueAchievements";
- $link;
- $button;
- $hiddenLink;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({
- label: t("true-achievements"),
- url: "#",
- icon: BxIcon.TRUE_ACHIEVEMENTS,
- style: 64 | 8 | 128 | 8192,
- onClick: this.onClick
- }), this.$button = createButton({
- label: t("true-achievements"),
- title: t("true-achievements"),
- icon: BxIcon.TRUE_ACHIEVEMENTS,
- style: 64,
- onClick: this.onClick
- }), this.$hiddenLink = CE("a", {
- target: "_blank"
- });
- }
- onClick = (e) => {
- e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll();
- let dataset = this.$link.dataset;
- this.open(!0, dataset.xboxTitleId, dataset.id);
- };
- updateIds(xboxTitleId, id) {
- let $link = this.$link, $button = this.$button;
- if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId;
- if (id) $link.dataset.id = id, $button.dataset.id = id;
- }
- injectAchievementsProgress($elm) {
- if (SCRIPT_VARIANT !== "full") return;
- let $parent = $elm.parentElement, $div = CE("div", {
- class: "bx-guide-home-achievements-progress"
- }, $elm), xboxTitleId;
- try {
- let $container = $parent.closest("div[class*=AchievementsPreview-module__container]");
- if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId;
- } catch (e) {}
- if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId();
- if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString();
- if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link);
- else $div.appendChild(this.$button);
- $parent.appendChild($div);
- }
- injectAchievementDetailPage($parent) {
- if (SCRIPT_VARIANT !== "full") return;
- let props = getReactProps($parent);
- if (!props) return;
- try {
- let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId;
- for (let achiev of achievementList)
- if (achiev.name === achievementName) {
- id = achiev.id, xboxTitleId = achiev.title.id;
- break;
- }
- if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link);
- } catch (e) {}
- }
- getStreamXboxTitleId() {
- return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
- }
- open(override, xboxTitleId, id) {
- if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId();
- if (AppInterface && AppInterface.openTrueAchievementsLink) {
- AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString());
- return;
- }
- let url = "https://www.trueachievements.com";
- if (xboxTitleId) {
- if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`;
- }
- this.$hiddenLink.href = url, this.$hiddenLink.click();
- }
+static instance;
+static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements);
+LOG_TAG = "TrueAchievements";
+$link;
+$button;
+$hiddenLink;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$link = createButton({
+label: t("true-achievements"),
+url: "#",
+icon: BxIcon.TRUE_ACHIEVEMENTS,
+style: 64 | 8 | 128 | 8192,
+onClick: this.onClick
+}), this.$button = createButton({
+label: t("true-achievements"),
+title: t("true-achievements"),
+icon: BxIcon.TRUE_ACHIEVEMENTS,
+style: 64,
+onClick: this.onClick
+}), this.$hiddenLink = CE("a", {
+target: "_blank"
+});
+}
+onClick = (e) => {
+e.preventDefault(), window.BX_EXPOSED.dialogRoutes?.closeAll();
+let dataset = this.$link.dataset;
+this.open(!0, dataset.xboxTitleId, dataset.id);
+};
+updateIds(xboxTitleId, id) {
+let $link = this.$link, $button = this.$button;
+if (clearDataSet($link), clearDataSet($button), xboxTitleId) $link.dataset.xboxTitleId = xboxTitleId, $button.dataset.xboxTitleId = xboxTitleId;
+if (id) $link.dataset.id = id, $button.dataset.id = id;
+}
+injectAchievementsProgress($elm) {
+if (SCRIPT_VARIANT !== "full") return;
+let $parent = $elm.parentElement, $div = CE("div", {
+class: "bx-guide-home-achievements-progress"
+}, $elm), xboxTitleId;
+try {
+let $container = $parent.closest("div[class*=AchievementsPreview-module__container]");
+if ($container) xboxTitleId = getReactProps($container).children.props.data.data.xboxTitleId;
+} catch (e) {}
+if (!xboxTitleId) xboxTitleId = this.getStreamXboxTitleId();
+if (typeof xboxTitleId !== "undefined") xboxTitleId = xboxTitleId.toString();
+if (this.updateIds(xboxTitleId), document.body.dataset.mediaType === "tv") $div.appendChild(this.$link);
+else $div.appendChild(this.$button);
+$parent.appendChild($div);
+}
+injectAchievementDetailPage($parent) {
+if (SCRIPT_VARIANT !== "full") return;
+let props = getReactProps($parent);
+if (!props) return;
+try {
+let achievementList = props.children.props.data.data, $header = $parent.querySelector("div[class*=AchievementDetailHeader]"), achievementName = getReactProps($header).children[0].props.achievementName, id, xboxTitleId;
+for (let achiev of achievementList)
+if (achiev.name === achievementName) {
+id = achiev.id, xboxTitleId = achiev.title.id;
+break;
+}
+if (id) this.updateIds(xboxTitleId, id), $parent.appendChild(this.$link);
+} catch (e) {}
+}
+getStreamXboxTitleId() {
+return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
+}
+open(override, xboxTitleId, id) {
+if (!xboxTitleId || xboxTitleId === "undefined") xboxTitleId = this.getStreamXboxTitleId();
+if (AppInterface && AppInterface.openTrueAchievementsLink) {
+AppInterface.openTrueAchievementsLink(override, xboxTitleId?.toString(), id?.toString());
+return;
+}
+let url = "https://www.trueachievements.com";
+if (xboxTitleId) {
+if (url += `/deeplink/${xboxTitleId}`, id) url += `/${id}`;
+}
+this.$hiddenLink.href = url, this.$hiddenLink.click();
+}
}
class VirtualControllerShortcut {
- static pressXboxButton() {
- let streamSession = window.BX_EXPOSED.streamSession;
- if (!streamSession) return;
- let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, {
- Nexus: 1,
- VirtualPhysicality: 1024
- });
- streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => {
- streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]);
- }, 100);
- }
+static pressXboxButton() {
+let streamSession = window.BX_EXPOSED.streamSession;
+if (!streamSession) return;
+let released = generateVirtualControllerMapping(0), pressed = generateVirtualControllerMapping(0, {
+Nexus: 1,
+VirtualPhysicality: 1024
+});
+streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [pressed]), setTimeout(() => {
+streamSession.onVirtualGamepadInput("systemMenu", performance.now(), [released]);
+}, 100);
+}
}
class ShortcutHandler {
- static runAction(action) {
- switch (action) {
- case "bx.settings.show":
- SettingsDialog.getInstance().show();
- break;
- case "stream.screenshot.capture":
- ScreenshotManager.getInstance().takeScreenshot();
- break;
- case "stream.video.toggle":
- RendererShortcut.toggleVisibility();
- break;
- case "stream.stats.toggle":
- StreamStats.getInstance().toggle();
- break;
- case "stream.microphone.toggle":
- MicrophoneShortcut.toggle();
- break;
- case "stream.menu.show":
- StreamUiShortcut.showHideStreamMenu();
- break;
- case "stream.sound.toggle":
- SoundShortcut.muteUnmute();
- break;
- case "stream.volume.inc":
- SoundShortcut.adjustGainNodeVolume(10);
- break;
- case "stream.volume.dec":
- SoundShortcut.adjustGainNodeVolume(-10);
- break;
- case "device.brightness.inc":
- case "device.brightness.dec":
- case "device.sound.toggle":
- case "device.volume.inc":
- case "device.volume.dec":
- AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
- break;
- case "mkb.toggle":
- if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle();
- else EmulatedMkbHandler.getInstance()?.toggle();
- break;
- case "ta.open":
- TrueAchievements.getInstance().open(!1);
- break;
- case "controller.xbox.press":
- VirtualControllerShortcut.pressXboxButton();
- break;
- }
- }
+static runAction(action) {
+switch (action) {
+case "bx.settings.show":
+SettingsDialog.getInstance().show();
+break;
+case "stream.screenshot.capture":
+ScreenshotManager.getInstance().takeScreenshot();
+break;
+case "stream.video.toggle":
+RendererShortcut.toggleVisibility();
+break;
+case "stream.stats.toggle":
+StreamStats.getInstance().toggle();
+break;
+case "stream.microphone.toggle":
+MicrophoneShortcut.toggle();
+break;
+case "stream.menu.show":
+StreamUiShortcut.showHideStreamMenu();
+break;
+case "stream.sound.toggle":
+SoundShortcut.muteUnmute();
+break;
+case "stream.volume.inc":
+SoundShortcut.adjustGainNodeVolume(10);
+break;
+case "stream.volume.dec":
+SoundShortcut.adjustGainNodeVolume(-10);
+break;
+case "device.brightness.inc":
+case "device.brightness.dec":
+case "device.sound.toggle":
+case "device.volume.inc":
+case "device.volume.dec":
+AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
+break;
+case "mkb.toggle":
+if (STATES.currentStream.titleInfo?.details.hasMkbSupport) NativeMkbHandler.getInstance()?.toggle();
+else EmulatedMkbHandler.getInstance()?.toggle();
+break;
+case "ta.open":
+TrueAchievements.getInstance().open(!1);
+break;
+case "controller.xbox.press":
+VirtualControllerShortcut.pressXboxButton();
+break;
+}
+}
}
class ControllerShortcut {
- static buttonsCache = {};
- static buttonsStatus = {};
- static reset(index) {
- ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = [];
- }
- static handle(gamepad) {
- let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
- if (!controllerSettings) return !1;
- let actions = controllerSettings.shortcuts;
- if (!actions) return !1;
- let gamepadIndex = gamepad.index;
- ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = [];
- let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button;
- for ([index, button] of entries)
- if (button.pressed && index !== 16) {
- if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {
- let idx = index;
- setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0);
- }
- }
- return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed;
- }
+static buttonsCache = {};
+static buttonsStatus = {};
+static reset(index) {
+ControllerShortcut.buttonsCache[index] = [], ControllerShortcut.buttonsStatus[index] = [];
+}
+static handle(gamepad) {
+let controllerSettings = window.BX_STREAM_SETTINGS.controllers[gamepad.id];
+if (!controllerSettings) return !1;
+let actions = controllerSettings.shortcuts;
+if (!actions) return !1;
+let gamepadIndex = gamepad.index;
+ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.buttonsStatus[gamepadIndex] = [];
+let pressed = [], otherButtonPressed = !1, entries = gamepad.buttons.entries(), index, button;
+for ([index, button] of entries)
+if (button.pressed && index !== 16) {
+if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {
+let idx = index;
+setTimeout(() => ShortcutHandler.runAction(actions[idx]), 0);
+}
+}
+return ControllerShortcut.buttonsStatus[gamepadIndex] = pressed, otherButtonPressed;
+}
}
var FeatureGates = {
- PwaPrompt: !1,
- EnableWifiWarnings: !1,
- EnableUpdateRequiredPage: !1,
- ShowForcedUpdateScreen: !1,
- EnableTakControlResizing: !0
+PwaPrompt: !1,
+EnableWifiWarnings: !1,
+EnableUpdateRequiredPage: !1,
+ShowForcedUpdateScreen: !1,
+EnableTakControlResizing: !0
}, nativeMkbMode = getGlobalPref("nativeMkb.mode");
if (nativeMkbMode !== "default") FeatureGates.EnableMouseAndKeyboard = nativeMkbMode === "on";
var blockFeatures = getGlobalPref("block.features");
@@ -7860,1238 +7865,1238 @@ if (blockFeatures.includes("friends")) FeatureGates.EnableFriendsAndFollowers =
if (blockFeatures.includes("byog")) FeatureGates.EnableBYOG = !1, FeatureGates.EnableBYOGPurchase = !1;
if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
class LocalCoOpManager {
- static instance;
- static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager);
- supportedIds;
- constructor() {
- BxEventBus.Script.once("list.localCoOp.updated", (e) => {
- this.supportedIds = e.ids;
- }), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds);
- }
- isSupported(productId) {
- return this.supportedIds.has(productId);
- }
+static instance;
+static getInstance = () => LocalCoOpManager.instance ?? (LocalCoOpManager.instance = new LocalCoOpManager);
+supportedIds;
+constructor() {
+BxEventBus.Script.once("list.localCoOp.updated", (e) => {
+this.supportedIds = e.ids;
+}), this.supportedIds = GhPagesUtils.getLocalCoOpList(), console.log("this.supportedIds", this.supportedIds);
+}
+isSupported(productId) {
+return this.supportedIds.has(productId);
+}
}
var BxExposed = {
- getTitleInfo: () => STATES.currentStream.titleInfo,
- modifyPreloadedState: (state) => {
- let LOG_TAG3 = "PreloadState";
- try {
- state.appContext.requestInfo.userAgent = window.navigator.userAgent;
- } catch (e) {
- BxLogger.error(LOG_TAG3, e);
- }
- try {
- for (let exp in FeatureGates)
- state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];
- } catch (e) {
- BxLogger.error(LOG_TAG3, e);
- }
- try {
- let sigls = state.xcloud.sigls;
- if (STATES.userAgent.capabilities.touch) {
- let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470";
- if (siglId in sigls) {
- let allGames = sigls[siglId].data.products;
- customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);
- } else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId);
- }
- } catch (e) {
- BxLogger.error(LOG_TAG3, e);
- }
- try {
- let sigls = state.xcloud.sigls;
- if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
- } catch (e) {
- BxLogger.error(LOG_TAG3, e);
- }
- try {
- let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;
- if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";
- } catch (e) {
- BxLogger.error(LOG_TAG3, e);
- }
- return state;
- },
- modifyTitleInfo: function(titleInfo) {
- titleInfo = deepClone(titleInfo);
- let supportedInputTypes = titleInfo.details.supportedInputTypes;
- if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB");
- if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB");
- if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) {
- let touchControllerAvailability = getGlobalPref("touchController.mode");
- if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) {
- let gamepads = window.navigator.getGamepads(), gamepadFound = !1;
- for (let gamepad of gamepads)
- if (gamepad && gamepad.connected) {
- gamepadFound = !0;
- break;
- }
- gamepadFound && (touchControllerAvailability = "off");
- }
- if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = [];
- if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch");
- }
- return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo;
- },
- setupGainNode: ($media, audioStream) => {
- if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {
- $media.muted = !0, $media.pause();
- });
- else $media.muted = !0, $media.addEventListener("playing", (e) => {
- $media.muted = !0;
- });
- try {
- let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();
- source.connect(gainNode).connect(audioCtx.destination);
- } catch (e) {
- BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;
- }
- },
- handleControllerShortcut: ControllerShortcut.handle,
- resetControllerShortcut: ControllerShortcut.reset,
- overrideSettings: {
- Tv_settings: {
- hasCompletedOnboarding: !0
- }
- },
- disableGamepadPolling: !1,
- backButtonPressed: () => {
- let navigationDialogManager = NavigationDialogManager.getInstance();
- if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;
- let dict = {
- bubbles: !0,
- cancelable: !0,
- key: "XF86Back",
- code: "XF86Back",
- keyCode: 4,
- which: 4
- };
- return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;
- },
- GameSlugRegexes: [
- /[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g,
- / {2,}/g,
- / /g
- ],
- toggleLocalCoOp(enable) {},
- beforePageLoad: (page) => {
- BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);
- },
- localCoOpManager: LocalCoOpManager.getInstance(),
- reactCreateElement: function(...args) {},
- createReactLocalCoOpIcon: (attrs) => {
- let reactCE = window.BX_EXPOSED.reactCreateElement;
- return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" })));
- }
+getTitleInfo: () => STATES.currentStream.titleInfo,
+modifyPreloadedState: (state) => {
+let LOG_TAG3 = "PreloadState";
+try {
+state.appContext.requestInfo.userAgent = window.navigator.userAgent;
+} catch (e) {
+BxLogger.error(LOG_TAG3, e);
+}
+try {
+for (let exp in FeatureGates)
+state.experiments.overrideFeatureGates[exp.toLocaleLowerCase()] = FeatureGates[exp];
+} catch (e) {
+BxLogger.error(LOG_TAG3, e);
+}
+try {
+let sigls = state.xcloud.sigls;
+if (STATES.userAgent.capabilities.touch) {
+let customList = TouchController.getCustomList(), siglId = "ce573635-7c18-4d0c-9d68-90b932393470";
+if (siglId in sigls) {
+let allGames = sigls[siglId].data.products;
+customList = customList.filter((id) => allGames.includes(id)), sigls["9c86f07a-f3e8-45ad-82a0-a1f759597059"]?.data.products.push(...customList);
+} else BxLogger.warning(LOG_TAG3, "Sigl not found: " + siglId);
+}
+} catch (e) {
+BxLogger.error(LOG_TAG3, e);
+}
+try {
+let sigls = state.xcloud.sigls;
+if (BX_FLAGS.ForceNativeMkbTitles) sigls["8fa264dd-124f-4af3-97e8-596fcdf4b486"]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
+} catch (e) {
+BxLogger.error(LOG_TAG3, e);
+}
+try {
+let xCloud = state.xcloud.authentication.authStatusByStrategy.XCloud;
+if (xCloud.type === 3 && xCloud.error.type === "UnsupportedMarketError") window.stop(), window.location.href = "https://www.xbox.com/en-US/play";
+} catch (e) {
+BxLogger.error(LOG_TAG3, e);
+}
+return state;
+},
+modifyTitleInfo: function(titleInfo) {
+titleInfo = deepClone(titleInfo);
+let supportedInputTypes = titleInfo.details.supportedInputTypes;
+if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId)) supportedInputTypes.push("MKB");
+if (getGlobalPref("nativeMkb.mode") === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "MKB");
+if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes("MKB"), STATES.userAgent.capabilities.touch) {
+let touchControllerAvailability = getGlobalPref("touchController.mode");
+if (touchControllerAvailability !== "off" && getGlobalPref("touchController.autoOff")) {
+let gamepads = window.navigator.getGamepads(), gamepadFound = !1;
+for (let gamepad of gamepads)
+if (gamepad && gamepad.connected) {
+gamepadFound = !0;
+break;
+}
+gamepadFound && (touchControllerAvailability = "off");
+}
+if (touchControllerAvailability === "off") supportedInputTypes = supportedInputTypes.filter((i) => i !== "CustomTouchOverlay" && i !== "GenericTouch"), titleInfo.details.supportedTabs = [];
+if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes("NativeTouch"), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes("CustomTouchOverlay") || supportedInputTypes.includes("GenericTouch"), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push("GenericTouch");
+}
+return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEventBus.Script.emit("titleInfo.ready", {}), titleInfo;
+},
+setupGainNode: ($media, audioStream) => {
+if ($media instanceof HTMLAudioElement) $media.muted = !0, $media.addEventListener("playing", (e) => {
+$media.muted = !0, $media.pause();
+});
+else $media.muted = !0, $media.addEventListener("playing", (e) => {
+$media.muted = !0;
+});
+try {
+let audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();
+source.connect(gainNode).connect(audioCtx.destination);
+} catch (e) {
+BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;
+}
+},
+handleControllerShortcut: ControllerShortcut.handle,
+resetControllerShortcut: ControllerShortcut.reset,
+overrideSettings: {
+Tv_settings: {
+hasCompletedOnboarding: !0
+}
+},
+disableGamepadPolling: !1,
+backButtonPressed: () => {
+let navigationDialogManager = NavigationDialogManager.getInstance();
+if (navigationDialogManager.isShowing()) return navigationDialogManager.hide(), !0;
+let dict = {
+bubbles: !0,
+cancelable: !0,
+key: "XF86Back",
+code: "XF86Back",
+keyCode: 4,
+which: 4
+};
+return document.body.dispatchEvent(new KeyboardEvent("keydown", dict)), document.body.dispatchEvent(new KeyboardEvent("keyup", dict)), !1;
+},
+GameSlugRegexes: [
+/[;,/?:@&=+_`~$%#^*()!^™\xae\xa9]/g,
+/ {2,}/g,
+/ /g
+],
+toggleLocalCoOp(enable) {},
+beforePageLoad: (page) => {
+BxLogger.info("beforePageLoad", page), Patcher.patchPage(page);
+},
+localCoOpManager: LocalCoOpManager.getInstance(),
+reactCreateElement: function(...args) {},
+createReactLocalCoOpIcon: (attrs) => {
+let reactCE = window.BX_EXPOSED.reactCreateElement;
+return reactCE("svg", { xmlns: "http://www.w3.org/2000/svg", width: "1em", height: "1em", viewBox: "0 0 32 32", "fill-rule": "evenodd", "stroke-linecap": "round", "stroke-linejoin": "round", ...attrs }, reactCE("g", null, reactCE("path", { d: "M24.272 11.165h-3.294l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "22.625", cy: "5.874", r: ".879" }), reactCE("path", { d: "M11.022 24.415H7.728l-3.14 3.564c-.391.391-.922.611-1.476.611a2.1 2.1 0 0 1-2.087-2.088 2.09 2.09 0 0 1 .031-.362l1.22-6.274a3.89 3.89 0 0 1 3.81-3.206h6.57c1.834 0 3.439 1.573 3.833 3.295l1.205 6.185a2.09 2.09 0 0 1 .031.362 2.1 2.1 0 0 1-2.087 2.088c-.554 0-1.085-.22-1.476-.611l-3.14-3.564", fill: "none", stroke: "#fff", "stroke-width": "2" }), reactCE("circle", { cx: "9.375", cy: "19.124", r: ".879" })));
+}
};
function localRedirect(path) {
- let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");
- if (!$pageContent) return;
- let $anchor = CE("a", {
- href: url,
- class: "bx-hidden bx-offscreen"
- }, "");
- $anchor.addEventListener("click", (e) => {
- window.setTimeout(() => {
- $pageContent.removeChild($anchor);
- }, 1000);
- }), $pageContent.appendChild($anchor), $anchor.click();
+let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");
+if (!$pageContent) return;
+let $anchor = CE("a", {
+href: url,
+class: "bx-hidden bx-offscreen"
+}, "");
+$anchor.addEventListener("click", (e) => {
+window.setTimeout(() => {
+$pageContent.removeChild($anchor);
+}, 1000);
+}), $pageContent.appendChild($anchor), $anchor.click();
}
window.localRedirect = localRedirect;
function getPreferredServerRegion(shortName = !1) {
- let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions;
- if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;
- else return preferredRegion;
- for (let regionName in serverRegions) {
- let region = serverRegions[regionName];
- if (!region.isDefault) continue;
- if (shortName && region.shortName) return region.shortName;
- else return regionName;
- }
- return null;
+let preferredRegion = getGlobalPref("server.region"), serverRegions = STATES.serverRegions;
+if (preferredRegion in serverRegions) if (shortName && serverRegions[preferredRegion].shortName) return serverRegions[preferredRegion].shortName;
+else return preferredRegion;
+for (let regionName in serverRegions) {
+let region = serverRegions[regionName];
+if (!region.isDefault) continue;
+if (shortName && region.shortName) return region.shortName;
+else return regionName;
+}
+return null;
}
class HeaderSection {
- static instance;
- static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);
- LOG_TAG = "HeaderSection";
- $btnRemotePlay;
- $btnSettings;
- $buttonsWrapper;
- observer;
- timeoutId;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({
- classes: ["bx-header-remote-play-button", "bx-gone"],
- icon: BxIcon.REMOTE_PLAY,
- title: t("remote-play"),
- style: 8 | 64 | 2048,
- onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()
- }), this.$btnSettings = createButton({
- classes: ["bx-header-settings-button"],
- label: "???",
- style: 16 | 32 | 64 | 256,
- onClick: (e) => SettingsDialog.getInstance().show()
- }), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);
- }
- injectSettingsButton($parent) {
- if (!$parent) return;
- let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings;
- if (isElementVisible(this.$buttonsWrapper)) return;
- if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
- $parent.appendChild(this.$buttonsWrapper);
- }
- checkHeader = () => {
- let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
- if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");
- $target && this.injectSettingsButton($target);
- };
- watchHeader() {
- let $root = document.querySelector("#PageContent header") || document.querySelector("#root");
- if (!$root) return;
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);
- }), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();
- }
- showRemotePlayButton() {
- this.$btnRemotePlay?.classList.remove("bx-gone");
- }
- static watchHeader() {
- HeaderSection.getInstance().watchHeader();
- }
+static instance;
+static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);
+LOG_TAG = "HeaderSection";
+$btnRemotePlay;
+$btnSettings;
+$buttonsWrapper;
+observer;
+timeoutId;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({
+classes: ["bx-header-remote-play-button", "bx-gone"],
+icon: BxIcon.REMOTE_PLAY,
+title: t("remote-play"),
+style: 8 | 64 | 2048,
+onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()
+}), this.$btnSettings = createButton({
+classes: ["bx-header-settings-button"],
+label: "???",
+style: 16 | 32 | 64 | 256,
+onClick: (e) => SettingsDialog.getInstance().show()
+}), this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings);
+}
+injectSettingsButton($parent) {
+if (!$parent) return;
+let PREF_LATEST_VERSION = getGlobalPref("version.latest"), $btnSettings = this.$btnSettings;
+if (isElementVisible(this.$buttonsWrapper)) return;
+if ($btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud"), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");
+$parent.appendChild(this.$buttonsWrapper);
+}
+checkHeader = () => {
+let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
+if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");
+$target && this.injectSettingsButton($target);
+};
+watchHeader() {
+let $root = document.querySelector("#PageContent header") || document.querySelector("#root");
+if (!$root) return;
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, this.observer && this.observer.disconnect(), this.observer = new MutationObserver((mutationList) => {
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.checkHeader, 2000);
+}), this.observer.observe($root, { subtree: !0, childList: !0 }), this.checkHeader();
+}
+showRemotePlayButton() {
+this.$btnRemotePlay?.classList.remove("bx-gone");
+}
+static watchHeader() {
+HeaderSection.getInstance().watchHeader();
+}
}
class RemotePlayDialog extends NavigationDialog {
- static instance;
- static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog);
- LOG_TAG = "RemotePlayNavigationDialog";
- STATE_LABELS = {
- On: t("powered-on"),
- Off: t("powered-off"),
- ConnectedStandby: t("standby"),
- Unknown: t("unknown")
- };
- $container;
- constructor() {
- super();
- BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();
- }
- setupDialog() {
- let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p"));
- $resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => {
- let value = e.target.value;
- $settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui");
- }), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", {
- manualTrigger: !0
- });
- let $qualitySettings = CE("div", {
- class: "bx-remote-play-settings"
- }, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions));
- $fragment.appendChild($qualitySettings);
- let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles();
- for (let con of consoles) {
- let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({
- classes: ["bx-remote-play-connect-button"],
- label: t("console-connect"),
- style: 1 | 64,
- onClick: (e) => manager.play(con.serverId)
- }));
- $fragment.appendChild($child);
- }
- $fragment.appendChild(CE("div", {
- class: "bx-remote-play-buttons",
- _nearby: {
- orientation: "horizontal"
- }
- }, createButton({
- icon: BxIcon.QUESTION,
- style: 8 | 64,
- url: "https://better-xcloud.github.io/remote-play",
- label: t("help")
- }), createButton({
- style: 8 | 64,
- label: t("close"),
- onClick: (e) => this.hide()
- }))), this.$container = $fragment;
- }
- getDialog() {
- return this;
- }
- getContent() {
- return this.$container;
- }
- focusIfNeeded() {
- let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button");
- $btnConnect && $btnConnect.focus();
- }
+static instance;
+static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog);
+LOG_TAG = "RemotePlayNavigationDialog";
+STATE_LABELS = {
+On: t("powered-on"),
+Off: t("powered-off"),
+ConnectedStandby: t("standby"),
+Unknown: t("unknown")
+};
+$container;
+constructor() {
+super();
+BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();
+}
+setupDialog() {
+let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p"));
+$resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => {
+let value = e.target.value;
+$settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui");
+}), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", {
+manualTrigger: !0
+});
+let $qualitySettings = CE("div", {
+class: "bx-remote-play-settings"
+}, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions));
+$fragment.appendChild($qualitySettings);
+let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles();
+for (let con of consoles) {
+let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, this.STATE_LABELS[con.powerState])), createButton({
+classes: ["bx-remote-play-connect-button"],
+label: t("console-connect"),
+style: 1 | 64,
+onClick: (e) => manager.play(con.serverId)
+}));
+$fragment.appendChild($child);
+}
+$fragment.appendChild(CE("div", {
+class: "bx-remote-play-buttons",
+_nearby: {
+orientation: "horizontal"
+}
+}, createButton({
+icon: BxIcon.QUESTION,
+style: 8 | 64,
+url: "https://better-xcloud.github.io/remote-play",
+label: t("help")
+}), createButton({
+style: 8 | 64,
+label: t("close"),
+onClick: (e) => this.hide()
+}))), this.$container = $fragment;
+}
+getDialog() {
+return this;
+}
+getContent() {
+return this.$container;
+}
+focusIfNeeded() {
+let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button");
+$btnConnect && $btnConnect.focus();
+}
}
class RemotePlayManager {
- static instance;
- static getInstance() {
- if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager;
- else RemotePlayManager.instance = null;
- return RemotePlayManager.instance;
- }
- LOG_TAG = "RemotePlayManager";
- isInitialized = !1;
- XCLOUD_TOKEN;
- XHOME_TOKEN;
- consoles;
- regions = [];
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- initialize() {
- if (this.isInitialized) return;
- this.isInitialized = !0, this.requestXhomeToken(() => {
- this.getConsolesList(() => {
- BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
- });
- });
- }
- getXcloudToken() {
- return this.XCLOUD_TOKEN;
- }
- setXcloudToken(token) {
- this.XCLOUD_TOKEN = token;
- }
- getXhomeToken() {
- return this.XHOME_TOKEN;
- }
- getConsoles() {
- return this.consoles;
- }
- requestXhomeToken(callback) {
- if (this.XHOME_TOKEN) {
- callback();
- return;
- }
- let GSSV_TOKEN;
- try {
- GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token;
- } catch (e) {
- for (let i = 0;i < localStorage.length; i++) {
- let key = localStorage.key(i);
- if (!key.startsWith("Auth.User.")) continue;
- let json = JSON.parse(localStorage.getItem(key));
- for (let token of json.tokens) {
- if (!token.relyingParty.includes("gssv.xboxlive.com")) continue;
- GSSV_TOKEN = token.tokenData.token;
- break;
- }
- break;
- }
- }
- let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
- method: "POST",
- body: JSON.stringify({
- offeringId: "xhome",
- token: GSSV_TOKEN
- }),
- headers: {
- "Content-Type": "application/json; charset=utf-8"
- }
- });
- fetch(request).then((resp) => resp.json()).then((json) => {
- this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback();
- });
- }
- async getConsolesList(callback) {
- if (this.consoles) {
- callback();
- return;
- }
- let options = {
- method: "GET",
- headers: {
- Authorization: `Bearer ${this.XHOME_TOKEN}`
- }
- };
- for (let region of this.regions)
- try {
- let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();
- if (json.results.length === 0) continue;
- this.consoles = json.results, STATES.remotePlay.server = region.baseUri;
- break;
- } catch (e) {}
- if (!STATES.remotePlay.server) this.consoles = [];
- callback();
- }
- play(serverId, resolution) {
- if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui");
- STATES.remotePlay.config = {
- serverId
- }, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play");
- }
- togglePopup(force = null) {
- if (!this.isReady()) {
- Toast.show(t("getting-consoles-list"));
- return;
- }
- if (this.consoles.length === 0) {
- Toast.show(t("no-consoles-found"), "", { instant: !0 });
- return;
- }
- RemotePlayDialog.getInstance().show();
- }
- static detect() {
- if (!getGlobalPref("xhome.enabled")) return;
- if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");
- else window.BX_REMOTE_PLAY_CONFIG = null;
- }
- isReady() {
- return this.consoles !== null;
- }
+static instance;
+static getInstance() {
+if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager;
+else RemotePlayManager.instance = null;
+return RemotePlayManager.instance;
+}
+LOG_TAG = "RemotePlayManager";
+isInitialized = !1;
+XCLOUD_TOKEN;
+XHOME_TOKEN;
+consoles;
+regions = [];
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+initialize() {
+if (this.isInitialized) return;
+this.isInitialized = !0, this.requestXhomeToken(() => {
+this.getConsolesList(() => {
+BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
+});
+});
+}
+getXcloudToken() {
+return this.XCLOUD_TOKEN;
+}
+setXcloudToken(token) {
+this.XCLOUD_TOKEN = token;
+}
+getXhomeToken() {
+return this.XHOME_TOKEN;
+}
+getConsoles() {
+return this.consoles;
+}
+requestXhomeToken(callback) {
+if (this.XHOME_TOKEN) {
+callback();
+return;
+}
+let GSSV_TOKEN;
+try {
+GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token;
+} catch (e) {
+for (let i = 0;i < localStorage.length; i++) {
+let key = localStorage.key(i);
+if (!key.startsWith("Auth.User.")) continue;
+let json = JSON.parse(localStorage.getItem(key));
+for (let token of json.tokens) {
+if (!token.relyingParty.includes("gssv.xboxlive.com")) continue;
+GSSV_TOKEN = token.tokenData.token;
+break;
+}
+break;
+}
+}
+let request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
+method: "POST",
+body: JSON.stringify({
+offeringId: "xhome",
+token: GSSV_TOKEN
+}),
+headers: {
+"Content-Type": "application/json; charset=utf-8"
+}
+});
+fetch(request).then((resp) => resp.json()).then((json) => {
+this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback();
+});
+}
+async getConsolesList(callback) {
+if (this.consoles) {
+callback();
+return;
+}
+let options = {
+method: "GET",
+headers: {
+Authorization: `Bearer ${this.XHOME_TOKEN}`
+}
+};
+for (let region of this.regions)
+try {
+let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();
+if (json.results.length === 0) continue;
+this.consoles = json.results, STATES.remotePlay.server = region.baseUri;
+break;
+} catch (e) {}
+if (!STATES.remotePlay.server) this.consoles = [];
+callback();
+}
+play(serverId, resolution) {
+if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui");
+STATES.remotePlay.config = {
+serverId
+}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play");
+}
+togglePopup(force = null) {
+if (!this.isReady()) {
+Toast.show(t("getting-consoles-list"));
+return;
+}
+if (this.consoles.length === 0) {
+Toast.show(t("no-consoles-found"), "", { instant: !0 });
+return;
+}
+RemotePlayDialog.getInstance().show();
+}
+static detect() {
+if (!getGlobalPref("xhome.enabled")) return;
+if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");
+else window.BX_REMOTE_PLAY_CONFIG = null;
+}
+isReady() {
+return this.consoles !== null;
+}
}
class XhomeInterceptor {
- static consoleAddrs = {};
- static async handleLogin(request) {
- try {
- let obj = await request.clone().json();
- obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
- method: "POST",
- body: JSON.stringify(obj),
- headers: {
- "Content-Type": "application/json"
- }
- });
- } catch (e) {
- alert(e), console.log(e);
- }
- return NATIVE_FETCH(request);
- }
- static async handleConfiguration(request) {
- BxEventBus.Stream.emit("state.starting", {});
- let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [
- ["ipAddress", "port"],
- ["ipV4Address", "ipV4Port"],
- ["ipV6Address", "ipV6Port"]
- ];
- XhomeInterceptor.consoleAddrs = {};
- for (let pair of pairs) {
- let [keyAddr, keyPort] = pair;
- if (keyAddr && keyPort && serverDetails[keyAddr]) {
- let port = serverDetails[keyPort], ports = new Set;
- port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);
- }
- }
- return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
- }
- static async handleInputConfigs(request, opts) {
- let response = await NATIVE_FETCH(request);
- if (getGlobalPref("touchController.mode") !== "all") return response;
- let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];
- TouchController.setXboxTitleId(xboxTitleId);
- let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;
- if (!hasTouchSupport) {
- let supportedInputTypes = inputConfigs.supportedInputTypes;
- hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");
- }
- if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
- data: null
- });
- else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);
- return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
- }
- static async handleTitles(request) {
- let clone = request.clone(), headers = {};
- for (let pair of clone.headers.entries())
- headers[pair[0]] = pair[1];
- headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;
- let index = request.url.indexOf(".xboxlive.com");
- return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {
- method: clone.method,
- body: await clone.text(),
- headers
- }), NATIVE_FETCH(request);
- }
- static async handlePlay(request) {
- BxEventBus.Stream.emit("state.loading", {});
- let body = await request.clone().json(), newRequest = new Request(request, {
- body: JSON.stringify(body)
- });
- return NATIVE_FETCH(newRequest);
- }
- static async handle(request) {
- TouchController.disable();
- let clone = request.clone(), headers = {};
- for (let pair of clone.headers.entries())
- headers[pair[0]] = pair[1];
- headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;
- let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));
- headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));
- let opts = {
- method: clone.method,
- headers
- };
- if (clone.method === "POST") opts.body = await clone.text();
- let url = request.url;
- if (!url.includes("/servers/home")) {
- let parsed = new URL(url);
- url = STATES.remotePlay.server + parsed.pathname;
- }
- if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request);
- else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request);
- else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts);
- else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request);
- else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request);
- else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
- return await NATIVE_FETCH(request);
- }
+static consoleAddrs = {};
+static async handleLogin(request) {
+try {
+let obj = await request.clone().json();
+obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
+method: "POST",
+body: JSON.stringify(obj),
+headers: {
+"Content-Type": "application/json"
+}
+});
+} catch (e) {
+alert(e), console.log(e);
+}
+return NATIVE_FETCH(request);
+}
+static async handleConfiguration(request) {
+BxEventBus.Stream.emit("state.starting", {});
+let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [
+["ipAddress", "port"],
+["ipV4Address", "ipV4Port"],
+["ipV6Address", "ipV6Port"]
+];
+XhomeInterceptor.consoleAddrs = {};
+for (let pair of pairs) {
+let [keyAddr, keyPort] = pair;
+if (keyAddr && keyPort && serverDetails[keyAddr]) {
+let port = serverDetails[keyPort], ports = new Set;
+port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);
+}
+}
+return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+}
+static async handleInputConfigs(request, opts) {
+let response = await NATIVE_FETCH(request);
+if (getGlobalPref("touchController.mode") !== "all") return response;
+let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];
+TouchController.setXboxTitleId(xboxTitleId);
+let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;
+if (!hasTouchSupport) {
+let supportedInputTypes = inputConfigs.supportedInputTypes;
+hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");
+}
+if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
+data: null
+});
+else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);
+return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+}
+static async handleTitles(request) {
+let clone = request.clone(), headers = {};
+for (let pair of clone.headers.entries())
+headers[pair[0]] = pair[1];
+headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;
+let index = request.url.indexOf(".xboxlive.com");
+return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {
+method: clone.method,
+body: await clone.text(),
+headers
+}), NATIVE_FETCH(request);
+}
+static async handlePlay(request) {
+BxEventBus.Stream.emit("state.loading", {});
+let body = await request.clone().json(), newRequest = new Request(request, {
+body: JSON.stringify(body)
+});
+return NATIVE_FETCH(newRequest);
+}
+static async handle(request) {
+TouchController.disable();
+let clone = request.clone(), headers = {};
+for (let pair of clone.headers.entries())
+headers[pair[0]] = pair[1];
+headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;
+let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));
+headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));
+let opts = {
+method: clone.method,
+headers
+};
+if (clone.method === "POST") opts.body = await clone.text();
+let url = request.url;
+if (!url.includes("/servers/home")) {
+let parsed = new URL(url);
+url = STATES.remotePlay.server + parsed.pathname;
+}
+if (request = new Request(url, opts), url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request);
+else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.handlePlay(request);
+else if (url.includes("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts);
+else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request);
+else if (url.endsWith("/titles")) return XhomeInterceptor.handleTitles(request);
+else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
+return await NATIVE_FETCH(request);
+}
}
class LoadingScreen {
- static $bgStyle;
- static $waitTimeBox;
- static waitTimeInterval = null;
- static orgWebTitle;
- static secondsToString(seconds) {
- let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");
- return mDisplay + sDisplay;
- }
- static setup() {
- let titleInfo = STATES.currentStream.titleInfo;
- if (!titleInfo) return;
- if (!LoadingScreen.$bgStyle) {
- let $bgStyle = CE("style");
- document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;
- }
- if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();
- }
- static hideRocket() {
- let $bgStyle = LoadingScreen.$bgStyle;
- $bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";
- }
- static setBackground(imageUrl) {
- let $bgStyle = LoadingScreen.$bgStyle;
- imageUrl = imageUrl + "?w=1920";
- let imageQuality = getGlobalPref("ui.imageQuality");
- if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;
- $bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;
- let bg = new Image;
- bg.onload = (e) => {
- $bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';
- }, bg.src = imageUrl;
- }
- static setupWaitTime(waitTime) {
- if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();
- let secondsLeft = waitTime, $countDown, $estimated;
- LoadingScreen.orgWebTitle = document.title;
- let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
- endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
- let endDateStr = endDate.toISOString().slice(0, 19);
- endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;
- let $waitTimeBox = LoadingScreen.$waitTimeBox;
- if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;
- else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");
- $estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {
- if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
- }, 1000);
- }
- static hide() {
- if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {
- let $rocketBg = document.querySelector('#game-stream rect[width="800"]');
- $rocketBg && $rocketBg.addEventListener("transitionend", (e) => {
- LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";
- }), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';
- }
- setTimeout(LoadingScreen.reset, 2000);
- }
- static reset() {
- LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
- }
+static $bgStyle;
+static $waitTimeBox;
+static waitTimeInterval = null;
+static orgWebTitle;
+static secondsToString(seconds) {
+let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");
+return mDisplay + sDisplay;
+}
+static setup() {
+let titleInfo = STATES.currentStream.titleInfo;
+if (!titleInfo) return;
+if (!LoadingScreen.$bgStyle) {
+let $bgStyle = CE("style");
+document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;
+}
+if (LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();
+}
+static hideRocket() {
+let $bgStyle = LoadingScreen.$bgStyle;
+$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";
+}
+static setBackground(imageUrl) {
+let $bgStyle = LoadingScreen.$bgStyle;
+imageUrl = imageUrl + "?w=1920";
+let imageQuality = getGlobalPref("ui.imageQuality");
+if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;
+$bgStyle.textContent += '#game-stream{background-color:transparent !important;background-position:center center !important;background-repeat:no-repeat !important;background-size:cover !important}#game-stream rect[width="800"]{transition:opacity .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;
+let bg = new Image;
+bg.onload = (e) => {
+$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';
+}, bg.src = imageUrl;
+}
+static setupWaitTime(waitTime) {
+if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();
+let secondsLeft = waitTime, $countDown, $estimated;
+LoadingScreen.orgWebTitle = document.title;
+let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
+endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
+let endDateStr = endDate.toISOString().slice(0, 19);
+endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;
+let $waitTimeBox = LoadingScreen.$waitTimeBox;
+if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;
+else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");
+$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {
+if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
+}, 1000);
+}
+static hide() {
+if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {
+let $rocketBg = document.querySelector('#game-stream rect[width="800"]');
+$rocketBg && $rocketBg.addEventListener("transitionend", (e) => {
+LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";
+}), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';
+}
+setTimeout(LoadingScreen.reset, 2000);
+}
+static reset() {
+LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;
+}
}
class GuideMenu {
- static instance;
- static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);
- $renderedButtons;
- closeGuideMenu() {
- if (window.BX_EXPOSED.dialogRoutes) {
- window.BX_EXPOSED.dialogRoutes.closeAll();
- return;
- }
- let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");
- $btnClose && $btnClose.click();
- }
- renderButtons() {
- if (this.$renderedButtons) return this.$renderedButtons;
- let buttons = {
- scriptSettings: createButton({
- label: t("better-xcloud"),
- icon: BxIcon.BETTER_XCLOUD,
- style: 128 | 64 | 1,
- onClick: () => {
- BxEventBus.Script.once("dialog.dismissed", () => {
- setTimeout(() => SettingsDialog.getInstance().show(), 50);
- }), this.closeGuideMenu();
- }
- }),
- closeApp: AppInterface && createButton({
- icon: BxIcon.POWER,
- label: t("close-app"),
- title: t("close-app"),
- style: 128 | 64 | 4,
- onClick: (e) => {
- AppInterface.closeApp();
- },
- attributes: {
- "data-state": "normal"
- }
- }),
- reloadPage: createButton({
- icon: BxIcon.REFRESH,
- label: t("reload-page"),
- title: t("reload-page"),
- style: 128 | 64,
- onClick: () => {
- if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
- else window.location.reload();
- }
- }),
- backToHome: createButton({
- icon: BxIcon.HOME,
- label: t("back-to-home"),
- title: t("back-to-home"),
- style: 128 | 64,
- onClick: () => {
- this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
- },
- attributes: {
- "data-state": "playing"
- }
- })
- }, buttonsLayout = [
- buttons.scriptSettings,
- [
- buttons.backToHome,
- buttons.reloadPage,
- buttons.closeApp
- ]
- ], $div = CE("div", {
- class: "bx-guide-home-buttons"
- });
- if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";
- for (let $button of buttonsLayout) {
- if (!$button) continue;
- if ($button instanceof HTMLElement) $div.appendChild($button);
- else if (Array.isArray($button)) {
- let $wrapper = CE("div", {});
- for (let $child of $button)
- $child && $wrapper.appendChild($child);
- $div.appendChild($wrapper);
- }
- }
- return this.$renderedButtons = $div, $div;
- }
- injectHome($root, isPlaying = !1) {
- {
- let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]");
- if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress);
- }
- let $target = null;
- if (isPlaying) {
- $target = $root.querySelector("a[class*=QuitGameButton]");
- let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");
- $btnXcloudHome && ($btnXcloudHome.style.display = "none");
- } else {
- let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");
- if ($dividers) $target = $dividers[$dividers.length - 1];
- }
- if (!$target) return !1;
- let $buttons = this.renderButtons();
- $buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);
- }
- onShown = async (e) => {
- if (e.where === "home") {
- let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");
- $root && this.injectHome($root, STATES.isPlaying);
- }
- };
- addEventListeners() {
- window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
- }
- observe($addedElm) {
- let className = $addedElm.className;
- if (!className) className = $addedElm.firstElementChild?.className ?? "";
- if (!className || className.startsWith("bx-")) return;
- if (className.includes("AchievementsButton-module__progressBarContainer")) {
- TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
- return;
- }
- if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
- {
- let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]");
- if ($achievDetailPage) {
- TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage);
- return;
- }
- }
- let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
- if ($selectedTab) {
- let $elm = $selectedTab, index;
- for (index = 0;$elm = $elm?.previousElementSibling; index++)
- ;
- if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });
- }
- }
+static instance;
+static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);
+$renderedButtons;
+closeGuideMenu() {
+if (window.BX_EXPOSED.dialogRoutes) {
+window.BX_EXPOSED.dialogRoutes.closeAll();
+return;
+}
+let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");
+$btnClose && $btnClose.click();
+}
+renderButtons() {
+if (this.$renderedButtons) return this.$renderedButtons;
+let buttons = {
+scriptSettings: createButton({
+label: t("better-xcloud"),
+icon: BxIcon.BETTER_XCLOUD,
+style: 128 | 64 | 1,
+onClick: () => {
+BxEventBus.Script.once("dialog.dismissed", () => {
+setTimeout(() => SettingsDialog.getInstance().show(), 50);
+}), this.closeGuideMenu();
+}
+}),
+closeApp: AppInterface && createButton({
+icon: BxIcon.POWER,
+label: t("close-app"),
+title: t("close-app"),
+style: 128 | 64 | 4,
+onClick: (e) => {
+AppInterface.closeApp();
+},
+attributes: {
+"data-state": "normal"
+}
+}),
+reloadPage: createButton({
+icon: BxIcon.REFRESH,
+label: t("reload-page"),
+title: t("reload-page"),
+style: 128 | 64,
+onClick: () => {
+if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();
+else window.location.reload();
+}
+}),
+backToHome: createButton({
+icon: BxIcon.HOME,
+label: t("back-to-home"),
+title: t("back-to-home"),
+style: 128 | 64,
+onClick: () => {
+this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
+},
+attributes: {
+"data-state": "playing"
+}
+})
+}, buttonsLayout = [
+buttons.scriptSettings,
+[
+buttons.backToHome,
+buttons.reloadPage,
+buttons.closeApp
+]
+], $div = CE("div", {
+class: "bx-guide-home-buttons"
+});
+if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";
+for (let $button of buttonsLayout) {
+if (!$button) continue;
+if ($button instanceof HTMLElement) $div.appendChild($button);
+else if (Array.isArray($button)) {
+let $wrapper = CE("div", {});
+for (let $child of $button)
+$child && $wrapper.appendChild($child);
+$div.appendChild($wrapper);
+}
+}
+return this.$renderedButtons = $div, $div;
+}
+injectHome($root, isPlaying = !1) {
+{
+let $achievementsProgress = $root.querySelector("button[class*=AchievementsButton-module__progressBarContainer]");
+if ($achievementsProgress) TrueAchievements.getInstance().injectAchievementsProgress($achievementsProgress);
+}
+let $target = null;
+if (isPlaying) {
+$target = $root.querySelector("a[class*=QuitGameButton]");
+let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");
+$btnXcloudHome && ($btnXcloudHome.style.display = "none");
+} else {
+let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");
+if ($dividers) $target = $dividers[$dividers.length - 1];
+}
+if (!$target) return !1;
+let $buttons = this.renderButtons();
+$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);
+}
+onShown = async (e) => {
+if (e.where === "home") {
+let $root = document.querySelector("#gamepass-dialog-root div[role=dialog] div[role=tabpanel] div[class*=HomeLandingPage]");
+$root && this.injectHome($root, STATES.isPlaying);
+}
+};
+addEventListeners() {
+window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, this.onShown);
+}
+observe($addedElm) {
+let className = $addedElm.className;
+if (!className) className = $addedElm.firstElementChild?.className ?? "";
+if (!className || className.startsWith("bx-")) return;
+if (className.includes("AchievementsButton-module__progressBarContainer")) {
+TrueAchievements.getInstance().injectAchievementsProgress($addedElm);
+return;
+}
+if (!className.startsWith("NavigationAnimation") && !className.startsWith("DialogRoutes") && !className.startsWith("Dialog-module__container")) return;
+{
+let $achievDetailPage = $addedElm.querySelector("div[class*=AchievementDetailPage]");
+if ($achievDetailPage) {
+TrueAchievements.getInstance().injectAchievementDetailPage($achievDetailPage);
+return;
+}
+}
+let $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
+if ($selectedTab) {
+let $elm = $selectedTab, index;
+for (index = 0;$elm = $elm?.previousElementSibling; index++)
+;
+if (index === 0) BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: "home" });
+}
+}
}
class StreamBadges {
- static instance;
- static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
- LOG_TAG = "StreamBadges";
- serverInfo = {};
- badges = {
- playtime: {
- name: t("playtime"),
- icon: BxIcon.PLAYTIME,
- color: "#ff004d"
- },
- battery: {
- name: t("battery"),
- icon: BxIcon.BATTERY,
- color: "#00b543"
- },
- download: {
- name: t("download"),
- icon: BxIcon.DOWNLOAD,
- color: "#29adff"
- },
- upload: {
- name: t("upload"),
- icon: BxIcon.UPLOAD,
- color: "#ff77a8"
- },
- server: {
- name: t("server"),
- icon: BxIcon.SERVER,
- color: "#ff6c24"
- },
- video: {
- name: t("video"),
- icon: BxIcon.DISPLAY,
- color: "#742f29"
- },
- audio: {
- name: t("audio"),
- icon: BxIcon.AUDIO,
- color: "#5f574f"
- }
- };
- $container;
- intervalId;
- REFRESH_INTERVAL = 3000;
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- setRegion(region) {
- this.serverInfo.server = {
- region
- };
- }
- renderBadge(name, value) {
- let badgeInfo = this.badges[name], $badge;
- if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;
- if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");
- return this.badges[name].$element = $badge, $badge;
- }
- updateBadges = async (forceUpdate = !1) => {
- if (!this.$container || !forceUpdate && !this.$container.isConnected) {
- this.stop();
- return;
- }
- let statsCollector = StreamStatsCollector.getInstance();
- await statsCollector.collect();
- let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {
- download: dl.toString(),
- upload: ul.toString(),
- playtime: play.toString(),
- battery: batt.toString()
- }, name;
- for (name in badges) {
- let value = badges[name];
- if (value === null) continue;
- let $elm = this.badges[name].$element;
- if (!$elm) continue;
- if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");
- else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");
- }
- };
- async start() {
- await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);
- }
- stop() {
- this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
- }
- destroy() {
- this.serverInfo = {}, delete this.$container;
- }
- async render() {
- if (this.$container) return this.start(), this.$container;
- await this.getServerStats();
- let batteryLevel = "";
- if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";
- let BADGES = [
- ["playtime", "1m"],
- ["battery", batteryLevel],
- ["download", humanFileSize(0)],
- ["upload", humanFileSize(0)],
- this.badges.server.$element ?? ["server", "?"],
- this.serverInfo.video ? this.badges.video.$element : ["video", "?"],
- this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]
- ], $container = CE("div", { class: "bx-badges" });
- for (let item2 of BADGES) {
- if (!item2) continue;
- let $badge;
- if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2);
- else $badge = item2;
- $container.appendChild($badge);
- }
- return this.$container = $container, await this.start(), $container;
- }
- async getServerStats() {
- let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;
- if (stats.forEach((stat) => {
- if (stat.type === "codec") {
- let mimeType = stat.mimeType.split("/")[0];
- if (mimeType === "video") allVideoCodecs[stat.id] = stat;
- else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;
- } else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {
- if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;
- else if (stat.kind === "audio") audioCodecId = stat.codecId;
- } else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;
- else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;
- else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;
- }), videoCodecId) {
- let videoStat = allVideoCodecs[videoCodecId], video = {
- width: videoWidth,
- height: videoHeight,
- codec: videoStat.mimeType.substring(6)
- };
- if (video.codec === "H264") {
- let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
- match && (video.profile = match[1]);
- }
- let text = videoHeight + "p";
- if (text && (text += "/"), text += video.codec, video.profile) {
- let profile = video.profile, quality = profile;
- if (profile.startsWith("4d")) quality = t("visual-quality-high");
- else if (profile.startsWith("42e")) quality = t("visual-quality-normal");
- else if (profile.startsWith("420")) quality = t("visual-quality-low");
- text += ` (${quality})`;
- }
- this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;
- }
- if (audioCodecId) {
- let audioStat = allAudioCodecs[audioCodecId], audio = {
- codec: audioStat.mimeType.substring(6),
- bitrate: audioStat.clockRate
- }, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;
- this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;
- }
- if (candidatePairId) {
- BxLogger.info("candidate", candidatePairId, allCandidatePairs);
- let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;
- if (server && server.region) text += server.region;
- text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);
- }
- }
- static setupEvents() {}
+static instance;
+static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
+LOG_TAG = "StreamBadges";
+serverInfo = {};
+badges = {
+playtime: {
+name: t("playtime"),
+icon: BxIcon.PLAYTIME,
+color: "#ff004d"
+},
+battery: {
+name: t("battery"),
+icon: BxIcon.BATTERY,
+color: "#00b543"
+},
+download: {
+name: t("download"),
+icon: BxIcon.DOWNLOAD,
+color: "#29adff"
+},
+upload: {
+name: t("upload"),
+icon: BxIcon.UPLOAD,
+color: "#ff77a8"
+},
+server: {
+name: t("server"),
+icon: BxIcon.SERVER,
+color: "#ff6c24"
+},
+video: {
+name: t("video"),
+icon: BxIcon.DISPLAY,
+color: "#742f29"
+},
+audio: {
+name: t("audio"),
+icon: BxIcon.AUDIO,
+color: "#5f574f"
+}
+};
+$container;
+intervalId;
+REFRESH_INTERVAL = 3000;
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+setRegion(region) {
+this.serverInfo.server = {
+region
+};
+}
+renderBadge(name, value) {
+let badgeInfo = this.badges[name], $badge;
+if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;
+if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");
+return this.badges[name].$element = $badge, $badge;
+}
+updateBadges = async (forceUpdate = !1) => {
+if (!this.$container || !forceUpdate && !this.$container.isConnected) {
+this.stop();
+return;
+}
+let statsCollector = StreamStatsCollector.getInstance();
+await statsCollector.collect();
+let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {
+download: dl.toString(),
+upload: ul.toString(),
+playtime: play.toString(),
+battery: batt.toString()
+}, name;
+for (name in badges) {
+let value = badges[name];
+if (value === null) continue;
+let $elm = this.badges[name].$element;
+if (!$elm) continue;
+if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");
+else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");
+}
+};
+async start() {
+await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);
+}
+stop() {
+this.intervalId && clearInterval(this.intervalId), this.intervalId = null;
+}
+destroy() {
+this.serverInfo = {}, delete this.$container;
+}
+async render() {
+if (this.$container) return this.start(), this.$container;
+await this.getServerStats();
+let batteryLevel = "";
+if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";
+let BADGES = [
+["playtime", "1m"],
+["battery", batteryLevel],
+["download", humanFileSize(0)],
+["upload", humanFileSize(0)],
+this.badges.server.$element ?? ["server", "?"],
+this.serverInfo.video ? this.badges.video.$element : ["video", "?"],
+this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]
+], $container = CE("div", { class: "bx-badges" });
+for (let item2 of BADGES) {
+if (!item2) continue;
+let $badge;
+if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2);
+else $badge = item2;
+$container.appendChild($badge);
+}
+return this.$container = $container, await this.start(), $container;
+}
+async getServerStats() {
+let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;
+if (stats.forEach((stat) => {
+if (stat.type === "codec") {
+let mimeType = stat.mimeType.split("/")[0];
+if (mimeType === "video") allVideoCodecs[stat.id] = stat;
+else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;
+} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {
+if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;
+else if (stat.kind === "audio") audioCodecId = stat.codecId;
+} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;
+else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;
+else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;
+}), videoCodecId) {
+let videoStat = allVideoCodecs[videoCodecId], video = {
+width: videoWidth,
+height: videoHeight,
+codec: videoStat.mimeType.substring(6)
+};
+if (video.codec === "H264") {
+let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
+match && (video.profile = match[1]);
+}
+let text = videoHeight + "p";
+if (text && (text += "/"), text += video.codec, video.profile) {
+let profile = video.profile, quality = profile;
+if (profile.startsWith("4d")) quality = t("visual-quality-high");
+else if (profile.startsWith("42e")) quality = t("visual-quality-normal");
+else if (profile.startsWith("420")) quality = t("visual-quality-low");
+text += ` (${quality})`;
+}
+this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;
+}
+if (audioCodecId) {
+let audioStat = allAudioCodecs[audioCodecId], audio = {
+codec: audioStat.mimeType.substring(6),
+bitrate: audioStat.clockRate
+}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;
+this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;
+}
+if (candidatePairId) {
+BxLogger.info("candidate", candidatePairId, allCandidatePairs);
+let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;
+if (server && server.region) text += server.region;
+text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);
+}
+}
+static setupEvents() {}
}
class XcloudInterceptor {
- static SERVER_EXTRA_INFO = {
- EastUS: ["🇺🇸", "america-north"],
- EastUS2: ["🇺🇸", "america-north"],
- NorthCentralUs: ["🇺🇸", "america-north"],
- SouthCentralUS: ["🇺🇸", "america-north"],
- WestUS: ["🇺🇸", "america-north"],
- WestUS2: ["🇺🇸", "america-north"],
- MexicoCentral: ["🇲🇽", "america-north"],
- BrazilSouth: ["🇧🇷", "america-south"],
- JapanEast: ["🇯🇵", "asia"],
- KoreaCentral: ["🇰🇷", "asia"],
- AustraliaEast: ["🇦🇺", "australia"],
- AustraliaSouthEast: ["🇦🇺", "australia"],
- SwedenCentral: ["🇸🇪", "europe"],
- UKSouth: ["🇬🇧", "europe"],
- WestEurope: ["🇪🇺", "europe"]
- };
- static async handleLogin(request, init) {
- let bypassServer = getGlobalPref("server.bypassRestriction");
- if (bypassServer !== "off") {
- let ip = BypassServerIps[bypassServer];
- ip && request.headers.set("X-Forwarded-For", ip);
- }
- let response = await NATIVE_FETCH(request, init);
- if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;
- let obj = await response.clone().json();
- RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
- let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
- for (region of obj.offeringSettings.regions) {
- let { name: regionName, name: shortName } = region;
- if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
- let match = serverRegex.exec(region.baseUri);
- if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
- else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);
- region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
- }
- BxEventBus.Script.emit("xcloud.server.ready", {});
- let preferredRegion = getPreferredServerRegion();
- if (preferredRegion && preferredRegion in STATES.serverRegions) {
- let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
- tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;
- }
- return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
- }
- static async handlePlay(request, init) {
- BxEventBus.Stream.emit("state.loading", {});
- let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];
- for (let regionName in STATES.serverRegions) {
- let region = STATES.serverRegions[regionName];
- if (region && parsedUrl.origin === region.baseUri) {
- badgeRegion = regionName;
- break;
- }
- }
- StreamBadges.getInstance().setRegion(badgeRegion);
- let clone = request.clone(), body = await clone.json(), headers = {};
- for (let pair of clone.headers.entries())
- headers[pair[0]] = pair[1];
- if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {
- let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);
- headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;
- }
- if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
- let newRequest = new Request(request, {
- body: JSON.stringify(body),
- headers
- });
- return NATIVE_FETCH(newRequest);
- }
- static async handleWaitTime(request, init) {
- let response = await NATIVE_FETCH(request, init);
- if (getGlobalPref("loadingScreen.waitTime.show")) {
- let json = await response.clone().json();
- if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
- }
- return response;
- }
- static async handleConfiguration(request, init) {
- if (request.method !== "GET") return NATIVE_FETCH(request, init);
- if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();
- else TouchController.enable();
- let response = await NATIVE_FETCH(request, init), text = await response.clone().text();
- if (!text.length) return response;
- BxEventBus.Stream.emit("state.starting", {});
- let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
- overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
- let overrideMkb = null;
- if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;
- if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;
- if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
- enableMouseInput: overrideMkb,
- enableKeyboardInput: overrideMkb
- });
- if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;
- if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;
- return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
- }
- static async handle(request, init) {
- let url = typeof request === "string" ? request : request.url;
- if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);
- else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);
- else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
- else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);
- else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
- return NATIVE_FETCH(request, init);
- }
+static SERVER_EXTRA_INFO = {
+EastUS: ["🇺🇸", "america-north"],
+EastUS2: ["🇺🇸", "america-north"],
+NorthCentralUs: ["🇺🇸", "america-north"],
+SouthCentralUS: ["🇺🇸", "america-north"],
+WestUS: ["🇺🇸", "america-north"],
+WestUS2: ["🇺🇸", "america-north"],
+MexicoCentral: ["🇲🇽", "america-north"],
+BrazilSouth: ["🇧🇷", "america-south"],
+JapanEast: ["🇯🇵", "asia"],
+KoreaCentral: ["🇰🇷", "asia"],
+AustraliaEast: ["🇦🇺", "australia"],
+AustraliaSouthEast: ["🇦🇺", "australia"],
+SwedenCentral: ["🇸🇪", "europe"],
+UKSouth: ["🇬🇧", "europe"],
+WestEurope: ["🇪🇺", "europe"]
+};
+static async handleLogin(request, init) {
+let bypassServer = getGlobalPref("server.bypassRestriction");
+if (bypassServer !== "off") {
+let ip = BypassServerIps[bypassServer];
+ip && request.headers.set("X-Forwarded-For", ip);
+}
+let response = await NATIVE_FETCH(request, init);
+if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server.unavailable", {}), response;
+let obj = await response.clone().json();
+RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);
+let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;
+for (region of obj.offeringSettings.regions) {
+let { name: regionName, name: shortName } = region;
+if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);
+let match = serverRegex.exec(region.baseUri);
+if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];
+else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);
+region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);
+}
+BxEventBus.Script.emit("xcloud.server.ready", {});
+let preferredRegion = getPreferredServerRegion();
+if (preferredRegion && preferredRegion in STATES.serverRegions) {
+let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
+tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;
+}
+return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
+}
+static async handlePlay(request, init) {
+BxEventBus.Stream.emit("state.loading", {});
+let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];
+for (let regionName in STATES.serverRegions) {
+let region = STATES.serverRegions[regionName];
+if (region && parsedUrl.origin === region.baseUri) {
+badgeRegion = regionName;
+break;
+}
+}
+StreamBadges.getInstance().setRegion(badgeRegion);
+let clone = request.clone(), body = await clone.json(), headers = {};
+for (let pair of clone.headers.entries())
+headers[pair[0]] = pair[1];
+if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {
+let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);
+headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;
+}
+if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
+let newRequest = new Request(request, {
+body: JSON.stringify(body),
+headers
+});
+return NATIVE_FETCH(newRequest);
+}
+static async handleWaitTime(request, init) {
+let response = await NATIVE_FETCH(request, init);
+if (getGlobalPref("loadingScreen.waitTime.show")) {
+let json = await response.clone().json();
+if (json.estimatedAllocationTimeInSeconds > 0) LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
+}
+return response;
+}
+static async handleConfiguration(request, init) {
+if (request.method !== "GET") return NATIVE_FETCH(request, init);
+if (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();
+else TouchController.enable();
+let response = await NATIVE_FETCH(request, init), text = await response.clone().text();
+if (!text.length) return response;
+BxEventBus.Stream.emit("state.starting", {});
+let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
+overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
+let overrideMkb = null;
+if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;
+if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;
+if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
+enableMouseInput: overrideMkb,
+enableKeyboardInput: overrideMkb
+});
+if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;
+if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;
+return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+}
+static async handle(request, init) {
+let url = typeof request === "string" ? request : request.url;
+if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);
+else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);
+else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
+else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);
+else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
+return NATIVE_FETCH(request, init);
+}
}
function clearApplicationInsightsBuffers() {
- window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");
+window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");
}
function clearDbLogs(dbName, table) {
- let request = window.indexedDB.open(dbName);
- request.onsuccess = (e) => {
- let db = e.target.result;
- try {
- let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
- objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);
- } catch (ex) {}
- };
+let request = window.indexedDB.open(dbName);
+request.onsuccess = (e) => {
+let db = e.target.result;
+try {
+let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
+objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);
+} catch (ex) {}
+};
}
function clearAllLogs() {
- clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");
+clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");
}
function updateIceCandidates(candidates, options) {
- let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = [];
- for (let item2 of candidates) {
- if (item2.candidate == "a=end-of-candidates") continue;
- let groups = pattern.exec(item2.candidate).groups;
- lst.push(groups);
- }
- if (options.preferIpv6Server) lst.sort((a, b) => {
- let firstIp = a.ip, secondIp = b.ip;
- return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;
- });
- let newCandidates = [], foundation = 1, newCandidate = (candidate) => {
- return {
- candidate,
- messageType: "iceCandidate",
- sdpMLineIndex: "0",
- sdpMid: "0"
- };
- };
- if (lst.forEach((item2) => {
- item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation;
- }), options.consoleAddrs)
- for (let ip in options.consoleAddrs)
- for (let port of options.consoleAddrs[ip])
- newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));
- return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates;
+let pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = [];
+for (let item2 of candidates) {
+if (item2.candidate == "a=end-of-candidates") continue;
+let groups = pattern.exec(item2.candidate).groups;
+lst.push(groups);
+}
+if (options.preferIpv6Server) lst.sort((a, b) => {
+let firstIp = a.ip, secondIp = b.ip;
+return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;
+});
+let newCandidates = [], foundation = 1, newCandidate = (candidate) => {
+return {
+candidate,
+messageType: "iceCandidate",
+sdpMLineIndex: "0",
+sdpMid: "0"
+};
+};
+if (lst.forEach((item2) => {
+item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation;
+}), options.consoleAddrs)
+for (let ip in options.consoleAddrs)
+for (let port of options.consoleAddrs[ip])
+newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));
+return newCandidates.push(newCandidate("a=end-of-candidates")), BxLogger.info("ICE Candidates", newCandidates), newCandidates;
}
async function patchIceCandidates(request, consoleAddrs) {
- let response = await NATIVE_FETCH(request), text = await response.clone().text();
- if (!text.length) return response;
- let options = {
- preferIpv6Server: getGlobalPref("server.ipv6.prefer"),
- consoleAddrs
- }, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse);
- return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
+let response = await NATIVE_FETCH(request), text = await response.clone().text();
+if (!text.length) return response;
+let options = {
+preferIpv6Server: getGlobalPref("server.ipv6.prefer"),
+consoleAddrs
+}, obj = JSON.parse(text), exchangeResponse = JSON.parse(obj.exchangeResponse);
+return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
function interceptHttpRequests() {
- let BLOCKED_URLS = [];
- if (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net");
- let blockFeatures2 = getGlobalPref("block.features");
- if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox");
- if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations");
- if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/");
- let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;
- xhrPrototype.open = function(method, url) {
- return this._url = url, nativeXhrOpen.apply(this, arguments);
- }, xhrPrototype.send = function(...arg) {
- for (let url of BLOCKED_URLS)
- if (this._url.startsWith(url)) {
- if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000);
- return BxLogger.warning("Blocked URL", url), !1;
- }
- return nativeXhrSend.apply(this, arguments);
- };
- let gamepassAllGames = [], IGNORED_DOMAINS = [
- "accounts.xboxlive.com",
- "chat.xboxlive.com",
- "notificationinbox.xboxlive.com",
- "peoplehub.xboxlive.com",
- "peoplehub-public.xboxlive.com",
- "rta.xboxlive.com",
- "userpresence.xboxlive.com",
- "xblmessaging.xboxlive.com",
- "consent.config.office.com",
- "arc.msn.com",
- "browser.events.data.microsoft.com",
- "dc.services.visualstudio.com",
- "2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
- ];
- window.BX_FETCH = window.fetch = async (request, init) => {
- let url = typeof request === "string" ? request : request.url;
- for (let blocked of BLOCKED_URLS)
- if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', {
- status: 200,
- statusText: "200 OK"
- });
- try {
- let domain = new URL(url).hostname;
- if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);
- } catch (e) {
- return NATIVE_FETCH(request, init);
- }
- if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {
- let response = await NATIVE_FETCH(request, init), json = await response.json();
- if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)
- json.exp.treatments[key] = FeatureGates[key];
- return response.json = () => Promise.resolve(json), response;
- } catch (e) {
- return console.log(e), NATIVE_FETCH(request, init);
- }
- if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {
- let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
- if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++)
- gamepassAllGames.push(obj[i].id);
- else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try {
- let customList = TouchController.getCustomList();
- customList = customList.filter((id) => gamepassAllGames.includes(id));
- let newCustomList = customList.map((item2) => ({ id: item2 }));
- obj.push(...newCustomList);
- } catch (e) {
- console.log(e);
- }
- return response.json = () => Promise.resolve(obj), response;
- }
- if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) {
- let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
- try {
- let newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 }));
- obj.push(...newCustomList);
- } catch (e) {
- console.log(e);
- }
- return response.json = () => Promise.resolve(obj), response;
- }
- let requestType;
- if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome";
- else requestType = "xcloud";
- if (requestType === "xhome") return XhomeInterceptor.handle(request);
- return XcloudInterceptor.handle(request, init);
- };
+let BLOCKED_URLS = [];
+if (getGlobalPref("block.tracking")) clearAllLogs(), BLOCKED_URLS.push("https://arc.msn.com", "https://browser.events.data.microsoft.com", "https://dc.services.visualstudio.com", "https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io", "https://mscom.demdex.net");
+let blockFeatures2 = getGlobalPref("block.features");
+if (blockFeatures2.includes("chat")) BLOCKED_URLS.push("https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox");
+if (blockFeatures2.includes("friends")) BLOCKED_URLS.push("https://peoplehub.xboxlive.com/users/me/people/social", "https://peoplehub.xboxlive.com/users/me/people/recommendations");
+if (blockAllNotifications()) BLOCKED_URLS.push("https://notificationinbox.xboxlive.com/");
+let xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;
+xhrPrototype.open = function(method, url) {
+return this._url = url, nativeXhrOpen.apply(this, arguments);
+}, xhrPrototype.send = function(...arg) {
+for (let url of BLOCKED_URLS)
+if (this._url.startsWith(url)) {
+if (url === "https://dc.services.visualstudio.com") window.setTimeout(clearAllLogs, 1000);
+return BxLogger.warning("Blocked URL", url), !1;
+}
+return nativeXhrSend.apply(this, arguments);
+};
+let gamepassAllGames = [], IGNORED_DOMAINS = [
+"accounts.xboxlive.com",
+"chat.xboxlive.com",
+"notificationinbox.xboxlive.com",
+"peoplehub.xboxlive.com",
+"peoplehub-public.xboxlive.com",
+"rta.xboxlive.com",
+"userpresence.xboxlive.com",
+"xblmessaging.xboxlive.com",
+"consent.config.office.com",
+"arc.msn.com",
+"browser.events.data.microsoft.com",
+"dc.services.visualstudio.com",
+"2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
+];
+window.BX_FETCH = window.fetch = async (request, init) => {
+let url = typeof request === "string" ? request : request.url;
+for (let blocked of BLOCKED_URLS)
+if (url.startsWith(blocked)) return BxLogger.warning("Blocked URL", url), new Response('{"acc":1,"webResult":{}}', {
+status: 200,
+statusText: "200 OK"
+});
+try {
+let domain = new URL(url).hostname;
+if (IGNORED_DOMAINS.includes(domain)) return NATIVE_FETCH(request, init);
+} catch (e) {
+return NATIVE_FETCH(request, init);
+}
+if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation")) try {
+let response = await NATIVE_FETCH(request, init), json = await response.json();
+if (json && json.exp && json.exp.treatments) for (let key in FeatureGates)
+json.exp.treatments[key] = FeatureGates[key];
+return response.json = () => Promise.resolve(json), response;
+} catch (e) {
+return console.log(e), NATIVE_FETCH(request, init);
+}
+if (STATES.userAgent.capabilities.touch && url.includes("catalog.gamepass.com/sigls/")) {
+let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
+if (url.includes("29a81209-df6f-41fd-a528-2ae6b91f719c") || url.includes("ce573635-7c18-4d0c-9d68-90b932393470")) for (let i = 1;i < obj.length; i++)
+gamepassAllGames.push(obj[i].id);
+else if (url.includes("9c86f07a-f3e8-45ad-82a0-a1f759597059")) try {
+let customList = TouchController.getCustomList();
+customList = customList.filter((id) => gamepassAllGames.includes(id));
+let newCustomList = customList.map((item2) => ({ id: item2 }));
+obj.push(...newCustomList);
+} catch (e) {
+console.log(e);
+}
+return response.json = () => Promise.resolve(obj), response;
+}
+if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes("8fa264dd-124f-4af3-97e8-596fcdf4b486")) {
+let response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
+try {
+let newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 }));
+obj.push(...newCustomList);
+} catch (e) {
+console.log(e);
+}
+return response.json = () => Promise.resolve(obj), response;
+}
+let requestType;
+if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs")) requestType = "xhome";
+else requestType = "xcloud";
+if (requestType === "xhome") return XhomeInterceptor.handle(request);
+return XcloudInterceptor.handle(request, init);
+};
}
function generateMsDeviceInfo(osName) {
- return {
- appInfo: {
- env: {
- clientAppId: window.location.host,
- clientAppType: "browser",
- clientAppVersion: "26.1.97",
- clientSdkVersion: "10.3.7",
- httpEnvironment: "prod",
- sdkInstallId: ""
- }
- },
- dev: {
- os: { name: osName, ver: "22631.2715", platform: "desktop" },
- hw: { make: "Microsoft", model: "unknown", sdktype: "web" },
- browser: { browserName: "chrome", browserVersion: "130.0" },
- displayInfo: {
- dimensions: { widthInPixels: 1920, heightInPixels: 1080 },
- pixelDensity: { dpiX: 1, dpiY: 1 }
- }
- }
- };
+return {
+appInfo: {
+env: {
+clientAppId: window.location.host,
+clientAppType: "browser",
+clientAppVersion: "26.1.97",
+clientSdkVersion: "10.3.7",
+httpEnvironment: "prod",
+sdkInstallId: ""
+}
+},
+dev: {
+os: { name: osName, ver: "22631.2715", platform: "desktop" },
+hw: { make: "Microsoft", model: "unknown", sdktype: "web" },
+browser: { browserName: "chrome", browserVersion: "130.0" },
+displayInfo: {
+dimensions: { widthInPixels: 1920, heightInPixels: 1080 },
+pixelDensity: { dpiX: 1, dpiY: 1 }
+}
+}
+};
}
function getOsNameFromResolution(resolution) {
- let osName;
- switch (resolution) {
- case "1080p-hq":
- osName = "tizen";
- break;
- case "1080p":
- osName = "windows";
- break;
- default:
- osName = "android";
- break;
- }
- return osName;
+let osName;
+switch (resolution) {
+case "1080p-hq":
+osName = "tizen";
+break;
+case "1080p":
+osName = "windows";
+break;
+default:
+osName = "android";
+break;
+}
+return osName;
}
function addCss() {
- let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = [];
- if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]");
- if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]");
- if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]");
- if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');
- if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');
- if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]");
- if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }";
- if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}";
- if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}";
- if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}";
- else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}";
- if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}";
- let $style = CE("style", !1, css);
- document.documentElement.appendChild($style);
+let css = ':root{--bx-title-font:Bahnschrift,Arial,Helvetica,sans-serif;--bx-title-font-semibold:Bahnschrift Semibold,Arial,Helvetica,sans-serif;--bx-normal-font:"Segoe UI",Arial,Helvetica,sans-serif;--bx-monospaced-font:Consolas,"Courier New",Courier,monospace;--bx-promptfont-font:promptfont;--bx-button-height:40px;--bx-default-button-color:#2d3036;--bx-default-button-rgb:45,48,54;--bx-default-button-hover-color:#515863;--bx-default-button-hover-rgb:81,88,99;--bx-default-button-active-color:#222428;--bx-default-button-active-rgb:34,36,40;--bx-default-button-disabled-color:#8e8e8e;--bx-default-button-disabled-rgb:142,142,142;--bx-primary-button-color:#008746;--bx-primary-button-rgb:0,135,70;--bx-primary-button-hover-color:#04b358;--bx-primary-button-hover-rgb:4,179,88;--bx-primary-button-active-color:#044e2a;--bx-primary-button-active-rgb:4,78,42;--bx-primary-button-disabled-color:#448262;--bx-primary-button-disabled-rgb:68,130,98;--bx-warning-button-color:#c16e04;--bx-warning-button-rgb:193,110,4;--bx-warning-button-hover-color:#fa9005;--bx-warning-button-hover-rgb:250,144,5;--bx-warning-button-active-color:#965603;--bx-warning-button-active-rgb:150,86,3;--bx-warning-button-disabled-color:#a2816c;--bx-warning-button-disabled-rgb:162,129,108;--bx-danger-button-color:#c10404;--bx-danger-button-rgb:193,4,4;--bx-danger-button-hover-color:#e61d1d;--bx-danger-button-hover-rgb:230,29,29;--bx-danger-button-active-color:#a26c6c;--bx-danger-button-active-rgb:162,108,108;--bx-danger-button-disabled-color:#bd8282;--bx-danger-button-disabled-rgb:189,130,130;--bx-fullscreen-text-z-index:9999;--bx-toast-z-index:6000;--bx-key-binding-dialog-z-index:5010;--bx-key-binding-dialog-overlay-z-index:5000;--bx-stats-bar-z-index:4010;--bx-navigation-dialog-z-index:3010;--bx-navigation-dialog-overlay-z-index:3000;--bx-mkb-pointer-lock-msg-z-index:2000;--bx-game-bar-z-index:1000;--bx-screenshot-animation-z-index:200;--bx-wait-time-box-z-index:100}@font-face{font-family:\'promptfont\';src:url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");unicode-range:U+2196-E011,U+27F6,U+FF31}div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]){opacity:0;pointer-events:none !important;position:absolute;top:-9999px;left:-9999px}@media screen and (max-width:640px){header a[href="/play"]{display:none}}.bx-full-width{width:100% !important}.bx-full-height{height:100% !important}.bx-auto-height{height:auto !important}.bx-no-scroll{overflow:hidden !important}.bx-hide-scroll-bar{scrollbar-width:none}.bx-hide-scroll-bar::-webkit-scrollbar{display:none}.bx-gone{display:none !important}.bx-offscreen{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}.bx-hidden{visibility:hidden !important}.bx-invisible{opacity:0}.bx-unclickable{pointer-events:none}.bx-pixel{width:1px !important;height:1px !important}.bx-no-margin{margin:0 !important}.bx-no-padding{padding:0 !important}.bx-prompt{font-family:var(--bx-promptfont-font) !important}.bx-monospaced{font-family:var(--bx-monospaced-font) !important}.bx-line-through{text-decoration:line-through !important}.bx-normal-case{text-transform:none !important}.bx-normal-link{text-transform:none !important;text-align:left !important;font-weight:400 !important;font-family:var(--bx-normal-font) !important}.bx-frosted{backdrop-filter:blur(4px) brightness(1.5)}select[multiple],select[multiple]:focus{overflow:auto;border:none}select[multiple] option,select[multiple]:focus option{padding:4px 6px}select[multiple] option:checked,select[multiple]:focus option:checked{background:#1a7bc0 linear-gradient(0deg,#1a7bc0 0%,#1a7bc0 100%)}select[multiple] option:checked::before,select[multiple]:focus option:checked::before{content:\'☑️\';font-size:12px;display:inline-block;margin-right:6px;height:100%;line-height:100%;vertical-align:middle}#headerArea,#uhfSkipToMain,.uhf-footer{display:none}div[class*=NotFocusedDialog]{position:absolute !important;top:-9999px !important;left:-9999px !important;width:0 !important;height:0 !important}#game-stream video:not([src]){visibility:hidden}.bx-game-tile-wait-time{position:absolute;top:0;left:0;z-index:1;background:rgba(0,0,0,0.5);display:flex;border-radius:4px 0 4px 0;align-items:center;padding:4px 8px}.bx-game-tile-wait-time svg{width:14px;height:16px;margin-right:2px}.bx-game-tile-wait-time span{display:inline-block;height:16px;line-height:16px;font-size:12px;font-weight:bold;margin-left:2px}.bx-game-tile-wait-time[data-duration=short]{background-color:rgba(0,133,133,0.75)}.bx-game-tile-wait-time[data-duration=medium]{background-color:rgba(213,133,0,0.75)}.bx-game-tile-wait-time[data-duration=long]{background-color:rgba(150,0,0,0.75)}.bx-fullscreen-text{position:fixed;top:0;bottom:0;left:0;right:0;background:rgba(0,0,0,0.8);z-index:var(--bx-fullscreen-text-z-index);line-height:100vh;color:#fff;text-align:center;font-weight:400;font-family:var(--bx-normal-font);font-size:1.3rem;user-select:none;-webkit-user-select:none}#root section[class*=DeviceCodePage-module__page]{margin-left:20px !important;margin-right:20px !important;margin-top:20px !important;max-width:800px !important}#root div[class*=DeviceCodePage-module__back]{display:none}.bx-blink-me{animation:bx-blinker 1s linear infinite}.bx-horizontal-shaking{animation:bx-horizontal-shaking .4s ease-in-out 2}@-moz-keyframes bx-blinker{100%{opacity:0}}@-webkit-keyframes bx-blinker{100%{opacity:0}}@-o-keyframes bx-blinker{100%{opacity:0}}@keyframes bx-blinker{100%{opacity:0}}@-moz-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-webkit-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@-o-keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}@keyframes bx-horizontal-shaking{0%{transform:translateX(0)}25%{transform:translateX(5px)}50%{transform:translateX(-5px)}75%{transform:translateX(5px)}100%{transform:translateX(0)}}.bx-button{--button-rgb:var(--bx-default-button-rgb);--button-hover-rgb:var(--bx-default-button-hover-rgb);--button-active-rgb:var(--bx-default-button-active-rgb);--button-disabled-rgb:var(--bx-default-button-disabled-rgb);background-color:rgb(var(--button-rgb));user-select:none;-webkit-user-select:none;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;border:none;font-weight:400;height:var(--bx-button-height);border-radius:4px;padding:0 8px;text-transform:uppercase;cursor:pointer;overflow:hidden}.bx-button:not([disabled]):active{background-color:rgb(var(--button-active-rgb))}.bx-button:focus{outline:none !important}.bx-button:not([disabled]):not(:active):hover,.bx-button:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button:disabled{cursor:default;background-color:rgb(var(--button-disabled-rgb));opacity:.5}.bx-button.bx-ghost{background-color:transparent}.bx-button.bx-ghost:not([disabled]):not(:active):hover,.bx-button.bx-ghost:not([disabled]):not(:active).bx-focusable:focus{background-color:rgb(var(--button-hover-rgb))}.bx-button.bx-primary{--button-rgb:var(--bx-primary-button-rgb)}.bx-button.bx-primary:not([disabled]):active{--button-active-rgb:var(--bx-primary-button-active-rgb)}.bx-button.bx-primary:not([disabled]):not(:active):hover,.bx-button.bx-primary:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-primary-button-hover-rgb)}.bx-button.bx-primary:disabled{--button-disabled-rgb:var(--bx-primary-button-disabled-rgb)}.bx-button.bx-warning{--button-rgb:var(--bx-warning-button-rgb)}.bx-button.bx-warning:not([disabled]):active{--button-active-rgb:var(--bx-warning-button-active-rgb)}.bx-button.bx-warning:not([disabled]):not(:active):hover,.bx-button.bx-warning:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-warning-button-hover-rgb)}.bx-button.bx-warning:disabled{--button-disabled-rgb:var(--bx-warning-button-disabled-rgb)}.bx-button.bx-danger{--button-rgb:var(--bx-danger-button-rgb)}.bx-button.bx-danger:not([disabled]):active{--button-active-rgb:var(--bx-danger-button-active-rgb)}.bx-button.bx-danger:not([disabled]):not(:active):hover,.bx-button.bx-danger:not([disabled]):not(:active).bx-focusable:focus{--button-hover-rgb:var(--bx-danger-button-hover-rgb)}.bx-button.bx-danger:disabled{--button-disabled-rgb:var(--bx-danger-button-disabled-rgb)}.bx-button.bx-frosted{--button-alpha:.2;background-color:rgba(var(--button-rgb), var(--button-alpha))}.bx-button.bx-frosted:not([disabled]):not(:active):hover,.bx-button.bx-frosted:not([disabled]):not(:active).bx-focusable:focus{background-color:rgba(var(--button-hover-rgb), var(--button-alpha))}.bx-button.bx-drop-shadow{box-shadow:0 0 4px rgba(0,0,0,0.502)}.bx-button.bx-tall{height:calc(var(--bx-button-height) * 1.5) !important}.bx-button.bx-circular{border-radius:var(--bx-button-height);width:var(--bx-button-height);height:var(--bx-button-height)}.bx-button svg{display:inline-block;width:16px;height:var(--bx-button-height)}.bx-button span{display:inline-block;line-height:var(--bx-button-height);vertical-align:middle;color:#fff;overflow:hidden;white-space:nowrap}.bx-button span:not(:only-child){margin-inline-start:8px}.bx-button.bx-button-multi-lines{height:auto;text-align:left;padding:10px}.bx-button.bx-button-multi-lines span{line-height:unset;display:block}.bx-button.bx-button-multi-lines span:last-of-type{text-transform:none;font-weight:normal;font-family:"Segoe Sans Variable Text";font-size:12px;margin-top:4px}.bx-focusable{position:relative;overflow:visible}.bx-focusable::after{border:2px solid transparent;border-radius:10px}.bx-focusable:focus::after{content:\'\';border-color:#fff;position:absolute;top:-6px;left:-6px;right:-6px;bottom:-6px}html[data-active-input=touch] .bx-focusable:focus::after,html[data-active-input=mouse] .bx-focusable:focus::after{border-color:transparent !important}.bx-focusable.bx-circular::after{border-radius:var(--bx-button-height)}a.bx-button{display:inline-block}a.bx-button.bx-full-width{text-align:center}button.bx-inactive{pointer-events:none;opacity:.2;background:transparent !important}.bx-header-remote-play-button{height:auto;margin-right:8px !important}.bx-header-remote-play-button svg{width:24px;height:24px}.bx-header-settings-button{line-height:30px;font-size:14px;text-transform:uppercase;position:relative}.bx-header-settings-button[data-update-available]::before{content:\'🌟\' !important;line-height:var(--bx-button-height);display:inline-block;margin-left:4px}.bx-key-binding-dialog-overlay{position:fixed;inset:0;z-index:var(--bx-key-binding-dialog-overlay-z-index);background:#000;opacity:50%}.bx-key-binding-dialog{display:flex;flex-flow:column;max-height:90vh;position:fixed;top:50%;left:50%;margin-right:-50%;transform:translate(-50%,-50%);min-width:420px;padding:16px;border-radius:8px;z-index:var(--bx-key-binding-dialog-z-index);background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-normal-font);box-shadow:0 0 6px #000;user-select:none;-webkit-user-select:none}.bx-key-binding-dialog *:focus{outline:none !important}.bx-key-binding-dialog h2{margin-bottom:12px;color:#fff;display:block;font-family:var(--bx-title-font);font-size:32px;font-weight:400;line-height:var(--bx-button-height)}.bx-key-binding-dialog > div{overflow:auto;padding:2px 0}.bx-key-binding-dialog > button{padding:8px 32px;margin:10px auto 0;border:none;border-radius:4px;display:block;background-color:#2d3036;text-align:center;color:#fff;text-transform:uppercase;font-family:var(--bx-title-font);font-weight:400;line-height:18px;font-size:14px}@media (hover:hover){.bx-key-binding-dialog > button:hover{background-color:#515863}}.bx-key-binding-dialog > button:focus{background-color:#515863}.bx-key-binding-dialog ul{margin-bottom:1rem}.bx-key-binding-dialog ul li{display:none}.bx-key-binding-dialog ul[data-flags*="[1]"] > li[data-flag="1"],.bx-key-binding-dialog ul[data-flags*="[2]"] > li[data-flag="2"],.bx-key-binding-dialog ul[data-flags*="[4]"] > li[data-flag="4"],.bx-key-binding-dialog ul[data-flags*="[8]"] > li[data-flag="8"]{display:list-item}@media screen and (max-width:450px){.bx-key-binding-dialog{min-width:100%}}.bx-navigation-dialog{position:absolute;z-index:var(--bx-navigation-dialog-z-index);font-family:var(--bx-title-font)}.bx-navigation-dialog *:focus{outline:none !important}.bx-navigation-dialog select:disabled{-webkit-appearance:none;text-align-last:right;text-align:right;color:#fff;background:#131416;border:none;border-radius:4px;padding:0 5px}.bx-navigation-dialog .bx-focusable::after{border-radius:4px}.bx-navigation-dialog .bx-focusable:focus::after{top:0;left:0;right:0;bottom:0}.bx-navigation-dialog-overlay{position:fixed;background:rgba(11,11,11,0.89);top:0;left:0;right:0;bottom:0;z-index:var(--bx-navigation-dialog-overlay-z-index)}.bx-navigation-dialog-overlay[data-is-playing="true"]{background:transparent}.bx-centered-dialog{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;background:#1a1b1e;border-radius:10px;min-width:min(calc(100vw - 20px), 500px);max-width:calc(100vw - 20px);margin:0 0 0 auto;padding:16px;max-height:95vh;flex-direction:column;overflow:hidden;display:flex;flex-direction:column}.bx-centered-dialog .bx-dialog-title{display:flex;flex-direction:row;align-items:center;margin-bottom:10px}.bx-centered-dialog .bx-dialog-title p{padding:0;margin:0;flex:1;font-size:1.3rem;font-weight:bold}.bx-centered-dialog .bx-dialog-title button{flex-shrink:0}.bx-centered-dialog .bx-dialog-content{flex:1;padding:6px;overflow:auto;overflow-x:hidden}.bx-centered-dialog .bx-dialog-preset-tools{display:flex;margin-bottom:12px;gap:6px}.bx-centered-dialog .bx-dialog-preset-tools button{align-self:center;min-height:50px}.bx-centered-dialog .bx-default-preset-note{font-size:12px;font-style:italic;text-align:center;margin-bottom:10px}.bx-centered-dialog input,.bx-settings-dialog input{accent-color:var(--bx-primary-button-color)}.bx-centered-dialog input:focus,.bx-settings-dialog input:focus{accent-color:var(--bx-danger-button-color)}.bx-centered-dialog select:disabled,.bx-settings-dialog select:disabled{-webkit-appearance:none;background:transparent;text-align-last:right;border:none;color:#fff}.bx-centered-dialog select option:disabled,.bx-settings-dialog select option:disabled{display:none}.bx-centered-dialog input[type=checkbox]:focus,.bx-settings-dialog input[type=checkbox]:focus,.bx-centered-dialog select:focus,.bx-settings-dialog select:focus{filter:drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff)}.bx-centered-dialog a,.bx-settings-dialog a{color:#1c9d1c;text-decoration:none}.bx-centered-dialog a:hover,.bx-settings-dialog a:hover,.bx-centered-dialog a:focus,.bx-settings-dialog a:focus{color:#5dc21e}.bx-centered-dialog label,.bx-settings-dialog label{margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-note{margin-top:10px;font-size:14px;text-align:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row{display:flex;gap:10px;margin-bottom:10px;align-items:center}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt{flex-shrink:0;font-size:32px;margin:0}.bx-controller-shortcuts-manager-container .bx-shortcut-row label.bx-prompt::first-letter{letter-spacing:6px}.bx-controller-shortcuts-manager-container select:disabled{text-align:left;text-align-last:left}.bx-keyboard-shortcuts-manager-container{display:flex;flex-direction:column;gap:16px}.bx-keyboard-shortcuts-manager-container fieldset{background:#2a2a2a;border:1px solid #2a2a2a;border-radius:4px;padding:4px}.bx-keyboard-shortcuts-manager-container legend{width:auto;padding:4px 8px;margin:0 4px 4px;background:#004f87;box-shadow:0 2px 0 #071e3d;border-radius:4px;font-size:14px;font-weight:bold;text-transform:uppercase}.bx-keyboard-shortcuts-manager-container .bx-settings-row{background:none;padding:10px}.bx-settings-dialog{display:flex;position:fixed;top:0;right:0;bottom:0;opacity:.98;user-select:none;-webkit-user-select:none}.bx-settings-dialog .bx-settings-reload-note{font-size:.8rem;display:block;padding:8px;font-style:italic;font-weight:normal;height:var(--bx-button-height)}.bx-settings-tabs-container{position:fixed;width:48px;max-height:100vh;display:flex;flex-direction:column}.bx-settings-tabs-container > div:last-of-type{display:flex;flex-direction:column;align-items:end}.bx-settings-tabs-container > div:last-of-type button{flex-shrink:0;border-top-right-radius:0;border-bottom-right-radius:0;margin-top:8px;height:unset;padding:8px 10px}.bx-settings-tabs-container > div:last-of-type button svg{width:16px;height:16px}.bx-settings-tabs{display:flex;flex-direction:column;border-radius:0 0 0 8px;box-shadow:0 0 6px #000;overflow:overlay;flex:1}.bx-settings-tabs svg{width:24px;height:24px;padding:10px;flex-shrink:0;box-sizing:content-box;background:#131313;cursor:pointer;border-left:4px solid #1e1e1e}.bx-settings-tabs svg.bx-active{background:#222;border-color:#008746}.bx-settings-tabs svg:not(.bx-active):hover{background:#2f2f2f;border-color:#484848}.bx-settings-tabs svg:focus{border-color:#fff}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]{background:var(--bx-danger-button-color) !important}.bx-settings-tabs svg[data-group=global][data-need-refresh=true]:hover{background:var(--bx-danger-button-hover-color) !important}.bx-settings-tab-contents{flex-direction:column;margin-left:48px;width:450px;background:#1a1b1e;color:#fff;font-weight:400;font-size:16px;font-family:var(--bx-title-font);text-align:center;box-shadow:0 0 6px #000;overflow:overlay;z-index:1}.bx-settings-tab-contents .bx-top-buttons{display:flex;flex-direction:column;gap:8px;margin-bottom:8px}.bx-settings-tab-contents .bx-top-buttons .bx-button{display:block}.bx-settings-tab-contents h2{margin:16px 0 8px 0;display:flex;align-items:center}.bx-settings-tab-contents h2:first-of-type{margin-top:0}.bx-settings-tab-contents h2 span{display:inline-block;font-size:20px;font-weight:bold;text-align:left;flex:1;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;min-height:var(--bx-button-height);align-content:center}@media (max-width:500px){.bx-settings-tab-contents{width:calc(100vw - 48px)}}.bx-settings-row{display:flex;gap:10px;padding:16px 10px;margin:0;background:#2a2a2a;border-bottom:1px solid #343434}.bx-settings-row:hover,.bx-settings-row:focus-within{background-color:#242424}.bx-settings-row:not(:has(> input[type=checkbox])){flex-wrap:wrap}.bx-settings-row > span.bx-settings-label{font-size:14px;display:block;text-align:left;align-self:center;margin-bottom:0 !important;flex:1}.bx-settings-row > span.bx-settings-label svg{width:20px;height:20px;margin-inline-end:8px}.bx-settings-row > span.bx-settings-label + *{margin:0 0 0 auto}.bx-settings-row[data-multi-lines="true"]{flex-direction:column}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label{align-self:start}.bx-settings-row[data-multi-lines="true"] > span.bx-settings-label + *{margin:unset}.bx-settings-dialog-note{display:block;color:#afafb0;font-size:12px;font-weight:lighter;font-style:italic}.bx-settings-dialog-note:not(:has(a)){margin-top:4px}.bx-settings-dialog-note a{display:inline-block;padding:4px}.bx-settings-custom-user-agent{display:block;width:100%;padding:6px}.bx-donation-link{display:block;text-align:center;text-decoration:none;height:20px;line-height:20px;font-size:14px;margin-top:10px;margin-bottom:10px}.bx-debug-info button{margin-top:10px}.bx-debug-info pre{margin-top:10px;cursor:copy;color:#fff;padding:8px;border:1px solid #2d2d2d;background:#212121;white-space:break-spaces;text-align:left}.bx-debug-info pre:hover{background:#272727}.bx-settings-app-version{margin-top:10px;text-align:center;color:#747474;font-size:12px}.bx-note-unsupported{display:block;font-size:12px;font-style:italic;font-weight:normal;color:#828282}.bx-settings-tab-content{padding:10px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:has(+ .bx-settings-row){border-top-left-radius:6px;border-top-right-radius:6px}.bx-settings-tab-content > div .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-bottom-left-radius:6px;border-bottom-right-radius:6px}.bx-settings-tab-content > div *:not(.bx-settings-row):has(+ .bx-settings-row) + .bx-settings-row:not(:has(+ .bx-settings-row)){border:none;border-radius:6px}.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row[data-override=true],.bx-settings-tab-content:not([data-game-id="-1"]) .bx-settings-row:has(*[data-override=true]){border-left:4px solid #ffa500 !important;border-top-left-radius:0 !important;border-bottom-left-radius:0 !important}.bx-suggest-toggler{text-align:left;display:flex;border-radius:4px;overflow:hidden;background:#003861;height:45px;align-items:center}.bx-suggest-toggler label{flex:1;align-content:center;padding:0 10px;background:#004f87;height:100%}.bx-suggest-toggler span{display:inline-block;align-self:center;padding:10px;width:45px;text-align:center}.bx-suggest-toggler:hover,.bx-suggest-toggler:focus{cursor:pointer;background:#005da1}.bx-suggest-toggler:hover label,.bx-suggest-toggler:focus label{background:#006fbe}.bx-suggest-toggler[bx-open] span{transform:rotate(90deg)}.bx-suggest-toggler[bx-open]+ .bx-suggest-box{display:block}.bx-suggest-box{display:none}.bx-suggest-wrapper{display:flex;flex-direction:column;gap:10px;margin:10px}.bx-suggest-note{font-size:11px;color:#8c8c8c;font-style:italic;font-weight:100}.bx-suggest-link{font-size:14px;display:inline-block;margin-top:4px;padding:4px}.bx-suggest-row{display:flex;flex-direction:row;gap:10px}.bx-suggest-row label{flex:1;overflow:overlay;border-radius:4px}.bx-suggest-row label .bx-suggest-label{background:#323232;padding:4px 10px;font-size:12px;text-align:left}.bx-suggest-row label .bx-suggest-value{padding:6px;font-size:14px}.bx-suggest-row label .bx-suggest-value.bx-suggest-change{background-color:var(--bx-warning-color)}.bx-suggest-row.bx-suggest-ok input{visibility:hidden}.bx-suggest-row.bx-suggest-ok .bx-suggest-label{background-color:#008114}.bx-suggest-row.bx-suggest-ok .bx-suggest-value{background-color:#13a72a}.bx-suggest-row.bx-suggest-change .bx-suggest-label{background-color:#a65e08}.bx-suggest-row.bx-suggest-change .bx-suggest-value{background-color:#d57f18}.bx-suggest-row.bx-suggest-change:hover label{cursor:pointer}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-label{background-color:#995707}.bx-suggest-row.bx-suggest-change:hover .bx-suggest-value{background-color:#bd7115}.bx-suggest-row.bx-suggest-change input:not(:checked) + label{opacity:.5}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-label{background-color:#2a2a2a}.bx-suggest-row.bx-suggest-change input:not(:checked) + label .bx-suggest-value{background-color:#393939}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label{opacity:1}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-label{background-color:#202020}.bx-suggest-row.bx-suggest-change:hover input:not(:checked) + label .bx-suggest-value{background-color:#303030}.bx-sub-content-box{background:#161616;padding:10px;box-shadow:0 0 12px #0f0f0f inset;border-radius:10px}.bx-settings-row .bx-sub-content-box{background:#202020;padding:12px;box-shadow:0 0 4px #000 inset;border-radius:6px}.bx-controller-extra-settings[data-has-gamepad=true] > :first-child{display:none}.bx-controller-extra-settings[data-has-gamepad=true] > :last-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :first-child{display:block}.bx-controller-extra-settings[data-has-gamepad=false] > :last-child{display:none}.bx-controller-extra-settings .bx-controller-extra-wrapper{flex:1;min-width:1px}.bx-controller-extra-settings .bx-sub-content-box{flex:1;text-align:left;display:flex;flex-direction:column;margin-top:10px}.bx-controller-extra-settings .bx-sub-content-box > label{font-size:14px}.bx-preset-row{display:flex;gap:8px}.bx-preset-row .bx-select{flex:1}.bx-stream-settings-selection{margin-bottom:8px;position:sticky;z-index:1000;top:0}.bx-stream-settings-selection > div{display:flex;gap:8px;background:#222;padding:10px;border-bottom:4px solid #353638;box-shadow:0 0 6px #000;position:relative;z-index:1}.bx-stream-settings-selection > div .bx-select{flex:1}.bx-stream-settings-selection > div .bx-select label{font-weight:bold;font-size:1.1rem;line-height:initial}.bx-stream-settings-selection > div .bx-select label span{line-height:initial}.bx-stream-settings-selection > div .bx-select .bx-select-indicators{display:none}.bx-stream-settings-selection p{font-family:var(--bx-promptfont-font),var(--bx-normal-font);margin:0;font-size:13px;background:rgba(80,80,80,0.949);height:25px;line-height:23px;position:absolute;bottom:-25px;left:0;right:0;text-shadow:0 1px #000}.bx-toast{user-select:none;-webkit-user-select:none;position:fixed;left:50%;top:24px;transform:translate(-50%,0);background:#000;border-radius:16px;color:#fff;z-index:var(--bx-toast-z-index);font-family:var(--bx-normal-font);border:2px solid #fff;display:flex;align-items:center;opacity:0;overflow:clip;transition:opacity .2s ease-in}.bx-toast.bx-show{opacity:.85}.bx-toast.bx-hide{opacity:0;pointer-events:none}.bx-toast-msg{font-size:14px;display:inline-block;padding:12px 16px;white-space:pre}.bx-toast-status{font-weight:bold;font-size:14px;text-transform:uppercase;display:inline-block;background:#515863;padding:12px 16px;color:#fff;white-space:pre}.bx-wait-time-box{position:fixed;top:0;right:0;background-color:rgba(0,0,0,0.8);color:#fff;z-index:var(--bx-wait-time-box-z-index);padding:12px;border-radius:0 0 0 8px}.bx-wait-time-box label{display:block;text-transform:uppercase;text-align:right;font-size:12px;font-weight:bold;margin:0}.bx-wait-time-box span{display:block;font-family:var(--bx-monospaced-font);text-align:right;font-size:16px;margin-bottom:10px}.bx-wait-time-box span:last-of-type{margin-bottom:0}.bx-remote-play-settings{margin-bottom:12px;padding-bottom:12px;border-bottom:1px solid #2d2d2d}.bx-remote-play-settings > div{display:flex}.bx-remote-play-settings label{flex:1;font-size:14px}.bx-remote-play-settings label p{margin:4px 0 0;padding:0;color:#888;font-size:12px}.bx-remote-play-resolution{display:block}.bx-remote-play-resolution input[type="radio"]{accent-color:var(--bx-primary-button-color);margin-right:6px}.bx-remote-play-resolution input[type="radio"]:focus{accent-color:var(--bx-primary-button-hover-color)}.bx-remote-play-device-wrapper{display:flex;margin-bottom:12px}.bx-remote-play-device-wrapper:last-child{margin-bottom:2px}.bx-remote-play-device-info{flex:1;align-self:center;padding:4px 0}.bx-remote-play-device-name{font-size:14px;font-weight:bold;display:inline-block;vertical-align:middle}.bx-remote-play-console-type{font-size:8px;background:#004c87;color:#fff;display:inline-block;border-radius:8px;padding:2px 6px;margin-left:8px;vertical-align:middle}.bx-remote-play-power-state{color:#888;font-size:12px}.bx-remote-play-connect-button{min-height:100%;margin:4px 0}.bx-remote-play-buttons{display:flex;justify-content:space-between}select.bx-select{min-height:30px}div.bx-select{display:flex;align-items:stretch;flex:0 1 auto;gap:8px}div.bx-select select:disabled ~ button{display:none}div.bx-select select:disabled ~ div{background:#131416;color:#fff;pointer-events:none}div.bx-select select:disabled ~ div .bx-select-indicators{visibility:hidden}div.bx-select > div,div.bx-select button.bx-select-value{min-width:120px;text-align:left;line-height:24px;vertical-align:middle;background:#fff;color:#000;border-radius:4px;padding:2px 8px;display:flex;flex:1;flex-direction:column}div.bx-select > div{min-height:24px}div.bx-select > div input{display:inline-block;margin-right:8px}div.bx-select > div label{margin-bottom:0;font-size:14px;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;min-height:15px}div.bx-select > div label span{display:block;font-size:10px;font-weight:bold;text-align:left;line-height:20px;white-space:pre;min-height:15px;align-content:center}div.bx-select button.bx-select-value{border:none;cursor:pointer;min-height:30px;font-size:.9rem;align-items:center}div.bx-select button.bx-select-value > div{display:flex;width:100%}div.bx-select button.bx-select-value span{flex:1;text-align:left;display:inline-block}div.bx-select button.bx-select-value input{margin:0 4px;accent-color:var(--bx-primary-button-color);pointer-events:none}div.bx-select button.bx-select-value:hover input,div.bx-select button.bx-select-value:focus input{accent-color:var(--bx-danger-button-color)}div.bx-select button.bx-select-value:hover::after,div.bx-select button.bx-select-value:focus::after{border-color:#4d4d4d !important}div.bx-select button.bx-button{border:none;width:24px;height:auto;padding:0;color:#fff;border-radius:4px;font-weight:bold;font-size:12px;font-family:var(--bx-monospaced-font);flex-shrink:0}div.bx-select button.bx-button span{line-height:unset}div.bx-select[data-controller-friendly=true] > div{box-sizing:content-box}div.bx-select[data-controller-friendly=true] select{position:absolute !important;top:-9999px !important;left:-9999px !important;visibility:hidden !important}div.bx-select[data-controller-friendly=false]{position:relative}div.bx-select[data-controller-friendly=false] > div{box-sizing:border-box}div.bx-select[data-controller-friendly=false] > div label{margin-right:24px}div.bx-select[data-controller-friendly=false] select:disabled{display:none}div.bx-select[data-controller-friendly=false] select:not(:disabled){cursor:pointer;position:absolute;top:0;right:0;bottom:0;display:block;opacity:0;z-index:calc(var(--bx-settings-z-index) + 1)}div.bx-select[data-controller-friendly=false] select:not(:disabled):hover + div{background:#f0f0f0}div.bx-select[data-controller-friendly=false] select:not(:disabled) + div label::after{content:\'▾\';font-size:14px;position:absolute;right:8px;pointer-events:none}.bx-select-indicators{display:flex;height:4px;gap:2px;margin-bottom:2px}.bx-select-indicators span{content:\' \';display:inline-block;flex:1;background:#cfcfcf;border-radius:4px;min-width:1px}.bx-select-indicators span[data-highlighted]{background:#9c9c9c;min-width:6px}.bx-select-indicators span[data-selected]{background:#aacfe7}.bx-select-indicators span[data-highlighted][data-selected]{background:#5fa3d0}.bx-guide-home-achievements-progress{display:flex;gap:10px;flex-direction:row}.bx-guide-home-achievements-progress .bx-button{margin-bottom:0 !important}body[data-bx-media-type=tv] .bx-guide-home-achievements-progress{flex-direction:column}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress{flex-direction:row}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:first-of-type{flex:1}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type{width:40px}body:not([data-bx-media-type=tv]) .bx-guide-home-achievements-progress > button:last-of-type span{display:none}.bx-guide-home-buttons > div{display:flex;flex-direction:row;gap:12px}body[data-bx-media-type=tv] .bx-guide-home-buttons > div{flex-direction:column}body[data-bx-media-type=tv] .bx-guide-home-buttons > div button{margin-bottom:0 !important}body:not([data-bx-media-type=tv]) .bx-guide-home-buttons > div button span{display:none}.bx-guide-home-buttons[data-is-playing="true"] button[data-state=\'normal\']{display:none}.bx-guide-home-buttons[data-is-playing="false"] button[data-state=\'playing\']{display:none}div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]{overflow:visible}.bx-stream-menu-button-on{fill:#000 !important;background-color:#2d2d2d !important;color:#000 !important}.bx-stream-refresh-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important}body[data-media-type=default] .bx-stream-refresh-button{left:calc(env(safe-area-inset-left, 0px) + 11px) !important}body[data-media-type=tv] .bx-stream-refresh-button{top:calc(var(--gds-focus-borderSize) + 80px) !important}.bx-stream-home-button{top:calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important}body[data-media-type=default] .bx-stream-home-button{left:calc(env(safe-area-inset-left, 0px) + 12px) !important}body[data-media-type=tv] .bx-stream-home-button{top:calc(var(--gds-focus-borderSize) + 80px * 2) !important}div[data-testid=media-container][data-position=center]{display:flex}div[data-testid=media-container][data-position=top] video,div[data-testid=media-container][data-position=top] canvas{top:0}div[data-testid=media-container][data-position=bottom] video,div[data-testid=media-container][data-position=bottom] canvas{bottom:0}#game-stream video{margin:auto;align-self:center;background:#000;position:absolute;left:0;right:0}#game-stream canvas{align-self:center;margin:auto;position:absolute;left:0;right:0}#game-stream.bx-taking-screenshot:before{animation:bx-anim-taking-screenshot .5s ease;content:\' \';position:absolute;width:100%;height:100%;z-index:var(--bx-screenshot-animation-z-index)}#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button{overflow:visible;margin-bottom:12px}@-moz-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-webkit-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@-o-keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}@keyframes bx-anim-taking-screenshot{0%{border:0 solid rgba(255,255,255,0.502)}50%{border:8px solid rgba(255,255,255,0.502)}100%{border:0 solid rgba(255,255,255,0.502)}}.bx-number-stepper{text-align:center}.bx-number-stepper > div{display:flex;align-items:center}.bx-number-stepper > div span{flex:1;display:inline-block;min-width:40px;font-family:var(--bx-monospaced-font);white-space:pre;font-size:13px;margin:0 4px}.bx-number-stepper > div button{flex-shrink:0;border:none;width:24px;height:24px;margin:0;line-height:24px;background-color:var(--bx-default-button-color);color:#fff;border-radius:4px;font-weight:bold;font-size:14px;font-family:var(--bx-monospaced-font)}@media (hover:hover){.bx-number-stepper > div button:hover{background-color:var(--bx-default-button-hover-color)}}.bx-number-stepper > div button:active{background-color:var(--bx-default-button-hover-color)}.bx-number-stepper > div button:disabled + span{font-family:var(--bx-title-font)}.bx-number-stepper input[type=range]{display:block;margin:8px 0 2px auto;min-width:180px;width:100%;color:#959595 !important}.bx-number-stepper input[type=range]:disabled,.bx-number-stepper button:disabled{display:none}.bx-number-stepper[data-disabled=true] input[type=range],.bx-number-stepper[disabled=true] input[type=range],.bx-number-stepper[data-disabled=true] button,.bx-number-stepper[disabled=true] button{display:none}.bx-dual-number-stepper > span{display:block;font-family:var(--bx-monospaced-font);font-size:13px;white-space:pre;margin:0 4px;text-align:center}.bx-dual-number-stepper > div input[type=range]{display:block;width:100%;min-width:180px;background:transparent;color:#959595 !important;appearance:none;padding:8px 0}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),var(--bx-primary-button-color) var(--from) var(--to),#fff var(--to) 100%);height:8px;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-webkit-slider-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]::-moz-range-thumb{margin-top:-4px;appearance:none;width:4px;height:16px;background:#00b85f;border:none;border-radius:2px}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-runnable-track,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-runnable-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-track,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-track,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-track{background:linear-gradient(90deg,#fff var(--from),#006635 var(--from) var(--to),#fff var(--to) 100%)}.bx-dual-number-stepper > div input[type=range]:hover::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-webkit-slider-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-webkit-slider-thumb{background:#fb3232}.bx-dual-number-stepper > div input[type=range]:hover::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range].bx-dual-number-stepper > div input[type=range]:active::-moz-range-thumb,.bx-dual-number-stepper > div input[type=range]:focus::-moz-range-thumb{background:#fb3232}.bx-dual-number-stepper[data-disabled=true] input[type=range],.bx-dual-number-stepper[disabled=true] input[type=range]{display:none}#bx-game-bar{z-index:var(--bx-game-bar-z-index);position:fixed;bottom:0;width:40px;height:90px;overflow:visible;cursor:pointer}#bx-game-bar > svg{display:none;pointer-events:none;position:absolute;height:28px;margin-top:16px}@media (hover:hover){#bx-game-bar:hover > svg{display:block}}#bx-game-bar .bx-game-bar-container{opacity:0;position:absolute;display:flex;overflow:hidden;background:rgba(26,27,30,0.91);box-shadow:0 0 6px #1c1c1c;transition:opacity .1s ease-in}#bx-game-bar .bx-game-bar-container.bx-show{opacity:.9}#bx-game-bar .bx-game-bar-container.bx-show + svg{display:none !important}#bx-game-bar .bx-game-bar-container.bx-hide{opacity:0;pointer-events:none}#bx-game-bar .bx-game-bar-container button{width:60px;height:60px;border-radius:0}#bx-game-bar .bx-game-bar-container button svg{width:28px;height:28px;transition:transform .08s ease 0s}#bx-game-bar .bx-game-bar-container button:hover{border-radius:0}#bx-game-bar .bx-game-bar-container button:active svg{transform:scale(.75)}#bx-game-bar .bx-game-bar-container button.bx-activated{background-color:#fff}#bx-game-bar .bx-game-bar-container button.bx-activated svg{filter:invert(1)}#bx-game-bar .bx-game-bar-container div[data-activated] button{display:none}#bx-game-bar .bx-game-bar-container div[data-activated=\'false\'] button:first-of-type{display:block}#bx-game-bar .bx-game-bar-container div[data-activated=\'true\'] button:last-of-type{display:block}#bx-game-bar[data-position="bottom-left"]{left:0;direction:ltr}#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container{border-radius:0 10px 10px 0}#bx-game-bar[data-position="bottom-right"]{right:0;direction:rtl}#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container{direction:ltr;border-radius:10px 0 0 10px}.bx-badges{margin-left:0;user-select:none;-webkit-user-select:none}.bx-badge{border:none;display:inline-block;line-height:24px;color:#fff;font-family:var(--bx-title-font-semibold);font-size:14px;font-weight:400;margin:0 8px 8px 0;box-shadow:0 0 6px #000;border-radius:4px}.bx-badge-name{background-color:#2d3036;border-radius:4px 0 0 4px}.bx-badge-name svg{width:16px;height:16px}.bx-badge-value{background-color:#808080;border-radius:0 4px 4px 0}.bx-badge-name,.bx-badge-value{display:inline-block;padding:0 8px;line-height:30px;vertical-align:bottom}.bx-badge-battery[data-charging=true] span:first-of-type::after{content:\' ⚡️\'}div[class^=StreamMenu-module__container] .bx-badges{position:absolute;max-width:500px}#gamepass-dialog-root .bx-badges{position:fixed;top:60px;left:460px;max-width:500px}@media (min-width:568px) and (max-height:480px){#gamepass-dialog-root .bx-badges{position:unset;top:unset;left:unset;margin:8px 0}}.bx-stats-bar{display:flex;flex-direction:row;gap:8px;user-select:none;-webkit-user-select:none;position:fixed;top:0;background-color:#000;color:#fff;font-family:var(--bx-monospaced-font);font-size:.9rem;padding-left:8px;z-index:var(--bx-stats-bar-z-index);text-wrap:nowrap}.bx-stats-bar[data-stats*="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats*="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats*="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats*="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats*="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats*="[ul]"] > .bx-stat-ul{display:inline-flex;align-items:baseline}.bx-stats-bar[data-stats$="[time]"] > .bx-stat-time,.bx-stats-bar[data-stats$="[play]"] > .bx-stat-play,.bx-stats-bar[data-stats$="[batt]"] > .bx-stat-batt,.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,.bx-stats-bar[data-stats$="[jit]"] > .bx-stat-jit,.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl,.bx-stats-bar[data-stats$="[dl]"] > .bx-stat-dl,.bx-stats-bar[data-stats$="[ul]"] > .bx-stat-ul{border-right:none}.bx-stats-bar::before{display:none;content:\'👀\';vertical-align:middle;margin-right:8px}.bx-stats-bar[data-display=glancing]::before{display:inline-block}.bx-stats-bar[data-position=top-left]{left:0;border-radius:0 0 4px 0}.bx-stats-bar[data-position=top-right]{right:0;border-radius:0 0 0 4px}.bx-stats-bar[data-position=top-center]{transform:translate(-50%,0);left:50%;border-radius:0 0 4px 4px}.bx-stats-bar[data-shadow=true]{background:none;filter:drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941))}.bx-stats-bar > div{display:none;border-right:1px solid #fff;padding-right:8px}.bx-stats-bar label{margin:0 8px 0 0;font-family:var(--bx-title-font);font-size:70%;font-weight:bold;vertical-align:middle;cursor:help}.bx-stats-bar span{display:inline-block;text-align:right;vertical-align:middle;white-space:pre}.bx-stats-bar span[data-grade=good]{color:#6bffff}.bx-stats-bar span[data-grade=ok]{color:#fff16b}.bx-stats-bar span[data-grade=bad]{color:#ff5f5f}.bx-mkb-settings{display:flex;flex-direction:column;flex:1;padding-bottom:10px;overflow:hidden}.bx-mkb-pointer-lock-msg{user-select:none;-webkit-user-select:none;position:fixed;left:50%;bottom:40px;transform:translateX(-50%);margin:auto;background:#151515;z-index:var(--bx-mkb-pointer-lock-msg-z-index);color:#fff;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem;padding:12px;border-radius:8px;align-items:center;box-shadow:0 0 6px #000;min-width:300px;opacity:.9;display:flex;flex-direction:column;gap:10px}.bx-mkb-pointer-lock-msg:hover{opacity:1}.bx-mkb-pointer-lock-msg > p{margin:0;width:100%;font-size:22px;margin-bottom:4px;font-weight:bold;text-align:left}.bx-mkb-pointer-lock-msg > div{width:100%;display:flex;flex-direction:row;gap:10px}.bx-mkb-pointer-lock-msg > div button:first-of-type{flex-shrink:1}.bx-mkb-pointer-lock-msg > div button:last-of-type{flex-grow:1}.bx-mkb-key-row{display:flex;margin-bottom:10px;align-items:center;gap:20px}.bx-mkb-key-row label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center}.bx-mkb-settings.bx-editing .bx-mkb-key-row button{background:#393939;border-radius:4px;border:none}.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover{background:#333;cursor:pointer}.bx-mkb-action-buttons > div{text-align:right;display:none}.bx-mkb-action-buttons button{margin-left:8px}.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child{display:block}.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child{display:block}.bx-mkb-note{display:block;margin:0 0 10px;font-size:12px;text-align:center}button.bx-binding-button{flex:1;min-height:38px;border:none;border-radius:4px;font-size:14px;color:#fff;display:flex;align-items:center;align-self:center;padding:0 6px}button.bx-binding-button:disabled{background:#131416;padding:0 8px}button.bx-binding-button:not(:disabled){border:2px solid transparent;border-top:none;border-bottom:4px solid #252525;background:#3b3b3b;cursor:pointer}button.bx-binding-button:not(:disabled):hover,button.bx-binding-button:not(:disabled).bx-focusable:focus{background:#20b217;border-bottom-color:#186c13}button.bx-binding-button:not(:disabled):active{background:#16900f;border-bottom:3px solid #0c4e08;border-left-width:2px;border-right-width:2px}button.bx-binding-button:not(:disabled).bx-focusable:focus::after{top:-6px;left:-8px;right:-8px;bottom:-10px}.bx-settings-row .bx-binding-button-wrapper button.bx-binding-button{min-width:60px}.bx-controller-customizations-container .bx-btn-detect{display:block;margin-bottom:20px}.bx-controller-customizations-container .bx-btn-detect.bx-monospaced{background:none;font-weight:bold;font-size:12px}.bx-controller-customizations-container .bx-buttons-grid{display:grid;grid-template-columns:auto auto;column-gap:20px;row-gap:10px;margin-bottom:20px}.bx-controller-key-row{display:flex;align-items:stretch}.bx-controller-key-row > label{margin-bottom:0;font-family:var(--bx-promptfont-font);font-size:32px;text-align:center;min-width:50px;flex-shrink:0;display:flex;align-self:center}.bx-controller-key-row > label::after{content:\'❯\';margin:0 12px;font-size:16px;align-self:center}.bx-controller-key-row .bx-select{width:100% !important}.bx-controller-key-row .bx-select > div{min-width:50px}.bx-controller-key-row .bx-select label{font-family:var(--bx-promptfont-font),var(--bx-normal-font);font-size:32px;text-align:center;margin-bottom:6px;height:40px;line-height:40px}.bx-controller-key-row:hover > label{color:#ffe64b}.bx-controller-key-row:hover > label::after{color:#fff}.bx-controller-customization-summary{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.bx-controller-customization-summary span{font-family:var(--bx-promptfont);font-size:24px;border-radius:6px;background:#131313;color:#fff;display:inline-block;padding:2px;text-align:center}.bx-product-details-icons{padding:8px;border-radius:4px}.bx-product-details-icons svg{margin-right:8px}.bx-product-details-buttons{display:flex;gap:10px;flex-direction:row}.bx-product-details-buttons button{max-width:max-content;margin:10px 0 0 0;display:flex}@media (min-width:568px) and (max-height:480px){.bx-product-details-buttons{flex-direction:column}.bx-product-details-buttons button{margin:8px 0 0 10px}}', PREF_HIDE_SECTIONS = getGlobalPref("ui.hideSections"), selectorToHide = [];
+if (PREF_HIDE_SECTIONS.includes("news")) selectorToHide.push("#BodyContent > div[class*=CarouselRow-module]");
+if (getGlobalPref("block.features").includes("byog")) selectorToHide.push("#BodyContent > div[class*=ByogRow-module__container___]");
+if (PREF_HIDE_SECTIONS.includes("all-games")) selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__gridContainer]"), selectorToHide.push("#BodyContent div[class*=AllGamesRow-module__rowHeader]");
+if (PREF_HIDE_SECTIONS.includes("most-popular")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/popular"])');
+if (PREF_HIDE_SECTIONS.includes("touch")) selectorToHide.push('#BodyContent div[class*=HomePage-module__bottomSpacing]:has(a[href="/play/gallery/touch"])');
+if (getGlobalPref("block.features").includes("friends")) selectorToHide.push("#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]");
+if (selectorToHide) css += selectorToHide.join(",") + "{ display: none; }";
+if (getGlobalPref("ui.reduceAnimations")) css += "div[class*=GameCard-module__gameTitleInnerWrapper],div[class*=GameCard-module__card],div[class*=ScrollArrows-module]{transition:none !important}";
+if (getGlobalPref("ui.systemMenu.hideHandle")) css += "div[class*=Grip-module__container]{visibility:hidden}@media (hover:hover){button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container]{visibility:visible}}button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container]{visibility:visible}button[class*=GripHandle-module__container][aria-expanded=false]{background-color:transparent !important}div[class*=StreamHUD-module__buttonsContainer]{padding:0 !important}";
+if (css += "div[class*=StreamMenu-module__menu]{min-width:100vw !important}", getGlobalPref("ui.streamMenu.simplify")) css += "div[class*=Menu-module__scrollable]{--bxStreamMenuItemSize:80px;--streamMenuItemSize:calc(var(--bxStreamMenuItemSize) + 40px) !important}.bx-badges{top:calc(var(--streamMenuItemSize) - 20px)}body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) - 10px) !important}button[class*=MenuItem-module__container]{min-width:auto !important;min-height:auto !important;width:var(--bxStreamMenuItemSize) !important;height:var(--bxStreamMenuItemSize) !important}div[class*=MenuItem-module__label]{display:none !important}svg[class*=MenuItem-module__icon]{width:36px;height:100% !important;padding:0 !important;margin:0 !important}";
+else css += "body[data-media-type=tv] .bx-badges{top:calc(var(--streamMenuItemSize) + 30px)}body:not([data-media-type=tv]) .bx-badges{top:calc(var(--streamMenuItemSize) + 20px)}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]{min-width:auto !important;width:100px !important}body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2){margin-left:10px !important}body:not([data-media-type=tv]) div[class*=MenuItem-module__label]{margin-left:8px !important;margin-right:8px !important}";
+if (getGlobalPref("ui.hideScrollbar")) css += "html{scrollbar-width:none}body::-webkit-scrollbar{display:none}";
+let $style = CE("style", !1, css);
+document.documentElement.appendChild($style);
}
function preloadFonts() {
- let $link = CE("link", {
- rel: "preload",
- href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",
- as: "font",
- type: "font/otf",
- crossorigin: ""
- });
- document.querySelector("head")?.appendChild($link);
+let $link = CE("link", {
+rel: "preload",
+href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",
+as: "font",
+type: "font/otf",
+crossorigin: ""
+});
+document.querySelector("head")?.appendChild($link);
}
class MouseCursorHider {
- static instance;
- static getInstance() {
- if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider;
- else MouseCursorHider.instance = null;
- return MouseCursorHider.instance;
- }
- timeoutId;
- isCursorVisible = !0;
- show() {
- document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0;
- }
- hide() {
- document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1;
- }
- onMouseMove = (e) => {
- !this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000);
- };
- start() {
- this.show(), document.addEventListener("mousemove", this.onMouseMove);
- }
- stop() {
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show();
- }
+static instance;
+static getInstance() {
+if (typeof MouseCursorHider.instance === "undefined") if (!getGlobalPref("mkb.enabled") && getGlobalPref("mkb.cursor.hideIdle")) MouseCursorHider.instance = new MouseCursorHider;
+else MouseCursorHider.instance = null;
+return MouseCursorHider.instance;
+}
+timeoutId;
+isCursorVisible = !0;
+show() {
+document.body && (document.body.style.cursor = "unset"), this.isCursorVisible = !0;
+}
+hide() {
+document.body && (document.body.style.cursor = "none"), this.timeoutId = null, this.isCursorVisible = !1;
+}
+onMouseMove = (e) => {
+!this.isCursorVisible && this.show(), this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = window.setTimeout(this.hide, 3000);
+};
+start() {
+this.show(), document.addEventListener("mousemove", this.onMouseMove);
+}
+stop() {
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null, document.removeEventListener("mousemove", this.onMouseMove), this.show();
+}
}
function patchHistoryMethod(type) {
- let orig = window.history[type];
- return function(...args) {
- return BxEvent.dispatch(window, BxEvent.POPSTATE, {
- arguments: args
- }), orig.apply(this, arguments);
- };
+let orig = window.history[type];
+return function(...args) {
+return BxEvent.dispatch(window, BxEvent.POPSTATE, {
+arguments: args
+}), orig.apply(this, arguments);
+};
}
function onHistoryChanged(e) {
- if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;
- window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});
+if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud") return;
+window.setTimeout(RemotePlayManager.detect, 10), NavigationDialogManager.getInstance().hide(), LoadingScreen.reset(), window.setTimeout(HeaderSection.watchHeader, 2000), BxEventBus.Stream.emit("state.stopped", {});
}
function setCodecPreferences(sdp, preferredCodec) {
- let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];
- for (let match of matches) {
- let id = match[1];
- if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);
- }
- if (!preferredCodecIds.length) return sdp;
- let lines = sdp.split(`\r
+let h264Pattern = /a=fmtp:(\d+).*profile-level-id=([0-9a-f]{6})/g, profilePrefix = preferredCodec === "high" ? "4d" : preferredCodec === "low" ? "420" : "42e", preferredCodecIds = [], matches = sdp.matchAll(h264Pattern) || [];
+for (let match of matches) {
+let id = match[1];
+if (match[2].startsWith(profilePrefix)) preferredCodecIds.push(id);
+}
+if (!preferredCodecIds.length) return sdp;
+let lines = sdp.split(`\r
`);
- for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
- let line = lines[lineIndex];
- if (!line.startsWith("m=video")) continue;
- let tmp = line.trim().split(" "), ids = tmp.slice(3);
- ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");
- break;
- }
- return lines.join(`\r
+for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
+let line = lines[lineIndex];
+if (!line.startsWith("m=video")) continue;
+let tmp = line.trim().split(" "), ids = tmp.slice(3);
+ids = ids.filter((item2) => !preferredCodecIds.includes(item2)), ids = preferredCodecIds.concat(ids), lines[lineIndex] = tmp.slice(0, 3).concat(ids).join(" ");
+break;
+}
+return lines.join(`\r
`);
}
function patchSdpBitrate(sdp, video, audio) {
- let lines = sdp.split(`\r
+let lines = sdp.split(`\r
`), mediaSet = new Set;
- !!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");
- let bitrate = {
- video,
- audio
- };
- for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {
- let media = "", line = lines[lineNumber];
- if (!line.startsWith("m=")) continue;
- for (let m of mediaSet)
- if (line.startsWith(`m=${m}`)) {
- media = m, mediaSet.delete(media);
- break;
- }
- if (!media) continue;
- let bLine = `b=AS:${bitrate[media]}`;
- while (lineNumber++, lineNumber < lines.length) {
- if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue;
- if (line.startsWith("b=AS:")) {
- lines[lineNumber] = bLine;
- break;
- }
- if (line.startsWith("m=")) {
- lines.splice(lineNumber, 0, bLine);
- break;
- }
- }
- }
- return lines.join(`\r
+!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");
+let bitrate = {
+video,
+audio
+};
+for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {
+let media = "", line = lines[lineNumber];
+if (!line.startsWith("m=")) continue;
+for (let m of mediaSet)
+if (line.startsWith(`m=${m}`)) {
+media = m, mediaSet.delete(media);
+break;
+}
+if (!media) continue;
+let bLine = `b=AS:${bitrate[media]}`;
+while (lineNumber++, lineNumber < lines.length) {
+if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c=")) continue;
+if (line.startsWith("b=AS:")) {
+lines[lineNumber] = bLine;
+break;
+}
+if (line.startsWith("m=")) {
+lines.splice(lineNumber, 0, bLine);
+break;
+}
+}
+}
+return lines.join(`\r
`);
}
var clarity_boost_default = `#version 300 es
@@ -9150,1022 +9155,1022 @@ color = brightness * color;
fragColor = vec4(color, 1.0);
}`;
class WebGL2Player {
- LOG_TAG = "WebGL2Player";
- $video;
- $canvas;
- gl = null;
- resources = [];
- program = null;
- stopped = !1;
- options = {
- filterId: 1,
- sharpenFactor: 0,
- brightness: 0,
- contrast: 0,
- saturation: 0
- };
- targetFps = 60;
- frameInterval = 0;
- lastFrameTime = 0;
- animFrameId = null;
- constructor($video) {
- BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;
- let $canvas = document.createElement("canvas");
- $canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
- }
- setFilter(filterId, update = !0) {
- this.options.filterId = filterId, update && this.updateCanvas();
- }
- setSharpness(sharpness, update = !0) {
- this.options.sharpenFactor = sharpness, update && this.updateCanvas();
- }
- setBrightness(brightness, update = !0) {
- this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
- }
- setContrast(contrast, update = !0) {
- this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
- }
- setSaturation(saturation, update = !0) {
- this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
- }
- setTargetFps(target) {
- this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
- }
- getCanvas() {
- return this.$canvas;
- }
- updateCanvas() {
- let gl = this.gl, program = this.program;
- gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
- }
- forceDrawFrame() {
- let gl = this.gl;
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
- }
- setupRendering() {
- let frameCallback;
- if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
- let $video = this.$video;
- frameCallback = $video.requestVideoFrameCallback.bind($video);
- } else frameCallback = requestAnimationFrame;
- let animate = () => {
- if (this.stopped) return;
- this.animFrameId = frameCallback(animate);
- let draw = !0;
- if (this.targetFps === 0) draw = !1;
- else if (this.targetFps < 60) {
- let currentTime = performance.now();
- if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
- else this.lastFrameTime = currentTime;
- }
- if (draw) {
- let gl = this.gl;
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
- }
- };
- this.animFrameId = frameCallback(animate);
- }
- setupShaders() {
- BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));
- let gl = this.$canvas.getContext("webgl2", {
- isBx: !0,
- antialias: !0,
- alpha: !1,
- powerPreference: getStreamPref("video.player.powerPreference")
- });
- this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
- let vShader = gl.createShader(gl.VERTEX_SHADER);
- gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
- let fShader = gl.createShader(gl.FRAGMENT_SHADER);
- gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
- let program = gl.createProgram();
- if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
- this.updateCanvas();
- let buffer = gl.createBuffer();
- this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
- let texture = gl.createTexture();
- this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
- }
- resume() {
- this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
- }
- stop() {
- if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
- if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
- else cancelAnimationFrame(this.animFrameId);
- this.animFrameId = null;
- }
- }
- destroy() {
- BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
- let gl = this.gl;
- if (gl) {
- gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
- for (let resource of this.resources)
- if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
- else if (resource instanceof WebGLShader) gl.deleteShader(resource);
- else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
- else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
- this.gl = null;
- }
- if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);
- this.$canvas.width = 1, this.$canvas.height = 1;
- }
+LOG_TAG = "WebGL2Player";
+$video;
+$canvas;
+gl = null;
+resources = [];
+program = null;
+stopped = !1;
+options = {
+filterId: 1,
+sharpenFactor: 0,
+brightness: 0,
+contrast: 0,
+saturation: 0
+};
+targetFps = 60;
+frameInterval = 0;
+lastFrameTime = 0;
+animFrameId = null;
+constructor($video) {
+BxLogger.info(this.LOG_TAG, "Initialize"), this.$video = $video;
+let $canvas = document.createElement("canvas");
+$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.$canvas = $canvas, this.setupShaders(), this.setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
+}
+setFilter(filterId, update = !0) {
+this.options.filterId = filterId, update && this.updateCanvas();
+}
+setSharpness(sharpness, update = !0) {
+this.options.sharpenFactor = sharpness, update && this.updateCanvas();
+}
+setBrightness(brightness, update = !0) {
+this.options.brightness = 1 + (brightness - 100) / 100, update && this.updateCanvas();
+}
+setContrast(contrast, update = !0) {
+this.options.contrast = 1 + (contrast - 100) / 100, update && this.updateCanvas();
+}
+setSaturation(saturation, update = !0) {
+this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
+}
+setTargetFps(target) {
+this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
+}
+getCanvas() {
+return this.$canvas;
+}
+updateCanvas() {
+let gl = this.gl, program = this.program;
+gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
+}
+forceDrawFrame() {
+let gl = this.gl;
+gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
+}
+setupRendering() {
+let frameCallback;
+if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
+let $video = this.$video;
+frameCallback = $video.requestVideoFrameCallback.bind($video);
+} else frameCallback = requestAnimationFrame;
+let animate = () => {
+if (this.stopped) return;
+this.animFrameId = frameCallback(animate);
+let draw = !0;
+if (this.targetFps === 0) draw = !1;
+else if (this.targetFps < 60) {
+let currentTime = performance.now();
+if (currentTime - this.lastFrameTime < this.frameInterval) draw = !1;
+else this.lastFrameTime = currentTime;
+}
+if (draw) {
+let gl = this.gl;
+gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video), gl.drawArrays(gl.TRIANGLES, 0, 6);
+}
+};
+this.animFrameId = frameCallback(animate);
+}
+setupShaders() {
+BxLogger.info(this.LOG_TAG, "Setting up", getStreamPref("video.player.powerPreference"));
+let gl = this.$canvas.getContext("webgl2", {
+isBx: !0,
+antialias: !0,
+alpha: !1,
+powerPreference: getStreamPref("video.player.powerPreference")
+});
+this.gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
+let vShader = gl.createShader(gl.VERTEX_SHADER);
+gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
+let fShader = gl.createShader(gl.FRAGMENT_SHADER);
+gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
+let program = gl.createProgram();
+if (this.program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS)) console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
+this.updateCanvas();
+let buffer = gl.createBuffer();
+this.resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
+let texture = gl.createTexture();
+this.resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
+}
+resume() {
+this.stop(), this.stopped = !1, BxLogger.info(this.LOG_TAG, "Resume"), this.$canvas.classList.remove("bx-gone"), this.setupRendering();
+}
+stop() {
+if (BxLogger.info(this.LOG_TAG, "Stop"), this.$canvas.classList.add("bx-gone"), this.stopped = !0, this.animFrameId) {
+if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) this.$video.cancelVideoFrameCallback(this.animFrameId);
+else cancelAnimationFrame(this.animFrameId);
+this.animFrameId = null;
+}
+}
+destroy() {
+BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
+let gl = this.gl;
+if (gl) {
+gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
+for (let resource of this.resources)
+if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
+else if (resource instanceof WebGLShader) gl.deleteShader(resource);
+else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
+else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
+this.gl = null;
+}
+if (this.$canvas.isConnected) this.$canvas.parentElement?.removeChild(this.$canvas);
+this.$canvas.width = 1, this.$canvas.height = 1;
+}
}
class StreamPlayer {
- $video;
- playerType = "default";
- options = {};
- webGL2Player = null;
- $videoCss = null;
- $usmMatrix = null;
- constructor($video, type, options) {
- this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
- }
- setupVideoElements() {
- if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;
- let $fragment = document.createDocumentFragment();
- this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
- let $svg = CE("svg", {
- id: "bx-video-filters",
- xmlns: "http://www.w3.org/2000/svg",
- class: "bx-gone"
- }, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {
- id: "bx-filter-usm",
- xmlns: "http://www.w3.org/2000/svg"
- }, this.$usmMatrix = CE("feConvolveMatrix", {
- id: "bx-filter-usm-matrix",
- order: "3",
- xmlns: "http://www.w3.org/2000/svg"
- }))));
- $fragment.appendChild($svg), document.documentElement.appendChild($fragment);
- }
- getVideoPlayerFilterStyle() {
- let filters = [], sharpness = this.options.sharpness || 0;
- if (this.options.processing === "usm" && sharpness != 0) {
- let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
- this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");
- }
- let saturation = this.options.saturation || 100;
- if (saturation != 100) filters.push(`saturate(${saturation}%)`);
- let contrast = this.options.contrast || 100;
- if (contrast != 100) filters.push(`contrast(${contrast}%)`);
- let brightness = this.options.brightness || 100;
- if (brightness != 100) filters.push(`brightness(${brightness}%)`);
- return filters.join(" ");
- }
- resizePlayer() {
- let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;
- if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
- let targetWidth, targetHeight, targetObjectFit;
- if (PREF_RATIO.includes(":")) {
- let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
- if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
- else width = parentRect.width, height = width / videoRatio;
- width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();
- let $parent = $video.parentElement, position = getStreamPref("video.position");
- if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {
- let padding = Math.floor((window.innerHeight - height) / 4);
- if (padding > 0) {
- if (position === "bottom-half") padding *= 3;
- $parent.style.paddingTop = padding + "px";
- }
- }
- targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
- } else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
- if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));
- if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
- }
- setPlayerType(type, refreshPlayer = !1) {
- if (this.playerType !== type) {
- let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
- if (type === "webgl2") {
- if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
- else this.webGL2Player.resume();
- this.$videoCss.textContent = "", this.$video.classList.add(videoClass);
- } else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);
- }
- this.playerType = type, refreshPlayer && this.refreshPlayer();
- }
- setOptions(options, refreshPlayer = !1) {
- this.options = options, refreshPlayer && this.refreshPlayer();
- }
- updateOptions(options, refreshPlayer = !1) {
- this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
- }
- getPlayerElement(playerType) {
- if (typeof playerType === "undefined") playerType = this.playerType;
- if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
- return this.$video;
- }
- getWebGL2Player() {
- return this.webGL2Player;
- }
- refreshPlayer() {
- if (this.playerType === "webgl2") {
- let options = this.options, webGL2Player = this.webGL2Player;
- if (options.processing === "usm") webGL2Player.setFilter(1);
- else webGL2Player.setFilter(2);
- ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
- } else {
- let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
- if (filters) videoCss += `filter: ${filters} !important;`;
- if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);
- let css = "";
- if (videoCss) css = `#game-stream video { ${videoCss} }`;
- this.$videoCss.textContent = css;
- }
- this.resizePlayer();
- }
- reloadPlayer() {
- this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);
- }
- cleanUpWebGL2Player() {
- this.webGL2Player?.destroy(), this.webGL2Player = null;
- }
- destroy() {
- this.cleanUpWebGL2Player();
- }
+$video;
+playerType = "default";
+options = {};
+webGL2Player = null;
+$videoCss = null;
+$usmMatrix = null;
+constructor($video, type, options) {
+this.setupVideoElements(), this.$video = $video, this.options = options || {}, this.setPlayerType(type);
+}
+setupVideoElements() {
+if (this.$videoCss = document.getElementById("bx-video-css"), this.$videoCss) return;
+let $fragment = document.createDocumentFragment();
+this.$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.$videoCss);
+let $svg = CE("svg", {
+id: "bx-video-filters",
+xmlns: "http://www.w3.org/2000/svg",
+class: "bx-gone"
+}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {
+id: "bx-filter-usm",
+xmlns: "http://www.w3.org/2000/svg"
+}, this.$usmMatrix = CE("feConvolveMatrix", {
+id: "bx-filter-usm-matrix",
+order: "3",
+xmlns: "http://www.w3.org/2000/svg"
+}))));
+$fragment.appendChild($svg), document.documentElement.appendChild($fragment);
+}
+getVideoPlayerFilterStyle() {
+let filters = [], sharpness = this.options.sharpness || 0;
+if (this.options.processing === "usm" && sharpness != 0) {
+let matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
+this.$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");
+}
+let saturation = this.options.saturation || 100;
+if (saturation != 100) filters.push(`saturate(${saturation}%)`);
+let contrast = this.options.contrast || 100;
+if (contrast != 100) filters.push(`contrast(${contrast}%)`);
+let brightness = this.options.brightness || 100;
+if (brightness != 100) filters.push(`brightness(${brightness}%)`);
+return filters.join(" ");
+}
+resizePlayer() {
+let PREF_RATIO = getStreamPref("video.ratio"), $video = this.$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport, $webGL2Canvas;
+if (this.playerType == "webgl2") $webGL2Canvas = this.webGL2Player?.getCanvas();
+let targetWidth, targetHeight, targetObjectFit;
+if (PREF_RATIO.includes(":")) {
+let tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]), width = 0, height = 0, parentRect = $video.parentElement.getBoundingClientRect();
+if (parentRect.width / parentRect.height > videoRatio) height = parentRect.height, width = height * videoRatio;
+else width = parentRect.width, height = width / videoRatio;
+width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString();
+let $parent = $video.parentElement, position = getStreamPref("video.position");
+if ($parent.style.removeProperty("padding-top"), $parent.dataset.position = position, position === "top-half" || position === "bottom-half") {
+let padding = Math.floor((window.innerHeight - height) / 4);
+if (padding > 0) {
+if (position === "bottom-half") padding *= 3;
+$parent.style.paddingTop = padding + "px";
+}
+}
+targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
+} else targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
+if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas) $webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit, $video.dispatchEvent(new Event("resize"));
+if (isNativeTouchGame && this.playerType == "webgl2") window.BX_EXPOSED.streamSession.updateDimensions();
+}
+setPlayerType(type, refreshPlayer = !1) {
+if (this.playerType !== type) {
+let videoClass = BX_FLAGS.DeviceInfo.deviceType === "android-tv" ? "bx-pixel" : "bx-gone";
+if (type === "webgl2") {
+if (!this.webGL2Player) this.webGL2Player = new WebGL2Player(this.$video);
+else this.webGL2Player.resume();
+this.$videoCss.textContent = "", this.$video.classList.add(videoClass);
+} else this.webGL2Player?.stop(), this.$video.classList.remove(videoClass);
+}
+this.playerType = type, refreshPlayer && this.refreshPlayer();
+}
+setOptions(options, refreshPlayer = !1) {
+this.options = options, refreshPlayer && this.refreshPlayer();
+}
+updateOptions(options, refreshPlayer = !1) {
+this.options = Object.assign(this.options, options), refreshPlayer && this.refreshPlayer();
+}
+getPlayerElement(playerType) {
+if (typeof playerType === "undefined") playerType = this.playerType;
+if (playerType === "webgl2") return this.webGL2Player?.getCanvas();
+return this.$video;
+}
+getWebGL2Player() {
+return this.webGL2Player;
+}
+refreshPlayer() {
+if (this.playerType === "webgl2") {
+let options = this.options, webGL2Player = this.webGL2Player;
+if (options.processing === "usm") webGL2Player.setFilter(1);
+else webGL2Player.setFilter(2);
+ScreenshotManager.getInstance().updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
+} else {
+let filters = this.getVideoPlayerFilterStyle(), videoCss = "";
+if (filters) videoCss += `filter: ${filters} !important;`;
+if (getGlobalPref("screenshot.applyFilters")) ScreenshotManager.getInstance().updateCanvasFilters(filters);
+let css = "";
+if (videoCss) css = `#game-stream video { ${videoCss} }`;
+this.$videoCss.textContent = css;
+}
+this.resizePlayer();
+}
+reloadPlayer() {
+this.cleanUpWebGL2Player(), this.playerType = "default", this.setPlayerType("webgl2", !1);
+}
+cleanUpWebGL2Player() {
+this.webGL2Player?.destroy(), this.webGL2Player = null;
+}
+destroy() {
+this.cleanUpWebGL2Player();
+}
}
function patchVideoApi() {
- let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {
- if (this.style.visibility = "visible", !this.videoWidth) return;
- let playerOptions = {
- processing: getStreamPref("video.processing"),
- sharpness: getStreamPref("video.processing.sharpness"),
- saturation: getStreamPref("video.saturation"),
- contrast: getStreamPref("video.contrast"),
- brightness: getStreamPref("video.brightness")
- };
- STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {
- $video: this
- });
- }, nativePlay = HTMLMediaElement.prototype.play;
- HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {
- if (this.className && this.className.startsWith("XboxSplashVideo")) {
- if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});
- return nativePlay.apply(this);
- }
- let $parent = this.parentElement;
- if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });
- return nativePlay.apply(this);
- };
+let PREF_SKIP_SPLASH_VIDEO = getGlobalPref("ui.splashVideo.skip"), showFunc = function() {
+if (this.style.visibility = "visible", !this.videoWidth) return;
+let playerOptions = {
+processing: getStreamPref("video.processing"),
+sharpness: getStreamPref("video.processing.sharpness"),
+saturation: getStreamPref("video.saturation"),
+contrast: getStreamPref("video.contrast"),
+brightness: getStreamPref("video.brightness")
+};
+STATES.currentStream.streamPlayer = new StreamPlayer(this, getStreamPref("video.player.type"), playerOptions), BxEventBus.Stream.emit("state.playing", {
+$video: this
+});
+}, nativePlay = HTMLMediaElement.prototype.play;
+HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {
+if (this.className && this.className.startsWith("XboxSplashVideo")) {
+if (PREF_SKIP_SPLASH_VIDEO) return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {});
+return nativePlay.apply(this);
+}
+let $parent = this.parentElement;
+if (!this.src && $parent.dataset.testid === "media-container") this.addEventListener("loadedmetadata", showFunc, { once: !0 });
+return nativePlay.apply(this);
+};
}
function patchRtcCodecs() {
- if (getGlobalPref("stream.video.codecProfile") === "default") return;
- if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;
+if (getGlobalPref("stream.video.codecProfile") === "default") return;
+if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype)) return !1;
}
function patchRtcPeerConnection() {
- let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
- RTCPeerConnection.prototype.createDataChannel = function() {
- let dataChannel = nativeCreateDataChannel.apply(this, arguments);
- return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;
- };
- let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile");
- if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {
- let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
- RTCPeerConnection.prototype.setLocalDescription = function(description) {
- if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);
- try {
- if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
- } catch (e) {
- BxLogger.error("setLocalDescription", e);
- }
- return nativeSetLocalDescription.apply(this, arguments);
- };
- }
- let OrgRTCPeerConnection = window.RTCPeerConnection;
- window.RTCPeerConnection = function() {
- let conn = new OrgRTCPeerConnection;
- return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {
- BxLogger.info("connectionstatechange", conn.connectionState);
- }), conn;
- };
+let nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
+RTCPeerConnection.prototype.createDataChannel = function() {
+let dataChannel = nativeCreateDataChannel.apply(this, arguments);
+return BxEventBus.Stream.emit("dataChannelCreated", { dataChannel }), dataChannel;
+};
+let maxVideoBitrateDef = getGlobalPrefDefinition("stream.video.maxBitrate"), maxVideoBitrate = getGlobalPref("stream.video.maxBitrate"), codec = getGlobalPref("stream.video.codecProfile");
+if (codec !== "default" || maxVideoBitrate < maxVideoBitrateDef.max) {
+let nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
+RTCPeerConnection.prototype.setLocalDescription = function(description) {
+if (codec !== "default") arguments[0].sdp = setCodecPreferences(arguments[0].sdp, codec);
+try {
+if (maxVideoBitrate < maxVideoBitrateDef.max && description) arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
+} catch (e) {
+BxLogger.error("setLocalDescription", e);
+}
+return nativeSetLocalDescription.apply(this, arguments);
+};
+}
+let OrgRTCPeerConnection = window.RTCPeerConnection;
+window.RTCPeerConnection = function() {
+let conn = new OrgRTCPeerConnection;
+return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {
+BxLogger.info("connectionstatechange", conn.connectionState);
+}), conn;
+};
}
function patchAudioContext() {
- let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;
- window.AudioContext = function(options) {
- if (options && options.latencyHint) options.latencyHint = 0;
- let ctx = new OrgAudioContext(options);
- return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {
- let gainNode = nativeCreateGain.apply(this);
- return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;
- }, STATES.currentStream.audioContext = ctx, ctx;
- };
+let OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;
+window.AudioContext = function(options) {
+if (options && options.latencyHint) options.latencyHint = 0;
+let ctx = new OrgAudioContext(options);
+return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {
+let gainNode = nativeCreateGain.apply(this);
+return gainNode.gain.value = getStreamPref("audio.volume") / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;
+}, STATES.currentStream.audioContext = ctx, ctx;
+};
}
function patchMeControl() {
- let overrideConfigs = {
- enableAADTelemetry: !1,
- enableTelemetry: !1,
- telEvs: "",
- oneDSUrl: ""
- }, MSA = {
- MeControl: {
- API: {
- setDisplayMode: () => {},
- setMobileState: () => {},
- addEventListener: () => {},
- removeEventListener: () => {}
- }
- }
- }, MeControl = {}, MsaHandler = {
- get(target, prop, receiver) {
- return target[prop];
- },
- set(obj, prop, value) {
- if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs);
- return obj[prop] = value, !0;
- }
- }, MeControlHandler = {
- get(target, prop, receiver) {
- return target[prop];
- },
- set(obj, prop, value) {
- if (prop === "Config") value = Object.assign(value, overrideConfigs);
- return obj[prop] = value, !0;
- }
- };
- window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);
+let overrideConfigs = {
+enableAADTelemetry: !1,
+enableTelemetry: !1,
+telEvs: "",
+oneDSUrl: ""
+}, MSA = {
+MeControl: {
+API: {
+setDisplayMode: () => {},
+setMobileState: () => {},
+addEventListener: () => {},
+removeEventListener: () => {}
+}
+}
+}, MeControl = {}, MsaHandler = {
+get(target, prop, receiver) {
+return target[prop];
+},
+set(obj, prop, value) {
+if (prop === "MeControl" && value.Config) value.Config = Object.assign(value.Config, overrideConfigs);
+return obj[prop] = value, !0;
+}
+}, MeControlHandler = {
+get(target, prop, receiver) {
+return target[prop];
+},
+set(obj, prop, value) {
+if (prop === "Config") value = Object.assign(value, overrideConfigs);
+return obj[prop] = value, !0;
+}
+};
+window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);
}
function disableAdobeAudienceManager() {
- Object.defineProperty(window, "adobe", {
- get() {
- return Object.freeze({});
- }
- });
+Object.defineProperty(window, "adobe", {
+get() {
+return Object.freeze({});
+}
+});
}
function patchCanvasContext() {
- let nativeGetContext = HTMLCanvasElement.prototype.getContext;
- HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
- if (contextType.includes("webgl")) {
- if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {
- if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power";
- }
- }
- return nativeGetContext.apply(this, [contextType, contextAttributes]);
- };
+let nativeGetContext = HTMLCanvasElement.prototype.getContext;
+HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
+if (contextType.includes("webgl")) {
+if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {
+if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance") contextAttributes.powerPreference = "low-power";
+}
+}
+return nativeGetContext.apply(this, [contextType, contextAttributes]);
+};
}
function patchPointerLockApi() {
- Object.defineProperty(document, "fullscreenElement", {
- configurable: !0,
- get() {
- return document.documentElement;
- }
- }), HTMLElement.prototype.requestFullscreen = function(options) {
- return Promise.resolve();
- };
- let pointerLockElement = null;
- Object.defineProperty(document, "pointerLockElement", {
- configurable: !0,
- get() {
- return pointerLockElement;
- }
- }), HTMLElement.prototype.requestPointerLock = function() {
- pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED));
- }, Document.prototype.exitPointerLock = function() {
- pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED));
- };
+Object.defineProperty(document, "fullscreenElement", {
+configurable: !0,
+get() {
+return document.documentElement;
+}
+}), HTMLElement.prototype.requestFullscreen = function(options) {
+return Promise.resolve();
+};
+let pointerLockElement = null;
+Object.defineProperty(document, "pointerLockElement", {
+configurable: !0,
+get() {
+return pointerLockElement;
+}
+}), HTMLElement.prototype.requestPointerLock = function() {
+pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED));
+}, Document.prototype.exitPointerLock = function() {
+pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED));
+};
}
class BaseGameBarAction {
- constructor() {}
- reset() {}
- onClick(e) {
- BxEventBus.Stream.emit("gameBar.activated", {});
- }
- render() {
- return this.$content;
- }
+constructor() {}
+reset() {}
+onClick(e) {
+BxEventBus.Stream.emit("gameBar.activated", {});
+}
+render() {
+return this.$content;
+}
}
class ScreenshotAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- this.$content = createButton({
- style: 8,
- icon: BxIcon.SCREENSHOT,
- title: t("take-screenshot"),
- onClick: this.onClick
- });
- }
- onClick = (e) => {
- super.onClick(e), ScreenshotManager.getInstance().takeScreenshot();
- };
+$content;
+constructor() {
+super();
+this.$content = createButton({
+style: 8,
+icon: BxIcon.SCREENSHOT,
+title: t("take-screenshot"),
+onClick: this.onClick
+});
+}
+onClick = (e) => {
+super.onClick(e), ScreenshotManager.getInstance().takeScreenshot();
+};
}
class TouchControlAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- let $btnEnable = createButton({
- style: 8,
- icon: BxIcon.TOUCH_CONTROL_ENABLE,
- title: t("show-touch-controller"),
- onClick: this.onClick
- }), $btnDisable = createButton({
- style: 8,
- icon: BxIcon.TOUCH_CONTROL_DISABLE,
- title: t("hide-touch-controller"),
- onClick: this.onClick,
- classes: ["bx-activated"]
- });
- this.$content = CE("div", !1, $btnEnable, $btnDisable);
- }
- onClick = (e) => {
- super.onClick(e);
- let isVisible = TouchController.toggleVisibility();
- this.$content.dataset.activated = (!isVisible).toString();
- };
- reset() {
- this.$content.dataset.activated = "false";
- }
+$content;
+constructor() {
+super();
+let $btnEnable = createButton({
+style: 8,
+icon: BxIcon.TOUCH_CONTROL_ENABLE,
+title: t("show-touch-controller"),
+onClick: this.onClick
+}), $btnDisable = createButton({
+style: 8,
+icon: BxIcon.TOUCH_CONTROL_DISABLE,
+title: t("hide-touch-controller"),
+onClick: this.onClick,
+classes: ["bx-activated"]
+});
+this.$content = CE("div", !1, $btnEnable, $btnDisable);
+}
+onClick = (e) => {
+super.onClick(e);
+let isVisible = TouchController.toggleVisibility();
+this.$content.dataset.activated = (!isVisible).toString();
+};
+reset() {
+this.$content.dataset.activated = "false";
+}
}
class MicrophoneAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- let $btnDefault = createButton({
- style: 8,
- icon: BxIcon.MICROPHONE,
- onClick: this.onClick,
- classes: ["bx-activated"]
- }), $btnMuted = createButton({
- style: 8,
- icon: BxIcon.MICROPHONE_MUTED,
- onClick: this.onClick
- });
- this.$content = CE("div", !1, $btnMuted, $btnDefault), BxEventBus.Stream.on("microphone.state.changed", (payload) => {
- let enabled = payload.state === "Enabled";
- this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone");
- });
- }
- onClick = (e) => {
- super.onClick(e);
- let enabled = MicrophoneShortcut.toggle(!1);
- this.$content.dataset.activated = enabled.toString();
- };
- reset() {
- this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false";
- }
+$content;
+constructor() {
+super();
+let $btnDefault = createButton({
+style: 8,
+icon: BxIcon.MICROPHONE,
+onClick: this.onClick,
+classes: ["bx-activated"]
+}), $btnMuted = createButton({
+style: 8,
+icon: BxIcon.MICROPHONE_MUTED,
+onClick: this.onClick
+});
+this.$content = CE("div", !1, $btnMuted, $btnDefault), BxEventBus.Stream.on("microphone.state.changed", (payload) => {
+let enabled = payload.state === "Enabled";
+this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone");
+});
+}
+onClick = (e) => {
+super.onClick(e);
+let enabled = MicrophoneShortcut.toggle(!1);
+this.$content.dataset.activated = enabled.toString();
+};
+reset() {
+this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false";
+}
}
class TrueAchievementsAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- this.$content = createButton({
- style: 8,
- icon: BxIcon.TRUE_ACHIEVEMENTS,
- onClick: this.onClick
- });
- }
- onClick = (e) => {
- super.onClick(e), TrueAchievements.getInstance().open(!1);
- };
+$content;
+constructor() {
+super();
+this.$content = createButton({
+style: 8,
+icon: BxIcon.TRUE_ACHIEVEMENTS,
+onClick: this.onClick
+});
+}
+onClick = (e) => {
+super.onClick(e), TrueAchievements.getInstance().open(!1);
+};
}
class SpeakerAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- let $btnEnable = createButton({
- style: 8,
- icon: BxIcon.AUDIO,
- onClick: this.onClick
- }), $btnMuted = createButton({
- style: 8,
- icon: BxIcon.SPEAKER_MUTED,
- onClick: this.onClick,
- classes: ["bx-activated"]
- });
- this.$content = CE("div", !1, $btnEnable, $btnMuted), BxEventBus.Stream.on("speaker.state.changed", (payload) => {
- let enabled = payload.state === 0;
- this.$content.dataset.activated = (!enabled).toString();
- });
- }
- onClick = (e) => {
- super.onClick(e), SoundShortcut.muteUnmute();
- };
- reset() {
- this.$content.dataset.activated = "false";
- }
+$content;
+constructor() {
+super();
+let $btnEnable = createButton({
+style: 8,
+icon: BxIcon.AUDIO,
+onClick: this.onClick
+}), $btnMuted = createButton({
+style: 8,
+icon: BxIcon.SPEAKER_MUTED,
+onClick: this.onClick,
+classes: ["bx-activated"]
+});
+this.$content = CE("div", !1, $btnEnable, $btnMuted), BxEventBus.Stream.on("speaker.state.changed", (payload) => {
+let enabled = payload.state === 0;
+this.$content.dataset.activated = (!enabled).toString();
+});
+}
+onClick = (e) => {
+super.onClick(e), SoundShortcut.muteUnmute();
+};
+reset() {
+this.$content.dataset.activated = "false";
+}
}
class RendererAction extends BaseGameBarAction {
- $content;
- constructor() {
- super();
- let $btnDefault = createButton({
- style: 8,
- icon: BxIcon.EYE,
- onClick: this.onClick
- }), $btnActivated = createButton({
- style: 8,
- icon: BxIcon.EYE_SLASH,
- onClick: this.onClick,
- classes: ["bx-activated"]
- });
- this.$content = CE("div", !1, $btnDefault, $btnActivated), BxEventBus.Stream.on("video.visibility.changed", (payload) => {
- this.$content.dataset.activated = (!payload.isVisible).toString();
- });
- }
- onClick = (e) => {
- super.onClick(e), RendererShortcut.toggleVisibility();
- };
- reset() {
- this.$content.dataset.activated = "false";
- }
+$content;
+constructor() {
+super();
+let $btnDefault = createButton({
+style: 8,
+icon: BxIcon.EYE,
+onClick: this.onClick
+}), $btnActivated = createButton({
+style: 8,
+icon: BxIcon.EYE_SLASH,
+onClick: this.onClick,
+classes: ["bx-activated"]
+});
+this.$content = CE("div", !1, $btnDefault, $btnActivated), BxEventBus.Stream.on("video.visibility.changed", (payload) => {
+this.$content.dataset.activated = (!payload.isVisible).toString();
+});
+}
+onClick = (e) => {
+super.onClick(e), RendererShortcut.toggleVisibility();
+};
+reset() {
+this.$content.dataset.activated = "false";
+}
}
class GameBar {
- static instance;
- static getInstance() {
- if (typeof GameBar.instance === "undefined") if (getGlobalPref("gameBar.position") !== "off") GameBar.instance = new GameBar;
- else GameBar.instance = null;
- return GameBar.instance;
- }
- LOG_TAG = "GameBar";
- static VISIBLE_DURATION = 2000;
- $gameBar;
- $container;
- timeoutId = null;
- actions = [];
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- let $container, position = getGlobalPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT));
- if (this.actions = [
- new ScreenshotAction,
- ...STATES.userAgent.capabilities.touch && getGlobalPref("touchController.mode") !== "off" ? [new TouchControlAction] : [],
- new SpeakerAction,
- new RendererAction,
- new MicrophoneAction,
- new TrueAchievementsAction
- ], position === "bottom-right")
- this.actions.reverse();
- for (let action of this.actions)
- $container.appendChild(action.render());
- $gameBar.addEventListener("click", (e) => {
- if (e.target !== $gameBar) return;
- $container.classList.contains("bx-show") ? this.hideBar() : this.showBar();
- }), BxEventBus.Stream.on("gameBar.activated", this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => {
- $container.classList.replace("bx-hide", "bx-offscreen");
- }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => {
- if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable();
- }).bind(this));
- }
- beginHideTimeout = () => {
- this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => {
- this.timeoutId = null, this.hideBar();
- }, GameBar.VISIBLE_DURATION);
- };
- clearHideTimeout = () => {
- this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null;
- };
- enable() {
- this.$gameBar.classList.remove("bx-gone");
- }
- disable() {
- this.hideBar(), this.$gameBar.classList.add("bx-gone");
- }
- showBar() {
- this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout();
- }
- hideBar = () => {
- this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide");
- };
- reset() {
- for (let action of this.actions)
- action.reset();
- }
+static instance;
+static getInstance() {
+if (typeof GameBar.instance === "undefined") if (getGlobalPref("gameBar.position") !== "off") GameBar.instance = new GameBar;
+else GameBar.instance = null;
+return GameBar.instance;
+}
+LOG_TAG = "GameBar";
+static VISIBLE_DURATION = 2000;
+$gameBar;
+$container;
+timeoutId = null;
+actions = [];
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+let $container, position = getGlobalPref("gameBar.position"), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT));
+if (this.actions = [
+new ScreenshotAction,
+...STATES.userAgent.capabilities.touch && getGlobalPref("touchController.mode") !== "off" ? [new TouchControlAction] : [],
+new SpeakerAction,
+new RendererAction,
+new MicrophoneAction,
+new TrueAchievementsAction
+], position === "bottom-right")
+this.actions.reverse();
+for (let action of this.actions)
+$container.appendChild(action.render());
+$gameBar.addEventListener("click", (e) => {
+if (e.target !== $gameBar) return;
+$container.classList.contains("bx-show") ? this.hideBar() : this.showBar();
+}), BxEventBus.Stream.on("gameBar.activated", this.hideBar), $container.addEventListener("pointerover", this.clearHideTimeout), $container.addEventListener("pointerout", this.beginHideTimeout), $container.addEventListener("transitionend", (e) => {
+$container.classList.replace("bx-hide", "bx-offscreen");
+}), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, position !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => {
+if (STATES.isPlaying) window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none" ? this.disable() : this.enable();
+}).bind(this));
+}
+beginHideTimeout = () => {
+this.clearHideTimeout(), this.timeoutId = window.setTimeout(() => {
+this.timeoutId = null, this.hideBar();
+}, GameBar.VISIBLE_DURATION);
+};
+clearHideTimeout = () => {
+this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null;
+};
+enable() {
+this.$gameBar.classList.remove("bx-gone");
+}
+disable() {
+this.hideBar(), this.$gameBar.classList.add("bx-gone");
+}
+showBar() {
+this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout();
+}
+hideBar = () => {
+this.clearHideTimeout(), this.$container.classList.replace("bx-show", "bx-hide");
+};
+reset() {
+for (let action of this.actions)
+action.reset();
+}
}
class XcloudApi {
- static instance;
- static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi);
- LOG_TAG = "XcloudApi";
- CACHE_TITLES = {};
- CACHE_WAIT_TIME = {};
- constructor() {
- BxLogger.info(this.LOG_TAG, "constructor()");
- }
- async getTitleInfo(id) {
- if (id in this.CACHE_TITLES) return this.CACHE_TITLES[id];
- let baseUri = STATES.selectedRegion.baseUri;
- if (!baseUri || !STATES.gsToken) return;
- let json;
- try {
- json = (await (await NATIVE_FETCH(`${baseUri}/v2/titles`, {
- method: "POST",
- headers: {
- Authorization: `Bearer ${STATES.gsToken}`,
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- alternateIds: [id],
- alternateIdType: "productId"
- })
- })).json()).results[0];
- } catch (e) {
- json = {};
- }
- return this.CACHE_TITLES[id] = json, json;
- }
- async getWaitTime(id) {
- if (id in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id];
- let baseUri = STATES.selectedRegion.baseUri;
- if (!baseUri || !STATES.gsToken) return null;
- let json;
- try {
- json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, {
- method: "GET",
- headers: {
- Authorization: `Bearer ${STATES.gsToken}`
- }
- })).json();
- } catch (e) {
- json = {};
- }
- return this.CACHE_WAIT_TIME[id] = json, json;
- }
+static instance;
+static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi);
+LOG_TAG = "XcloudApi";
+CACHE_TITLES = {};
+CACHE_WAIT_TIME = {};
+constructor() {
+BxLogger.info(this.LOG_TAG, "constructor()");
+}
+async getTitleInfo(id) {
+if (id in this.CACHE_TITLES) return this.CACHE_TITLES[id];
+let baseUri = STATES.selectedRegion.baseUri;
+if (!baseUri || !STATES.gsToken) return;
+let json;
+try {
+json = (await (await NATIVE_FETCH(`${baseUri}/v2/titles`, {
+method: "POST",
+headers: {
+Authorization: `Bearer ${STATES.gsToken}`,
+"Content-Type": "application/json"
+},
+body: JSON.stringify({
+alternateIds: [id],
+alternateIdType: "productId"
+})
+})).json()).results[0];
+} catch (e) {
+json = {};
+}
+return this.CACHE_TITLES[id] = json, json;
+}
+async getWaitTime(id) {
+if (id in this.CACHE_WAIT_TIME) return this.CACHE_WAIT_TIME[id];
+let baseUri = STATES.selectedRegion.baseUri;
+if (!baseUri || !STATES.gsToken) return null;
+let json;
+try {
+json = await (await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, {
+method: "GET",
+headers: {
+Authorization: `Bearer ${STATES.gsToken}`
+}
+})).json();
+} catch (e) {
+json = {};
+}
+return this.CACHE_WAIT_TIME[id] = json, json;
+}
}
class GameTile {
- static timeoutId;
- static async showWaitTime($elm, productId) {
- if ($elm.hasWaitTime) return;
- $elm.hasWaitTime = !0;
- let totalWaitTime, api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId);
- if (info) {
- let waitTime = await api.getWaitTime(info.titleId);
- if (waitTime) totalWaitTime = waitTime.estimatedAllocationTimeInSeconds;
- }
- if (typeof totalWaitTime === "number" && isElementVisible($elm)) {
- let $div = CE("div", { class: "bx-game-tile-wait-time" }, createSvgIcon(BxIcon.PLAYTIME), CE("span", !1, totalWaitTime < 60 ? totalWaitTime + "s" : secondsToHm(totalWaitTime))), duration = totalWaitTime >= 900 ? "long" : totalWaitTime >= 600 ? "medium" : totalWaitTime >= 300 ? "short" : "";
- if (duration) $div.dataset.duration = duration;
- $elm.insertAdjacentElement("afterbegin", $div);
- }
- }
- static requestWaitTime($elm, productId) {
- GameTile.timeoutId && clearTimeout(GameTile.timeoutId), GameTile.timeoutId = window.setTimeout(async () => {
- GameTile.showWaitTime($elm, productId);
- }, 500);
- }
- static findProductId($elm) {
- let productId = null;
- try {
- if ($elm.tagName === "BUTTON" && $elm.className.includes("MruGameCard") || $elm.tagName === "A" && $elm.className.includes("GameCard")) {
- let props = getReactProps($elm.parentElement);
- if (Array.isArray(props.children)) productId = props.children[0].props.productId;
- else productId = props.children.props.productId;
- } else if ($elm.tagName === "A" && $elm.className.includes("GameItem")) {
- let props = getReactProps($elm.parentElement);
- if (props = props.children.props, props.location !== "NonStreamableGameItem") if ("productId" in props) productId = props.productId;
- else productId = props.children.props.productId;
- }
- } catch (e) {}
- return productId;
- }
- static setup() {
- window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, (e) => {
- let $elm = e.element;
- if (($elm.className || "").includes("MruGameCard")) {
- let $ol = $elm.closest("ol");
- if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => {
- let productId = GameTile.findProductId($elm2);
- productId && GameTile.showWaitTime($elm2, productId);
- });
- } else {
- let productId = GameTile.findProductId($elm);
- productId && GameTile.requestWaitTime($elm, productId);
- }
- });
- }
+static timeoutId;
+static async showWaitTime($elm, productId) {
+if ($elm.hasWaitTime) return;
+$elm.hasWaitTime = !0;
+let totalWaitTime, api = XcloudApi.getInstance(), info = await api.getTitleInfo(productId);
+if (info) {
+let waitTime = await api.getWaitTime(info.titleId);
+if (waitTime) totalWaitTime = waitTime.estimatedAllocationTimeInSeconds;
+}
+if (typeof totalWaitTime === "number" && isElementVisible($elm)) {
+let $div = CE("div", { class: "bx-game-tile-wait-time" }, createSvgIcon(BxIcon.PLAYTIME), CE("span", !1, totalWaitTime < 60 ? totalWaitTime + "s" : secondsToHm(totalWaitTime))), duration = totalWaitTime >= 900 ? "long" : totalWaitTime >= 600 ? "medium" : totalWaitTime >= 300 ? "short" : "";
+if (duration) $div.dataset.duration = duration;
+$elm.insertAdjacentElement("afterbegin", $div);
+}
+}
+static requestWaitTime($elm, productId) {
+GameTile.timeoutId && clearTimeout(GameTile.timeoutId), GameTile.timeoutId = window.setTimeout(async () => {
+GameTile.showWaitTime($elm, productId);
+}, 500);
+}
+static findProductId($elm) {
+let productId = null;
+try {
+if ($elm.tagName === "BUTTON" && $elm.className.includes("MruGameCard") || $elm.tagName === "A" && $elm.className.includes("GameCard")) {
+let props = getReactProps($elm.parentElement);
+if (Array.isArray(props.children)) productId = props.children[0].props.productId;
+else productId = props.children.props.productId;
+} else if ($elm.tagName === "A" && $elm.className.includes("GameItem")) {
+let props = getReactProps($elm.parentElement);
+if (props = props.children.props, props.location !== "NonStreamableGameItem") if ("productId" in props) productId = props.productId;
+else productId = props.children.props.productId;
+}
+} catch (e) {}
+return productId;
+}
+static setup() {
+window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, (e) => {
+let $elm = e.element;
+if (($elm.className || "").includes("MruGameCard")) {
+let $ol = $elm.closest("ol");
+if ($ol && !$ol.hasWaitTime) $ol.hasWaitTime = !0, $ol.querySelectorAll("button[class*=MruGameCard]").forEach(($elm2) => {
+let productId = GameTile.findProductId($elm2);
+productId && GameTile.showWaitTime($elm2, productId);
+});
+} else {
+let productId = GameTile.findProductId($elm);
+productId && GameTile.requestWaitTime($elm, productId);
+}
+});
+}
}
class ProductDetailsPage {
- static $btnShortcut = AppInterface && createButton({
- icon: BxIcon.CREATE_SHORTCUT,
- label: t("create-shortcut"),
- style: 64,
- onClick: (e) => {
- AppInterface.createShortcut(window.location.pathname.substring(6));
- }
- });
- static $btnWallpaper = AppInterface && createButton({
- icon: BxIcon.DOWNLOAD,
- label: t("wallpaper"),
- style: 64,
- onClick: (e) => {
- let details = parseDetailsPath(window.location.pathname);
- details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
- }
- });
- static injectTimeoutId = null;
- static injectButtons() {
- ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => {
- let $inputsContainer = document.querySelector('div[class*="Header-module__gamePassAndInputsContainer"]');
- if ($inputsContainer && !$inputsContainer.dataset.bxInjected) {
- $inputsContainer.dataset.bxInjected = "true";
- let { productId } = parseDetailsPath(window.location.pathname);
- if (LocalCoOpManager.getInstance().isSupported(productId || "")) $inputsContainer.insertAdjacentElement("afterend", CE("div", {
- class: "bx-product-details-icons bx-frosted"
- }, createSvgIcon(BxIcon.LOCAL_CO_OP), t("local-co-op")));
- }
- if (AppInterface) {
- let $container = document.querySelector("div[class*=ActionButtons-module__container]");
- if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", {
- class: "bx-product-details-buttons"
- }, ["android-handheld", "android"].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper));
- }
- }, 500);
- }
+static $btnShortcut = AppInterface && createButton({
+icon: BxIcon.CREATE_SHORTCUT,
+label: t("create-shortcut"),
+style: 64,
+onClick: (e) => {
+AppInterface.createShortcut(window.location.pathname.substring(6));
+}
+});
+static $btnWallpaper = AppInterface && createButton({
+icon: BxIcon.DOWNLOAD,
+label: t("wallpaper"),
+style: 64,
+onClick: (e) => {
+let details = parseDetailsPath(window.location.pathname);
+details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
+}
+});
+static injectTimeoutId = null;
+static injectButtons() {
+ProductDetailsPage.injectTimeoutId && clearTimeout(ProductDetailsPage.injectTimeoutId), ProductDetailsPage.injectTimeoutId = window.setTimeout(() => {
+let $inputsContainer = document.querySelector('div[class*="Header-module__gamePassAndInputsContainer"]');
+if ($inputsContainer && !$inputsContainer.dataset.bxInjected) {
+$inputsContainer.dataset.bxInjected = "true";
+let { productId } = parseDetailsPath(window.location.pathname);
+if (LocalCoOpManager.getInstance().isSupported(productId || "")) $inputsContainer.insertAdjacentElement("afterend", CE("div", {
+class: "bx-product-details-icons bx-frosted"
+}, createSvgIcon(BxIcon.LOCAL_CO_OP), t("local-co-op")));
+}
+if (AppInterface) {
+let $container = document.querySelector("div[class*=ActionButtons-module__container]");
+if ($container && $container.parentElement) $container.parentElement.appendChild(CE("div", {
+class: "bx-product-details-buttons"
+}, ["android-handheld", "android"].includes(BX_FLAGS.DeviceInfo.deviceType) && ProductDetailsPage.$btnShortcut, ProductDetailsPage.$btnWallpaper));
+}
+}, 500);
+}
}
class StreamUiHandler {
- static $btnStreamSettings;
- static $btnStreamStats;
- static $btnRefresh;
- static $btnHome;
- static observer;
- static cloneStreamHudButton($btnOrg, label, svgIcon) {
- if (!$btnOrg) return null;
- let $container = $btnOrg.cloneNode(!0), timeout;
- if (STATES.browser.capabilities.touch) {
- let onTransitionStart = (e) => {
- if (e.propertyName !== "opacity") return;
- timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none";
- }, onTransitionEnd = (e) => {
- if (e.propertyName !== "opacity") return;
- let $streamHud = e.target.closest("#StreamHud");
- if (!$streamHud) return;
- if ($streamHud.style.left === "0px") {
- let $target = e.target;
- timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {
- $target.style.pointerEvents = "auto";
- }, 100);
- }
- };
- $container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);
- }
- let $button = $container.querySelector("button");
- if (!$button) return null;
- $button.setAttribute("title", label);
- let $orgSvg = $button.querySelector("svg");
- if (!$orgSvg) return null;
- let $svg = createSvgIcon(svgIcon);
- return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;
- }
- static cloneCloseButton($btnOrg, icon, className, onChange) {
- if (!$btnOrg) return null;
- let $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);
- return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;
- }
- static async handleStreamMenu() {
- let $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
- if (!$btnCloseHud) return;
- let { $btnRefresh, $btnHome } = StreamUiHandler;
- if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {
- confirm(t("confirm-reload-stream")) && window.location.reload();
- });
- if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {
- confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
- });
- if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome);
- document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render());
- }
- static handleSystemMenu($streamHud) {
- let $orgButton = $streamHud.querySelector("div[class^=HUDButton]");
- if (!$orgButton) return;
- let hideGripHandle = () => {
- let $gripHandle = document.querySelector("#StreamHud button[class^=GripHandle]");
- if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();
- }, $btnStreamSettings = StreamUiHandler.$btnStreamSettings;
- if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => {
- hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show();
- }), StreamUiHandler.$btnStreamSettings = $btnStreamSettings;
- let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats;
- if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => {
- hideGripHandle(), e.preventDefault(), await streamStats.toggle();
- let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
- $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);
- }), StreamUiHandler.$btnStreamStats = $btnStreamStats;
- let $btnParent = $orgButton.parentElement;
- if ($btnStreamSettings && $btnStreamStats) {
- let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
- $btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);
- }
- let $dotsButton = $btnParent.lastElementChild;
- $dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);
- }
- static reset() {
- StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0;
- }
- static observe() {
- StreamUiHandler.reset();
- let $screen = document.querySelector("#PageContent section[class*=PureScreens]");
- if (!$screen) return;
- let observer = new MutationObserver((mutationList) => {
- let item2;
- for (item2 of mutationList) {
- if (item2.type !== "childList") continue;
- item2.addedNodes.forEach(async ($node) => {
- if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return;
- let $elm = $node;
- if (!($elm instanceof HTMLElement)) return;
- let className = $elm.className || "";
- if (className.includes("PureErrorPage")) {
- BxEventBus.Stream.emit("state.error", {});
- return;
- }
- if (className.startsWith("StreamMenu-module__container")) {
- StreamUiHandler.handleStreamMenu();
- return;
- }
- if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud");
- if (!$elm || ($elm.id || "") !== "StreamHud") return;
- StreamUiHandler.handleSystemMenu($elm);
- });
- }
- });
- observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer;
- }
+static $btnStreamSettings;
+static $btnStreamStats;
+static $btnRefresh;
+static $btnHome;
+static observer;
+static cloneStreamHudButton($btnOrg, label, svgIcon) {
+if (!$btnOrg) return null;
+let $container = $btnOrg.cloneNode(!0), timeout;
+if (STATES.browser.capabilities.touch) {
+let onTransitionStart = (e) => {
+if (e.propertyName !== "opacity") return;
+timeout && clearTimeout(timeout), e.target.style.pointerEvents = "none";
+}, onTransitionEnd = (e) => {
+if (e.propertyName !== "opacity") return;
+let $streamHud = e.target.closest("#StreamHud");
+if (!$streamHud) return;
+if ($streamHud.style.left === "0px") {
+let $target = e.target;
+timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {
+$target.style.pointerEvents = "auto";
+}, 100);
+}
+};
+$container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);
+}
+let $button = $container.querySelector("button");
+if (!$button) return null;
+$button.setAttribute("title", label);
+let $orgSvg = $button.querySelector("svg");
+if (!$orgSvg) return null;
+let $svg = createSvgIcon(svgIcon);
+return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;
+}
+static cloneCloseButton($btnOrg, icon, className, onChange) {
+if (!$btnOrg) return null;
+let $btn = $btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);
+return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;
+}
+static async handleStreamMenu() {
+let $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
+if (!$btnCloseHud) return;
+let { $btnRefresh, $btnHome } = StreamUiHandler;
+if (typeof $btnRefresh === "undefined") $btnRefresh = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {
+confirm(t("confirm-reload-stream")) && window.location.reload();
+});
+if (typeof $btnHome === "undefined") $btnHome = StreamUiHandler.cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {
+confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
+});
+if ($btnRefresh && $btnHome) $btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome);
+document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render());
+}
+static handleSystemMenu($streamHud) {
+let $orgButton = $streamHud.querySelector("div[class^=HUDButton]");
+if (!$orgButton) return;
+let hideGripHandle = () => {
+let $gripHandle = document.querySelector("#StreamHud button[class^=GripHandle]");
+if ($gripHandle && $gripHandle.ariaExpanded === "true") $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();
+}, $btnStreamSettings = StreamUiHandler.$btnStreamSettings;
+if (typeof $btnStreamSettings === "undefined") $btnStreamSettings = StreamUiHandler.cloneStreamHudButton($orgButton, t("better-xcloud"), BxIcon.BETTER_XCLOUD), $btnStreamSettings?.addEventListener("click", (e) => {
+hideGripHandle(), e.preventDefault(), SettingsDialog.getInstance().show();
+}), StreamUiHandler.$btnStreamSettings = $btnStreamSettings;
+let streamStats = StreamStats.getInstance(), $btnStreamStats = StreamUiHandler.$btnStreamStats;
+if (typeof $btnStreamStats === "undefined") $btnStreamStats = StreamUiHandler.cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats?.addEventListener("click", async (e) => {
+hideGripHandle(), e.preventDefault(), await streamStats.toggle();
+let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
+$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);
+}), StreamUiHandler.$btnStreamStats = $btnStreamStats;
+let $btnParent = $orgButton.parentElement;
+if ($btnStreamSettings && $btnStreamStats) {
+let btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
+$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);
+}
+let $dotsButton = $btnParent.lastElementChild;
+$dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);
+}
+static reset() {
+StreamUiHandler.$btnStreamSettings = void 0, StreamUiHandler.$btnStreamStats = void 0, StreamUiHandler.$btnRefresh = void 0, StreamUiHandler.$btnHome = void 0, StreamUiHandler.observer && StreamUiHandler.observer.disconnect(), StreamUiHandler.observer = void 0;
+}
+static observe() {
+StreamUiHandler.reset();
+let $screen = document.querySelector("#PageContent section[class*=PureScreens]");
+if (!$screen) return;
+let observer = new MutationObserver((mutationList) => {
+let item2;
+for (item2 of mutationList) {
+if (item2.type !== "childList") continue;
+item2.addedNodes.forEach(async ($node) => {
+if (!$node || $node.nodeType !== Node.ELEMENT_NODE) return;
+let $elm = $node;
+if (!($elm instanceof HTMLElement)) return;
+let className = $elm.className || "";
+if (className.includes("PureErrorPage")) {
+BxEventBus.Stream.emit("state.error", {});
+return;
+}
+if (className.startsWith("StreamMenu-module__container")) {
+StreamUiHandler.handleStreamMenu();
+return;
+}
+if (className.startsWith("Overlay-module_") || className.startsWith("InProgressScreen")) $elm = $elm.querySelector("#StreamHud");
+if (!$elm || ($elm.id || "") !== "StreamHud") return;
+StreamUiHandler.handleSystemMenu($elm);
+});
+}
+});
+observer.observe($screen, { subtree: !0, childList: !0 }), StreamUiHandler.observer = observer;
+}
}
class RootDialogObserver {
- static $btnShortcut = AppInterface && createButton({
- icon: BxIcon.CREATE_SHORTCUT,
- label: t("create-shortcut"),
- style: 64 | 8 | 128 | 4096 | 8192,
- onClick: (e) => {
- window.BX_EXPOSED.dialogRoutes?.closeAll();
- let $btn = e.target.closest("button");
- AppInterface.createShortcut($btn?.dataset.path);
- }
- });
- static $btnWallpaper = AppInterface && createButton({
- icon: BxIcon.DOWNLOAD,
- label: t("wallpaper"),
- style: 64 | 8 | 128 | 4096 | 8192,
- onClick: (e) => {
- window.BX_EXPOSED.dialogRoutes?.closeAll();
- let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);
- details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
- }
- });
- static handleGameCardMenu($root) {
- let $detail = $root.querySelector('a[href^="/play/"]');
- if (!$detail) return;
- let path = $detail.getAttribute("href");
- RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
- }
- static handleAddedElement($root, $addedElm) {
- if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {
- let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");
- if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;
- } else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;
- return !1;
- }
- static observe($root) {
- let beingShown = !1;
- new MutationObserver((mutationList) => {
- for (let mutation of mutationList) {
- if (mutation.type !== "childList") continue;
- if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
- let $addedElm = mutation.addedNodes[0];
- if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
- }
- let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
- if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});
- }
- }).observe($root, { subtree: !0, childList: !0 });
- }
- static waitForRootDialog() {
- let observer = new MutationObserver((mutationList) => {
- for (let mutation of mutationList) {
- if (mutation.type !== "childList") continue;
- let $target = mutation.target;
- if ($target.id && $target.id === "gamepass-dialog-root") {
- observer.disconnect(), RootDialogObserver.observe($target);
- break;
- }
- }
- });
- observer.observe(document.documentElement, { subtree: !0, childList: !0 });
- }
+static $btnShortcut = AppInterface && createButton({
+icon: BxIcon.CREATE_SHORTCUT,
+label: t("create-shortcut"),
+style: 64 | 8 | 128 | 4096 | 8192,
+onClick: (e) => {
+window.BX_EXPOSED.dialogRoutes?.closeAll();
+let $btn = e.target.closest("button");
+AppInterface.createShortcut($btn?.dataset.path);
+}
+});
+static $btnWallpaper = AppInterface && createButton({
+icon: BxIcon.DOWNLOAD,
+label: t("wallpaper"),
+style: 64 | 8 | 128 | 4096 | 8192,
+onClick: (e) => {
+window.BX_EXPOSED.dialogRoutes?.closeAll();
+let $btn = e.target.closest("button"), details = parseDetailsPath($btn.dataset.path);
+details && AppInterface.downloadWallpapers(details.titleSlug, details.productId);
+}
+});
+static handleGameCardMenu($root) {
+let $detail = $root.querySelector('a[href^="/play/"]');
+if (!$detail) return;
+let path = $detail.getAttribute("href");
+RootDialogObserver.$btnShortcut.dataset.path = path, RootDialogObserver.$btnWallpaper.dataset.path = path, $root.append(RootDialogObserver.$btnShortcut, RootDialogObserver.$btnWallpaper);
+}
+static handleAddedElement($root, $addedElm) {
+if (AppInterface && $addedElm.className.startsWith("SlideSheet-module__container")) {
+let $gameCardMenu = $addedElm.querySelector("div[class^=MruContextMenu],div[class^=GameCardContextMenu]");
+if ($gameCardMenu) return RootDialogObserver.handleGameCardMenu($gameCardMenu), !0;
+} else if ($root.querySelector("div[class*=GuideDialog]")) return GuideMenu.getInstance().observe($addedElm), !0;
+return !1;
+}
+static observe($root) {
+let beingShown = !1;
+new MutationObserver((mutationList) => {
+for (let mutation of mutationList) {
+if (mutation.type !== "childList") continue;
+if (BX_FLAGS.Debug && BxLogger.warning("RootDialog", "added", mutation.addedNodes), mutation.addedNodes.length === 1) {
+let $addedElm = mutation.addedNodes[0];
+if ($addedElm instanceof HTMLElement) RootDialogObserver.handleAddedElement($root, $addedElm);
+}
+let shown = !!($root.firstElementChild && $root.firstElementChild.childElementCount > 0);
+if (shown !== beingShown) beingShown = shown, BxEventBus.Script.emit(shown ? "dialog.shown" : "dialog.dismissed", {});
+}
+}).observe($root, { subtree: !0, childList: !0 });
+}
+static waitForRootDialog() {
+let observer = new MutationObserver((mutationList) => {
+for (let mutation of mutationList) {
+if (mutation.type !== "childList") continue;
+let $target = mutation.target;
+if ($target.id && $target.id === "gamepass-dialog-root") {
+observer.disconnect(), RootDialogObserver.observe($target);
+break;
+}
+}
+});
+observer.observe(document.documentElement, { subtree: !0, childList: !0 });
+}
}
class KeyboardShortcutHandler {
- static instance;
- static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler);
- start() {
- window.addEventListener("keydown", this.onKeyDown);
- }
- stop() {
- window.removeEventListener("keydown", this.onKeyDown);
- }
- onKeyDown = (e) => {
- if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
- if (e.repeat) return;
- let fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);
- if (!fullKeyCode) return;
- let action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];
- if (action) e.preventDefault(), e.stopPropagation(), ShortcutHandler.runAction(action);
- };
+static instance;
+static getInstance = () => KeyboardShortcutHandler.instance ?? (KeyboardShortcutHandler.instance = new KeyboardShortcutHandler);
+start() {
+window.addEventListener("keydown", this.onKeyDown);
+}
+stop() {
+window.removeEventListener("keydown", this.onKeyDown);
+}
+onKeyDown = (e) => {
+if (window.BX_STREAM_SETTINGS.xCloudPollingMode !== "none") return;
+if (e.repeat) return;
+let fullKeyCode = KeyHelper.getFullKeyCodeFromEvent(e);
+if (!fullKeyCode) return;
+let action = window.BX_STREAM_SETTINGS.keyboardShortcuts?.[fullKeyCode];
+if (action) e.preventDefault(), e.stopPropagation(), ShortcutHandler.runAction(action);
+};
}
var VIBRATION_DATA_MAP = {
- gamepadIndex: 8,
- leftMotorPercent: 8,
- rightMotorPercent: 8,
- leftTriggerMotorPercent: 8,
- rightTriggerMotorPercent: 8,
- durationMs: 16
+gamepadIndex: 8,
+leftMotorPercent: 8,
+rightMotorPercent: 8,
+leftTriggerMotorPercent: 8,
+rightTriggerMotorPercent: 8,
+durationMs: 16
};
class DeviceVibrationManager {
- static instance;
- static getInstance() {
- if (typeof DeviceVibrationManager.instance === "undefined") if (STATES.browser.capabilities.deviceVibration) DeviceVibrationManager.instance = new DeviceVibrationManager;
- else DeviceVibrationManager.instance = null;
- return DeviceVibrationManager.instance;
- }
- dataChannel = null;
- boundOnMessage;
- constructor() {
- this.boundOnMessage = this.onMessage.bind(this), BxEventBus.Stream.on("dataChannelCreated", (payload) => {
- let { dataChannel } = payload;
- if (dataChannel?.label === "input") this.reset(), this.dataChannel = dataChannel, this.setupDataChannel();
- }), BxEventBus.Stream.on("deviceVibration.updated", () => this.setupDataChannel());
- }
- setupDataChannel() {
- if (!this.dataChannel) return;
- if (this.removeEventListeners(), window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) this.dataChannel.addEventListener("message", this.boundOnMessage);
- }
- playVibration(data) {
- let vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity;
- if (AppInterface) {
- AppInterface.vibrate(JSON.stringify(data), vibrationIntensity);
- return;
- }
- let realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity;
- if (realIntensity === 0 || realIntensity === 100) {
- window.navigator.vibrate(realIntensity ? data.durationMs : 0);
- return;
- }
- let pulseDuration = 200, onDuration = Math.floor(pulseDuration * realIntensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat();
- window.navigator.vibrate(pulses);
- }
- onMessage(e) {
- if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return;
- let dataView = new DataView(e.data), offset = 0, messageType;
- if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
- else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
- if (!(messageType & 128)) return;
- let vibrationType = dataView.getUint8(offset);
- if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return;
- let data = {}, key;
- for (key in VIBRATION_DATA_MAP)
- if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
- else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
- this.playVibration(data);
- }
- removeEventListeners() {
- try {
- this.dataChannel?.removeEventListener("message", this.boundOnMessage);
- } catch (e) {}
- }
- reset() {
- this.removeEventListeners(), this.dataChannel = null;
- }
+static instance;
+static getInstance() {
+if (typeof DeviceVibrationManager.instance === "undefined") if (STATES.browser.capabilities.deviceVibration) DeviceVibrationManager.instance = new DeviceVibrationManager;
+else DeviceVibrationManager.instance = null;
+return DeviceVibrationManager.instance;
+}
+dataChannel = null;
+boundOnMessage;
+constructor() {
+this.boundOnMessage = this.onMessage.bind(this), BxEventBus.Stream.on("dataChannelCreated", (payload) => {
+let { dataChannel } = payload;
+if (dataChannel?.label === "input") this.reset(), this.dataChannel = dataChannel, this.setupDataChannel();
+}), BxEventBus.Stream.on("deviceVibration.updated", () => this.setupDataChannel());
+}
+setupDataChannel() {
+if (!this.dataChannel) return;
+if (this.removeEventListeners(), window.BX_STREAM_SETTINGS.deviceVibrationIntensity > 0) this.dataChannel.addEventListener("message", this.boundOnMessage);
+}
+playVibration(data) {
+let vibrationIntensity = StreamSettings.settings.deviceVibrationIntensity;
+if (AppInterface) {
+AppInterface.vibrate(JSON.stringify(data), vibrationIntensity);
+return;
+}
+let realIntensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * vibrationIntensity;
+if (realIntensity === 0 || realIntensity === 100) {
+window.navigator.vibrate(realIntensity ? data.durationMs : 0);
+return;
+}
+let pulseDuration = 200, onDuration = Math.floor(pulseDuration * realIntensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat();
+window.navigator.vibrate(pulses);
+}
+onMessage(e) {
+if (typeof e !== "object" || !(e.data instanceof ArrayBuffer)) return;
+let dataView = new DataView(e.data), offset = 0, messageType;
+if (dataView.byteLength === 13) messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
+else messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
+if (!(messageType & 128)) return;
+let vibrationType = dataView.getUint8(offset);
+if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0) return;
+let data = {}, key;
+for (key in VIBRATION_DATA_MAP)
+if (VIBRATION_DATA_MAP[key] === 16) data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
+else data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
+this.playVibration(data);
+}
+removeEventListeners() {
+try {
+this.dataChannel?.removeEventListener("message", this.boundOnMessage);
+} catch (e) {}
+}
+reset() {
+this.removeEventListeners(), this.dataChannel = null;
+}
}
SettingsManager.getInstance();
if (window.location.pathname.includes("/auth/msa")) {
- let nativePushState = window.history.pushState;
- throw window.history.pushState = function(...args) {
- let url = args[2];
- if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) {
- console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url;
- return;
- }
- return nativePushState.apply(this, arguments);
- }, new Error("[Better xCloud] Refreshing the page after logging in");
+let nativePushState = window.history.pushState;
+throw window.history.pushState = function(...args) {
+let url = args[2];
+if (url && (url.startsWith("/play") || url.substring(6).startsWith("/play"))) {
+console.log("Redirecting to xbox.com/play"), window.stop(), window.location.href = "https://www.xbox.com" + url;
+return;
+}
+return nativePushState.apply(this, arguments);
+}, new Error("[Better xCloud] Refreshing the page after logging in");
}
BxLogger.info("readyState", document.readyState);
if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") {
- window.stop();
- let css = "";
- css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}';
- let isSafari = UserAgent.isSafari(), $secondaryAction;
- if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading"));
- else $secondaryAction = CE("a", {
- href: "https://better-xcloud.github.io/troubleshooting",
- target: "_blank"
- }, "🤓 " + t("how-to-fix"));
- let $fragment = document.createDocumentFragment();
- throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", {
- class: "bx-reload-overlay"
- }, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari");
+window.stop();
+let css = "";
+css += '.bx-reload-overlay{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;background:rgba(0,0,0,0.8);z-index:9999;color:#fff;text-align:center;font-weight:400;font-family:"Segoe UI",Arial,Helvetica,sans-serif;font-size:1.3rem}.bx-reload-overlay *:focus{outline:none !important}.bx-reload-overlay > div{margin:0 auto}.bx-reload-overlay a{text-decoration:none;display:inline-block;background:#107c10;color:#fff;border-radius:4px;padding:6px}';
+let isSafari = UserAgent.isSafari(), $secondaryAction;
+if (isSafari) $secondaryAction = CE("p", !1, t("settings-reloading"));
+else $secondaryAction = CE("a", {
+href: "https://better-xcloud.github.io/troubleshooting",
+target: "_blank"
+}, "🤓 " + t("how-to-fix"));
+let $fragment = document.createDocumentFragment();
+throw $fragment.appendChild(CE("style", !1, css)), $fragment.appendChild(CE("div", {
+class: "bx-reload-overlay"
+}, CE("div", !1, CE("p", !1, t("load-failed-message")), $secondaryAction))), document.documentElement.appendChild($fragment), isSafari && window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari");
}
window.addEventListener("load", (e) => {
- window.setTimeout(() => {
- if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0);
- }, 3000);
+window.setTimeout(() => {
+if (document.body.classList.contains("legacyBackground")) window.stop(), window.location.reload(!0);
+}, 3000);
});
document.addEventListener("readystatechange", (e) => {
- if (document.readyState !== "interactive") return;
- if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize();
- else window.setTimeout(HeaderSection.watchHeader, 2000);
- if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {
- let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");
- $parent && ($parent.style.display = "none");
- }
- preloadFonts();
+if (document.readyState !== "interactive") return;
+if (STATES.isSignedIn = !!window.xbcUser?.isSignedIn, STATES.isSignedIn) RemotePlayManager.getInstance()?.initialize();
+else window.setTimeout(HeaderSection.watchHeader, 2000);
+if (getGlobalPref("ui.hideSections").includes("friends") || getGlobalPref("block.features").includes("friends")) {
+let $parent = document.querySelector("div[class*=PlayWithFriendsSkeleton]")?.closest("div[class*=HomePage-module]");
+$parent && ($parent.style.display = "none");
+}
+preloadFonts();
});
window.BX_EXPOSED = BxExposed;
window.addEventListener(BxEvent.POPSTATE, onHistoryChanged);
@@ -10173,77 +10178,77 @@ window.addEventListener("popstate", onHistoryChanged);
window.history.pushState = patchHistoryMethod("pushState");
window.history.replaceState = patchHistoryMethod("replaceState");
BxEventBus.Script.once("xcloud.server.unavailable", () => {
- if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();
+if (STATES.supportedRegion = !1, window.setTimeout(HeaderSection.watchHeader, 2000), document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();
});
BxEventBus.Script.on("xcloud.server.ready", () => {
- STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);
+STATES.isSignedIn = !0, window.setTimeout(HeaderSection.watchHeader, 2000);
});
BxEventBus.Stream.on("state.loading", () => {
- if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
- else STATES.currentStream.titleSlug = "remote-play";
+if (window.location.pathname.includes("/launch/") && STATES.currentStream.titleInfo) STATES.currentStream.titleSlug = productTitleToSlug(STATES.currentStream.titleInfo.product.title);
+else STATES.currentStream.titleSlug = "remote-play";
});
getGlobalPref("loadingScreen.gameArt.show") && BxEventBus.Script.on("titleInfo.ready", LoadingScreen.setup);
BxEventBus.Stream.on("state.starting", () => {
- LoadingScreen.hide();
- {
- let cursorHider = MouseCursorHider.getInstance();
- if (cursorHider) cursorHider.start(), cursorHider.hide();
- }
+LoadingScreen.hide();
+{
+let cursorHider = MouseCursorHider.getInstance();
+if (cursorHider) cursorHider.start(), cursorHider.hide();
+}
});
BxEventBus.Stream.on("state.playing", (payload) => {
- window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0, StreamUiHandler.observe();
- {
- let gameBar = GameBar.getInstance();
- if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar();
- KeyboardShortcutHandler.getInstance().start();
- let $video = payload.$video;
- if (ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), getStreamPref("localCoOp.enabled")) BxExposed.toggleLocalCoOp(!0), Toast.show(t("local-co-op"), t("enabled"));
- }
- updateVideoPlayer();
+window.BX_STREAM_SETTINGS = StreamSettings.settings, StreamSettings.refreshAllSettings(), STATES.isPlaying = !0, StreamUiHandler.observe();
+{
+let gameBar = GameBar.getInstance();
+if (gameBar) gameBar.reset(), gameBar.enable(), gameBar.showBar();
+KeyboardShortcutHandler.getInstance().start();
+let $video = payload.$video;
+if (ScreenshotManager.getInstance().updateCanvasSize($video.videoWidth, $video.videoHeight), getStreamPref("localCoOp.enabled")) BxExposed.toggleLocalCoOp(!0), Toast.show(t("local-co-op"), t("enabled"));
+}
+updateVideoPlayer();
});
BxEventBus.Stream.on("state.error", () => {
- BxEventBus.Stream.emit("state.stopped", {});
+BxEventBus.Stream.emit("state.stopped", {});
});
window.addEventListener(BxEvent.XCLOUD_RENDERING_COMPONENT, (e) => {
- if (e.component === "product-detail") ProductDetailsPage.injectButtons();
+if (e.component === "product-detail") ProductDetailsPage.injectButtons();
});
BxEventBus.Stream.on("dataChannelCreated", (payload) => {
- let { dataChannel } = payload;
- if (dataChannel?.label !== "message") return;
- dataChannel.addEventListener("message", async (msg) => {
- if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
- if (!msg.data.includes("/titleinfo")) return;
- let currentStream = STATES.currentStream, json = JSON.parse(JSON.parse(msg.data).content), currentId = currentStream.xboxTitleId ?? null, newId = parseInt(json.titleid, 16);
- if (STATES.remotePlay.isPlaying) if (currentStream.titleSlug = "remote-play", json.focused) {
- let productTitle = await XboxApi.getProductTitle(newId);
- if (productTitle) currentStream.titleSlug = productTitleToSlug(productTitle);
- else newId = -1;
- } else newId = 0;
- if (currentId !== newId) currentStream.xboxTitleId = newId, BxEventBus.Stream.emit("xboxTitleId.changed", {
- id: newId
- });
- });
+let { dataChannel } = payload;
+if (dataChannel?.label !== "message") return;
+dataChannel.addEventListener("message", async (msg) => {
+if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;
+if (!msg.data.includes("/titleinfo")) return;
+let currentStream = STATES.currentStream, json = JSON.parse(JSON.parse(msg.data).content), currentId = currentStream.xboxTitleId ?? null, newId = parseInt(json.titleid, 16);
+if (STATES.remotePlay.isPlaying) if (currentStream.titleSlug = "remote-play", json.focused) {
+let productTitle = await XboxApi.getProductTitle(newId);
+if (productTitle) currentStream.titleSlug = productTitleToSlug(productTitle);
+else newId = -1;
+} else newId = 0;
+if (currentId !== newId) currentStream.xboxTitleId = newId, BxEventBus.Stream.emit("xboxTitleId.changed", {
+id: newId
+});
+});
});
function unload() {
- if (!STATES.isPlaying) return;
- KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });
+if (!STATES.isPlaying) return;
+KeyboardShortcutHandler.getInstance().stop(), EmulatedMkbHandler.getInstance()?.destroy(), NativeMkbHandler.getInstance()?.destroy(), DeviceVibrationManager.getInstance()?.reset(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1, NavigationDialogManager.getInstance().hide(), StreamStats.getInstance().destroy(), StreamBadges.getInstance().destroy(), MouseCursorHider.getInstance()?.stop(), TouchController.reset(), GameBar.getInstance()?.disable(), BxEventBus.Stream.emit("xboxTitleId.changed", { id: -1 });
}
BxEventBus.Stream.on("state.stopped", unload);
window.addEventListener("pagehide", (e) => {
- BxEventBus.Stream.emit("state.stopped", {});
+BxEventBus.Stream.emit("state.stopped", {});
});
window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {
- ScreenshotManager.getInstance().takeScreenshot();
+ScreenshotManager.getInstance().takeScreenshot();
});
function main() {
- if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {
- let customList = getGlobalPref("nativeMkb.forcedGames");
- BX_FLAGS.ForceNativeMkbTitles.push(...customList);
- }
- if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
- if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();
- if (getGlobalPref("touchController.mode") === "all") TouchController.setup();
- if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
- if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
+if (GhPagesUtils.fetchLatestCommit(), getGlobalPref("nativeMkb.mode") !== "off") {
+let customList = getGlobalPref("nativeMkb.forcedGames");
+BX_FLAGS.ForceNativeMkbTitles.push(...customList);
+}
+if (StreamSettings.setup(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getGlobalPref("audio.volume.booster.enabled") && patchAudioContext(), getGlobalPref("block.tracking")) patchMeControl(), disableAdobeAudienceManager();
+if (RootDialogObserver.waitForRootDialog(), addCss(), GuideMenu.getInstance().addEventListeners(), StreamStatsCollector.setupEvents(), StreamBadges.setupEvents(), StreamStats.setupEvents(), STATES.userAgent.capabilities.touch && TouchController.updateCustomList(), DeviceVibrationManager.getInstance(), BX_FLAGS.CheckForUpdate && checkForUpdate(), Patcher.init(), disablePwa(), getGlobalPref("xhome.enabled")) RemotePlayManager.detect();
+if (getGlobalPref("touchController.mode") === "all") TouchController.setup();
+if (AppInterface && (getGlobalPref("mkb.enabled") || getGlobalPref("nativeMkb.mode") === "on")) STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
+if (getGlobalPref("ui.gameCard.waitTime.show") && GameTile.setup(), EmulatedMkbHandler.setupEvents(), getGlobalPref("ui.controllerStatus.show")) window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
}
main();